@ryusenkei/maezae 0.1.0 → 0.2.0

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
@@ -10,6 +10,7 @@ maezae の SDK(導入先のサイト向け)
10
10
  | `@ryusenkei/maezae/body` | 本文のブロックの型と描画の補助 |
11
11
  | `@ryusenkei/maezae/model` | コンテンツスキーマの型・ページのパス・画像の切り抜き |
12
12
  | `@ryusenkei/maezae/preview` | プレビューと下書きの取得の署名(ヘッダー `x-preview-signature`) |
13
+ | `@ryusenkei/maezae/integrations` | 外の関数・Webhook が受け取る本文の型と署名の検証(ヘッダー `x-cms-signature`) |
13
14
 
14
15
  ## 取得(Cloudflare Workers の service binding)
15
16
 
@@ -35,3 +36,14 @@ import { SIGNED_HEADERS, verifyRequest } from '@ryusenkei/maezae/preview';
35
36
 
36
37
  const isPreview = await verifyRequest(env.PREVIEW_TOKEN, request, { header: SIGNED_HEADERS.preview });
37
38
  ```
39
+
40
+ ## 外の関数・Webhook の受け手
41
+
42
+ - 鍵: 媒体の管理画面の「設定 → 連携」の登録ごとの署名の鍵
43
+
44
+ ```ts
45
+ import { type PublishCheckRequest, SIGNED_HEADERS, verifyRequest } from '@ryusenkei/maezae/integrations';
46
+
47
+ if (!(await verifyRequest(env.CMS_FUNCTION_SECRET, request, { header: SIGNED_HEADERS.outbound }))) return new Response(null, { status: 401 });
48
+ const body = (await request.json()) as PublishCheckRequest;
49
+ ```
package/dist/body.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ComponentBlock, c as InlineComponent, d as ListItem, f as Mark, h as Text, i as Body, l as Link, m as StandardBlock, o as HeadingLevel, p as Rich, r as Block, s as Inline, t as blockSchemaOf, u as List } from "./index-D11fUsoi.js";
1
+ import { _ as Rich, a as PropType, c as Body, d as Inline, f as InlineComponent, g as Mark, h as ListItem, i as PropSchema, l as ComponentBlock, m as List, o as PropsSchema, p as Link, r as DeclaredComponent, s as Block, t as blockSchemaOf, u as HeadingLevel, v as StandardBlock, y as Text } from "./index-C8duwjns.js";
2
2
  /** meta description の長さの上限(検索結果で省略されにくい字数の目安) */
3
3
  export declare const DESCRIPTION_MAX = 120;
4
4
  /**
@@ -21,4 +21,4 @@ export declare const readMinOf: <M>(body: Body<M>, speed?: number) => number;
21
21
  export declare const linksOf: <M>(body: Body<M>) => string[];
22
22
  /** 本文のリンク先として許可する形か(保存の検査とサイトの出力で共通) */
23
23
  export declare const isSafeHref: (href: string) => boolean;
24
- export { type Block, type Body, type ComponentBlock, type HeadingLevel, type Inline, type InlineComponent, type Link, type List, type ListItem, type Mark, type Rich, type StandardBlock, type Text, blockSchemaOf };
24
+ export { type Block, type Body, type ComponentBlock, type DeclaredComponent, type HeadingLevel, type Inline, type InlineComponent, type Link, type List, type ListItem, type Mark, type PropSchema, type PropType, type PropsSchema, type Rich, type StandardBlock, type Text, blockSchemaOf };
package/dist/body.js CHANGED
@@ -1,18 +1,14 @@
1
1
  import { z } from "zod";
2
- // ../body/src/description.ts
3
- /** meta description の長さの上限(検索結果で省略されにくい字数の目安) */
4
- const DESCRIPTION_MAX = 120;
5
- /**
6
- * 説明を meta description の長さに調整
7
- * - 上限を超える場合: 上限までの最後の句点まで
8
- * - 句点がない場合: 上限で切って省略の記号
9
- */
10
- function descriptionOf(excerpt, max = 120) {
11
- if ([...excerpt].length <= max) return excerpt;
12
- const head = [...excerpt].slice(0, max).join("");
13
- const end = head.lastIndexOf("。");
14
- return end > 0 ? head.slice(0, end + 1) : `${[...head].slice(0, max - 1).join("")}…`;
15
- }
2
+ // ../body/src/components.ts
3
+ /** 予約の名前(ブロック) */
4
+ const RESERVED_BLOCK_NAMES = /* @__PURE__ */ new Set(["block"]);
5
+ /** 予約の名前(インライン) */
6
+ const RESERVED_INLINE_NAMES = /* @__PURE__ */ new Set([
7
+ "mark",
8
+ "sup",
9
+ "sub",
10
+ "link"
11
+ ]);
16
12
  // ../body/src/library.ts
17
13
  /** 注のコンポーネントの名前(本文からの算出 citesOf が参照) */
18
14
  const CITE = "cite";
@@ -231,6 +227,125 @@ const CORE_COMPONENTS = [
231
227
  ];
232
228
  /** 名前から核のコンポーネント(なければ undefined) */
233
229
  const coreComponentOf = (name) => CORE_COMPONENTS.find((c) => c.name === name);
230
+ // ../body/src/declared.ts
231
+ const NAME = /^[A-Za-z][A-Za-z0-9-]*$/;
232
+ const KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
233
+ const propSchema = z.lazy(() => z.object({
234
+ type: z.enum([
235
+ "string",
236
+ "number",
237
+ "integer",
238
+ "boolean",
239
+ "array",
240
+ "object"
241
+ ]),
242
+ title: z.string().min(1).optional(),
243
+ description: z.string().optional(),
244
+ enum: z.array(z.string().min(1)).min(1).optional(),
245
+ items: propSchema.optional(),
246
+ properties: z.record(z.string().regex(KEY, "引数の名前は英字で始まる英数字"), propSchema).optional(),
247
+ required: z.array(z.string()).optional()
248
+ }).strict().superRefine((p, ctx) => {
249
+ if (p.enum && p.type !== "string") ctx.addIssue({
250
+ code: "custom",
251
+ message: "選択肢 enum は文字列の引数だけ",
252
+ path: ["enum"]
253
+ });
254
+ if (p.type === "array" !== (p.items !== void 0)) ctx.addIssue({
255
+ code: "custom",
256
+ message: "配列の引数は items が必要(ほかの型は不可)",
257
+ path: ["items"]
258
+ });
259
+ if ((p.properties || p.required) && p.type !== "object") ctx.addIssue({
260
+ code: "custom",
261
+ message: "properties・required はオブジェクトの引数だけ",
262
+ path: ["properties"]
263
+ });
264
+ for (const key of p.required ?? []) if (!p.properties?.[key]) ctx.addIssue({
265
+ code: "custom",
266
+ message: `必須の引数 ${key} が properties にない`,
267
+ path: ["required"]
268
+ });
269
+ }));
270
+ const declaredComponent = z.object({
271
+ name: z.string().regex(NAME, "部品の名前は英字で始まる英数字とハイフン"),
272
+ label: z.string().min(1, "部品の表示名が未入力"),
273
+ description: z.string().optional(),
274
+ inline: z.boolean().optional(),
275
+ props: z.object({
276
+ type: z.literal("object"),
277
+ properties: z.record(z.string().regex(KEY, "引数の名前は英字で始まる英数字"), propSchema),
278
+ required: z.array(z.string()).optional()
279
+ }).strict()
280
+ }).strict().superRefine((d, ctx) => {
281
+ if (coreComponentOf(d.name) || RESERVED_BLOCK_NAMES.has(d.name) || RESERVED_INLINE_NAMES.has(d.name)) ctx.addIssue({
282
+ code: "custom",
283
+ message: `部品の名前 ${d.name} はコアの部品・予約の名前と重複`,
284
+ path: ["name"]
285
+ });
286
+ for (const key of d.props.required ?? []) if (!d.props.properties[key]) ctx.addIssue({
287
+ code: "custom",
288
+ message: `必須の引数 ${key} が properties にない`,
289
+ path: ["props", "required"]
290
+ });
291
+ if (d.inline) {
292
+ for (const [key, p] of Object.entries(d.props.properties)) if (p.type !== "string") ctx.addIssue({
293
+ code: "custom",
294
+ message: `インラインの部品の引数は文字列だけ: ${key}`,
295
+ path: [
296
+ "props",
297
+ "properties",
298
+ key
299
+ ]
300
+ });
301
+ }
302
+ });
303
+ z.array(declaredComponent).superRefine((list, ctx) => {
304
+ const seen = /* @__PURE__ */ new Set();
305
+ list.forEach((d, i) => {
306
+ if (seen.has(d.name)) ctx.addIssue({
307
+ code: "custom",
308
+ message: `部品の名前 ${d.name} が重複`,
309
+ path: [i, "name"]
310
+ });
311
+ seen.add(d.name);
312
+ });
313
+ });
314
+ /** 1つの引数の検査(label はメッセージの項目名) */
315
+ function propZod(p, label) {
316
+ const wrong = { error: (iss) => iss.input === void 0 ? `${label}が未入力` : `${label}の形が不正` };
317
+ switch (p.type) {
318
+ case "string": return p.enum ? z.enum(p.enum, wrong) : z.string(wrong);
319
+ case "number": return z.number(wrong);
320
+ case "integer": return z.number(wrong).int(`${label}は整数`);
321
+ case "boolean": return z.boolean(wrong);
322
+ case "array": return z.array(propZod(p.items, label), wrong);
323
+ case "object": return objectZod(p.properties ?? {}, p.required ?? [], wrong);
324
+ }
325
+ }
326
+ /** オブジェクトの引数の検査(必須でない引数は省略可、宣言にない引数は除去) */
327
+ const objectZod = (properties, required, error) => z.object(Object.fromEntries(Object.entries(properties).map(([key, p]) => [key, required.includes(key) ? propZod(p, p.title ?? key) : propZod(p, p.title ?? key).optional()])), error);
328
+ const propsCache = /* @__PURE__ */ new WeakMap();
329
+ /** 宣言した部品の引数の検査(宣言ごとに作成を再利用) */
330
+ function declaredPropsZod(d) {
331
+ let schema = propsCache.get(d);
332
+ if (!schema) propsCache.set(d, schema = objectZod(d.props.properties, d.props.required ?? [], { error: () => `${d.label}の引数の形が不正` }));
333
+ return schema;
334
+ }
335
+ // ../body/src/description.ts
336
+ /** meta description の長さの上限(検索結果で省略されにくい字数の目安) */
337
+ const DESCRIPTION_MAX = 120;
338
+ /**
339
+ * 説明を meta description の長さに調整
340
+ * - 上限を超える場合: 上限までの最後の句点まで
341
+ * - 句点がない場合: 上限で切って省略の記号
342
+ */
343
+ function descriptionOf(excerpt, max = 120) {
344
+ if ([...excerpt].length <= max) return excerpt;
345
+ const head = [...excerpt].slice(0, max).join("");
346
+ const end = head.lastIndexOf("。");
347
+ return end > 0 ? head.slice(0, end + 1) : `${[...head].slice(0, max - 1).join("")}…`;
348
+ }
234
349
  // ../body/src/derive.ts
235
350
  /** 読了時間の既定の分速(字/分) */
236
351
  const READING_SPEED = 500;
@@ -412,42 +527,61 @@ const text = z.object({
412
527
  marks: z.array(z.enum(MARKS)).optional(),
413
528
  link: link.optional()
414
529
  });
415
- const inlineComponent = z.object({
530
+ /** 部品の props の検査(名前で核のコンポーネント、なければ宣言した部品、どちらもないか種類が違えば null) */
531
+ function propsOf(name, inline, media, declared) {
532
+ const core = coreComponentOf(name);
533
+ if (core) return Boolean(core.inline) === inline ? core.props(media) : null;
534
+ const d = declared.find((c) => c.name === name);
535
+ return d && Boolean(d.inline) === inline ? declaredPropsZod(d) : null;
536
+ }
537
+ const inlineComponentOf = (declared) => z.object({
416
538
  component: z.string().min(1),
417
539
  props: z.record(z.string(), z.string()).optional(),
418
540
  text: z.string().optional()
419
541
  }).superRefine((c, ctx) => {
420
- const def = coreComponentOf(c.component);
421
- if (!def?.inline) return void ctx.addIssue({
542
+ const props = propsOf(c.component, true, z.never(), declared);
543
+ if (!props) return void ctx.addIssue({
422
544
  code: "custom",
423
545
  message: `インラインのコンポーネント ${c.component} はない`
424
546
  });
425
- const r = def.props(z.never()).safeParse(c.props ?? {});
547
+ const r = props.safeParse(c.props ?? {});
426
548
  if (!r.success) for (const issue of r.error.issues) ctx.addIssue({
427
549
  code: "custom",
428
550
  message: issue.message,
429
551
  path: ["props", ...issue.path]
430
552
  });
431
553
  });
432
- const inline = z.union([
433
- z.string(),
434
- inlineComponent,
435
- text
436
- ]);
437
- const richOf = (cites) => z.array(inline).transform((items) => cites ? items.flatMap((item) => typeof item === "string" ? liftCites(item) : [item]) : items);
554
+ /** インラインの1要素(component を持つ要素は部品としてだけ検査、union で文字の形に落ちて部品の検査を素通りしないため) */
555
+ function inlineItemOf(declared) {
556
+ const component = inlineComponentOf(declared);
557
+ const plain = z.union([z.string(), text]);
558
+ return z.unknown().transform((value, ctx) => {
559
+ const r = (typeof value === "object" && value !== null && "component" in value ? component : plain).safeParse(value);
560
+ if (r.success) return r.data;
561
+ for (const issue of r.error.issues) ctx.addIssue({
562
+ code: "custom",
563
+ message: issue.message,
564
+ path: issue.path
565
+ });
566
+ return z.NEVER;
567
+ });
568
+ }
569
+ const richOf = (cites, declared) => z.array(inlineItemOf(declared)).transform((items) => cites ? items.flatMap((item) => typeof item === "string" ? liftCites(item) : [item]) : items);
438
570
  const variant = z.string().optional();
439
- /** 本文の1ブロックの検査(profile は本文の設定、media は画像の参照の検査) */
440
- function blockSchemaOf(profile, media) {
441
- const rich = richOf(profile.components.includes(CITE));
442
- const list = z.lazy(() => z.object({
571
+ /** 本文の1ブロックの検査(profile は本文の設定、media は画像の参照の検査、declared は導入先が宣言した部品) */
572
+ function blockSchemaOf(profile, media, declared = []) {
573
+ const rich = richOf(profile.components.includes(CITE), declared);
574
+ const list = z.object({
443
575
  type: z.literal("list"),
444
576
  ordered: z.boolean().optional(),
445
577
  items: z.array(z.object({
446
578
  content: rich,
447
- children: list.optional()
579
+ get children() {
580
+ return list.optional();
581
+ }
448
582
  })),
449
583
  variant
450
- }));
584
+ });
451
585
  const heading = z.object({
452
586
  type: z.literal("heading"),
453
587
  level: z.union([
@@ -462,7 +596,7 @@ function blockSchemaOf(profile, media) {
462
596
  id: z.string().optional(),
463
597
  variant
464
598
  });
465
- return z.union([
599
+ return z.discriminatedUnion("type", [
466
600
  z.object({
467
601
  type: z.literal("paragraph"),
468
602
  content: rich,
@@ -513,15 +647,15 @@ function blockSchemaOf(profile, media) {
513
647
  ]).transform((block, ctx) => {
514
648
  let out = block;
515
649
  if (out.type === "component") {
516
- const def = coreComponentOf(out.name);
517
- if (!def || def.inline) {
650
+ const props = propsOf(out.name, false, media, declared);
651
+ if (!props) {
518
652
  ctx.addIssue({
519
653
  code: "custom",
520
654
  message: `コンポーネント ${out.name} はない`
521
655
  });
522
656
  return z.NEVER;
523
657
  }
524
- const r = def.props(media).safeParse(out.props);
658
+ const r = props.safeParse(out.props);
525
659
  if (!r.success) {
526
660
  for (const issue of r.error.issues) ctx.addIssue({
527
661
  code: "custom",
@@ -1,5 +1,5 @@
1
- import { C as FieldDef, D as PageEntry, E as PageDef, S as CountDef, _ as Media, a as CropRect, b as ContentModel, h as Hotspot, m as Crop, n as DateString, t as Clock, w as Publishing, x as ContentType, y as ContentFormat } from "./index-57nirXxn.js";
2
- import { i as Body } from "./index-D11fUsoi.js";
1
+ import { C as ContentType, E as Publishing, O as PageDef, S as ContentModel, T as FieldDef, _ as Hotspot, g as Crop, k as PageEntry, n as DateString, s as CropRect, t as Clock, w as CountDef, x as ContentFormat, y as Media } from "./index-CLDe_wBc.js";
2
+ import { c as Body } from "./index-C8duwjns.js";
3
3
  /** 配信 API の1件の値(項目の key と id・updated_at) */
4
4
  type Item = Readonly<Record<string, unknown>>;
5
5
  /** 配信 API が解決した実体(hotspot・crop は元の画像に対する割合) */
@@ -90,6 +90,32 @@ type ComponentBlock = Readonly<{
90
90
  type Block<M> = StandardBlock<M> | ComponentBlock;
91
91
  /** 本文 */
92
92
  type Body<M> = readonly Block<M>[];
93
+ /** 引数の型 */
94
+ type PropType = 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object';
95
+ /** 1つの引数の形(title は管理画面の項目名、enum は文字列の選択肢、items は配列の要素、properties・required はオブジェクトの中) */
96
+ type PropSchema = Readonly<{
97
+ type: PropType;
98
+ title?: string;
99
+ description?: string;
100
+ enum?: readonly string[];
101
+ items?: PropSchema;
102
+ properties?: Readonly<Record<string, PropSchema>>;
103
+ required?: readonly string[];
104
+ }>;
105
+ /** 引数の全体の形(最上位はオブジェクト) */
106
+ type PropsSchema = Readonly<{
107
+ type: 'object';
108
+ properties: Readonly<Record<string, PropSchema>>;
109
+ required?: readonly string[];
110
+ }>;
111
+ /** 導入先が宣言する部品(label は管理画面の表示名、inline の省略はブロック) */
112
+ type DeclaredComponent = Readonly<{
113
+ name: string;
114
+ label: string;
115
+ description?: string;
116
+ inline?: boolean;
117
+ props: PropsSchema;
118
+ }>;
93
119
  /** 本文の設定 */
94
120
  type BodyProfile = Readonly<{
95
121
  headings: readonly HeadingLevel[];
@@ -126,6 +152,6 @@ type BodyProfile = Readonly<{
126
152
  /** 使用できるコンポーネントの名前(ブロック・インライン) */
127
153
  components: readonly string[];
128
154
  }>;
129
- /** 本文の1ブロックの検査(profile は本文の設定、media は画像の参照の検査) */
130
- declare function blockSchemaOf<M>(profile: BodyProfile, media: z.ZodType<M>): z.ZodType<Block<M>>;
131
- export { ComponentBlock as a, InlineComponent as c, ListItem as d, Mark as f, Text as h, Body as i, Link as l, StandardBlock as m, BodyProfile as n, HeadingLevel as o, Rich as p, Block as r, Inline as s, blockSchemaOf as t, List as u };
155
+ /** 本文の1ブロックの検査(profile は本文の設定、media は画像の参照の検査、declared は導入先が宣言した部品) */
156
+ declare function blockSchemaOf<M>(profile: BodyProfile, media: z.ZodType<M>, declared?: readonly DeclaredComponent[]): z.ZodType<Block<M>>;
157
+ export { Rich as _, PropType as a, Body as c, Inline as d, InlineComponent as f, Mark as g, ListItem as h, PropSchema as i, ComponentBlock as l, List as m, BodyProfile as n, PropsSchema as o, Link as p, DeclaredComponent as r, Block as s, blockSchemaOf as t, HeadingLevel as u, StandardBlock as v, Text as y };
@@ -1,4 +1,4 @@
1
- import { n as BodyProfile } from "./index-D11fUsoi.js";
1
+ import { n as BodyProfile, r as DeclaredComponent } from "./index-C8duwjns.js";
2
2
  import { z } from "zod";
3
3
  /** ページの行の値(項目の key と値、表の列・集計の値も含む) */
4
4
  type PageEntry = Readonly<Record<string, unknown>>;
@@ -188,6 +188,7 @@ type ContentType = Readonly<{
188
188
  /** コンテンツスキーマ(導入先ごとに1つ) */
189
189
  type ContentModel = Readonly<{
190
190
  types: readonly ContentType[];
191
+ components?: readonly DeclaredComponent[];
191
192
  }>;
192
193
  /** コンテンツスキーマの型(id から、なければ例外: 導入先の設定の誤り) */
193
194
  declare function typeOf(model: ContentModel, id: string): ContentType;
@@ -309,6 +310,10 @@ type CropRect = Readonly<{
309
310
  declare function cropRect(width: number, height: number, crop: Crop): CropRect;
310
311
  /** 元の画像に対する見せたい点を、切り抜いた後の画像に対する割合に変換(範囲の外は端に寄せる) */
311
312
  declare function croppedHotspot(hotspot: Hotspot, crop: Crop): Hotspot;
313
+ /** 画像の型 */
314
+ declare const IMAGE_TYPE: ContentType;
315
+ /** 動画の型(配信と再生は最初に使用する場所が決まったときに実装) */
316
+ declare const VIDEO_TYPE: ContentType;
312
317
  /** 暦の日付(`YYYY-MM-DD`) */
313
318
  type DateString = `${number}-${number}-${number}`;
314
319
  /** 日付だけの値(`YYYY-MM-DD`)か */
@@ -332,4 +337,4 @@ type Clock = Readonly<{
332
337
  }>;
333
338
  /** タイムゾーンの日時の変換を作成 */
334
339
  declare function createClock(timeZone: string): Clock;
335
- export { FieldDef as C, PageEntry as D, PageDef as E, pagePathOf as O, CountDef as S, typeOf as T, Media as _, CropRect as a, ContentModel as b, cropRect as c, stillOf as d, Asset as f, LocalImage as g, Hotspot as h, isDateOnly as i, publicPathOf as k, croppedHotspot as l, Crop as m, DateString as n, coverPosition as o, AssetRef as p, createClock as r, coverStart as s, Clock as t, objectPosition as u, mediaSchema as v, Publishing as w, ContentType as x, ContentFormat as y };
340
+ export { pagePathOf as A, ContentType as C, typeOf as D, Publishing as E, PageDef as O, ContentModel as S, FieldDef as T, Hotspot as _, IMAGE_TYPE as a, mediaSchema as b, coverPosition as c, croppedHotspot as d, objectPosition as f, Crop as g, AssetRef as h, isDateOnly as i, publicPathOf as j, PageEntry as k, coverStart as l, Asset as m, DateString as n, VIDEO_TYPE as o, stillOf as p, createClock as r, CropRect as s, Clock as t, cropRect as u, LocalImage as v, CountDef as w, ContentFormat as x, Media as y };
@@ -0,0 +1,72 @@
1
+ import { r as VerifyRequestOptions, s as verifyRequest, t as SIGNED_HEADERS } from "./signed-B_58gV21.js";
2
+ /** 依頼する時点(save_check は型の保存前、publish_check は提出・公開の前の検査) */
3
+ export declare const FUNCTION_HOOKS: readonly ["save_check", "publish_check"];
4
+ /** 依頼する時点 */
5
+ type FunctionHook = (typeof FUNCTION_HOOKS)[number];
6
+ /** 保存前の検査の本文(id は作成なら null、values は宣言と拡張機能で補正した後の項目の値、user は操作した人のアカウントの ID とロール) */
7
+ type SaveCheckRequest = Readonly<{
8
+ hook: 'save_check';
9
+ type: string;
10
+ id: string | null;
11
+ values: Readonly<Record<string, unknown>>;
12
+ user: Readonly<{
13
+ id: string;
14
+ role: string;
15
+ }>;
16
+ at: string;
17
+ }>;
18
+ /**
19
+ * 保存前の検査の応答(HTTP の状態は 2xx)
20
+ * - problems: 1件でもあれば保存を拒否(path は項目の key、message は画面に表示する理由)
21
+ * - values: 補正する項目だけの値(入力に重ねる、なしは補正なし)
22
+ */
23
+ type SaveCheckResponse = Readonly<{
24
+ problems?: readonly Readonly<{
25
+ path: string;
26
+ message: string;
27
+ }>[];
28
+ values?: Readonly<Record<string, unknown>>;
29
+ }>;
30
+ /** 提出・公開前の検査の本文(stage は submit か publish、values は保存済みの行の値、id はテストの呼び出しだけ null) */
31
+ type PublishCheckRequest = Readonly<{
32
+ hook: 'publish_check';
33
+ type: string;
34
+ id: string | null;
35
+ stage: 'submit' | 'publish';
36
+ values: Readonly<Record<string, unknown>>;
37
+ user: Readonly<{
38
+ id: string;
39
+ role: string;
40
+ }>;
41
+ at: string;
42
+ }>;
43
+ /** 提出・公開前の検査の応答(HTTP の状態は 2xx、problems が1件でもあれば提出・公開を拒否、値の補正はなし) */
44
+ type PublishCheckResponse = Readonly<{
45
+ problems?: readonly Readonly<{
46
+ path: string;
47
+ message: string;
48
+ }>[];
49
+ }>;
50
+ /** 外の関数への依頼の本文 */
51
+ type FunctionRequest = SaveCheckRequest | PublishCheckRequest;
52
+ /** 送れる操作(レビューの依頼・公開・非公開・差し戻し) */
53
+ declare const WEBHOOK_ACTIONS: readonly ["submitted", "published", "unpublished", "rejected"];
54
+ /** 送れる操作 */
55
+ type WebhookAction = (typeof WEBHOOK_ACTIONS)[number];
56
+ /** 送れる出来事(編集機能を持つ型の <型の id>.<操作> と、拡張機能が宣言した出来事) */
57
+ type WebhookEvent = `${string}.${string}`;
58
+ /**
59
+ * json の形式の本文(編集機能を持つ型の出来事、署名はヘッダー x-cms-signature で登録ごとの鍵)
60
+ * - item: 行の ID と題、by: 操作した人の表示名、at: 送信の日時(ISO 8601)
61
+ */
62
+ type WebhookBody = Readonly<{
63
+ event: WebhookEvent;
64
+ type: string;
65
+ item: Readonly<{
66
+ id: string;
67
+ title: string;
68
+ }>;
69
+ by: string;
70
+ at: string;
71
+ }>;
72
+ export { type FunctionHook, type FunctionRequest, type PublishCheckRequest, type PublishCheckResponse, SIGNED_HEADERS, type SaveCheckRequest, type SaveCheckResponse, type VerifyRequestOptions, type WebhookAction, type WebhookBody, type WebhookEvent, verifyRequest };
@@ -0,0 +1,6 @@
1
+ import { a as verifyRequest, t as SIGNED_HEADERS } from "./src-CA_67dWy.js";
2
+ import "./src-BrLwIWaT.js";
3
+ // ../cms-contract/src/functions.ts
4
+ /** 依頼する時点(save_check は型の保存前、publish_check は提出・公開の前の検査) */
5
+ const FUNCTION_HOOKS = ["save_check", "publish_check"];
6
+ export { FUNCTION_HOOKS, SIGNED_HEADERS, verifyRequest };
package/dist/model.d.ts CHANGED
@@ -1,2 +1,82 @@
1
- import { C as FieldDef, D as PageEntry, E as PageDef, O as pagePathOf, T as typeOf, _ as Media, a as CropRect, b as ContentModel, c as cropRect, d as stillOf, f as Asset, g as LocalImage, h as Hotspot, i as isDateOnly, k as publicPathOf, l as croppedHotspot, m as Crop, n as DateString, o as coverPosition, p as AssetRef, r as createClock, s as coverStart, t as Clock, u as objectPosition, v as mediaSchema, x as ContentType } from "./index-57nirXxn.js";
2
- export { type Asset, type AssetRef, type Clock, type ContentModel, type ContentType, type Crop, type CropRect, type DateString, type FieldDef, type Hotspot, type LocalImage, type Media, type PageDef, type PageEntry, coverPosition, coverStart, createClock, cropRect, croppedHotspot, isDateOnly, mediaSchema, objectPosition, pagePathOf, publicPathOf, stillOf, typeOf };
1
+ import { A as pagePathOf, C as ContentType, D as typeOf, O as PageDef, S as ContentModel, T as FieldDef, _ as Hotspot, a as IMAGE_TYPE, b as mediaSchema, c as coverPosition, d as croppedHotspot, f as objectPosition, g as Crop, h as AssetRef, i as isDateOnly, j as publicPathOf, k as PageEntry, l as coverStart, m as Asset, n as DateString, o as VIDEO_TYPE, p as stillOf, r as createClock, s as CropRect, t as Clock, u as cropRect, v as LocalImage, y as Media } from "./index-CLDe_wBc.js";
2
+ /** メニューのグループ(collapsed は既定で折りたたむ) */
3
+ type MenuGroup = Readonly<{
4
+ id: string;
5
+ label: string;
6
+ collapsed?: boolean;
7
+ items: readonly string[];
8
+ }>;
9
+ /** メニューの1段(項目の id かグループ) */
10
+ type MenuEntry = string | MenuGroup;
11
+ /** 編集画面のセクション(label が空は見出しなし、collapsed は既定で折りたたむ) */
12
+ type EditorSection = Readonly<{
13
+ id: string;
14
+ label: string;
15
+ collapsed?: boolean;
16
+ items: readonly string[];
17
+ }>;
18
+ /** コンテンツタイプの編集画面の配置(hidden は配置しない項目) */
19
+ type EditorLayout = Readonly<{
20
+ sections: readonly EditorSection[];
21
+ hidden?: readonly string[];
22
+ }>;
23
+ /** 画面の設定(editors・columns のキーはコンテンツタイプの id) */
24
+ type ScreenSettings = Readonly<{
25
+ menu: readonly MenuEntry[];
26
+ editors: Readonly<Record<string, EditorLayout>>;
27
+ columns: Readonly<Record<string, readonly string[]>>;
28
+ }>;
29
+ /** 操作 */
30
+ declare const ACTIONS: readonly ["read", "create", "update", "publish", "delete"];
31
+ /** 操作 */
32
+ type Action = (typeof ACTIONS)[number];
33
+ /** 段階 */
34
+ declare const LEVELS: readonly ["all", "own", "approval", "none"];
35
+ /** 段階 */
36
+ type Level = (typeof LEVELS)[number];
37
+ /** 権限の表(キーは対象の id、'*' は表にない対象の既定、操作がなければ不可) */
38
+ type Permissions = Readonly<Record<string, Readonly<Partial<Record<Action, Level>>>>>;
39
+ /** ビューで許可できる一括操作(assign: 担当者の割り当て、publish: 公開) */
40
+ declare const VIEW_ACTIONS: readonly ["assign", "publish"];
41
+ /** ビューで許可できる一括操作 */
42
+ type ViewAction = (typeof VIEW_ACTIONS)[number];
43
+ /** ビューの内容(filters は列の id ごとの絞り込みの値、query は検索の文、columns は表示する列、editable は一覧で編集する列) */
44
+ type ViewDefinition = Readonly<{
45
+ filters: Readonly<Record<string, string>>;
46
+ query: string;
47
+ columns: readonly string[];
48
+ editable: readonly string[];
49
+ actions: readonly ViewAction[];
50
+ }>;
51
+ /** スキーマと設定の形式の名前 */
52
+ export declare const CONFIG_FORMAT = "cms-config";
53
+ /** スキーマと設定(schema はコンテンツスキーマ ContentModel・screens は画面設定 ScreenSettings の形、roles は管理者以外、views はチームのビュー) */
54
+ type ConfigBundle = Readonly<{
55
+ format: typeof CONFIG_FORMAT;
56
+ version: 1;
57
+ exported_at: string;
58
+ schema: unknown;
59
+ screens: unknown;
60
+ roles: readonly Readonly<{
61
+ id: string;
62
+ label: string;
63
+ permissions: Permissions;
64
+ }>[];
65
+ views: readonly Readonly<{
66
+ id: string;
67
+ type: string;
68
+ label: string;
69
+ definition: ViewDefinition;
70
+ roles: readonly string[];
71
+ }>[];
72
+ extensions: readonly Readonly<{
73
+ id: string;
74
+ enabled: boolean;
75
+ settings: Readonly<Record<string, unknown>>;
76
+ }>[];
77
+ }>;
78
+ /** 中立の既定のスキーマ(新しい媒体の作成で適用する型: post・author・tag・image・video、設定の移入の土台) */
79
+ export declare const DEFAULT_SCHEMA: ContentModel;
80
+ /** 既定の画面設定(新しい媒体の作成で適用する中立の値、移入する screens の土台) */
81
+ export declare const DEFAULT_SCREENS: ScreenSettings;
82
+ export { type Asset, type AssetRef, type Clock, type ConfigBundle, type ContentModel, type ContentType, type Crop, type CropRect, type DateString, type FieldDef, type Hotspot, IMAGE_TYPE, type LocalImage, type Media, type PageDef, type PageEntry, type ScreenSettings, VIDEO_TYPE, coverPosition, coverStart, createClock, cropRect, croppedHotspot, isDateOnly, mediaSchema, objectPosition, pagePathOf, publicPathOf, stillOf, typeOf };
package/dist/model.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { a as coverPosition, c as croppedHotspot, d as createClock, f as isDateOnly, i as typeOf, l as objectPosition, n as publicPathOf, o as coverStart, s as cropRect, t as pagePathOf, u as stillOf } from "./page-UEKb7_gu.js";
2
+ import "./src-BrLwIWaT.js";
2
3
  import { z } from "zod";
3
4
  // ../model/src/asset.ts
4
5
  const unit = z.number().min(0).max(1);
@@ -28,4 +29,341 @@ const mediaSchema = z.union([mediaBase.extend({ kind: z.literal("image") }), med
28
29
  poster: mediaBase
29
30
  })]);
30
31
  z.object({ asset: z.string().min(1) });
31
- export { coverPosition, coverStart, createClock, cropRect, croppedHotspot, isDateOnly, mediaSchema, objectPosition, pagePathOf, publicPathOf, stillOf, typeOf };
32
+ // ../model/src/media.ts
33
+ const file = [
34
+ {
35
+ key: "mime_type",
36
+ label: "形式",
37
+ type: "text",
38
+ required: "save",
39
+ description: "MIME タイプ"
40
+ },
41
+ {
42
+ key: "size",
43
+ label: "サイズ",
44
+ type: "number",
45
+ required: "save",
46
+ description: "バイト数"
47
+ },
48
+ {
49
+ key: "width",
50
+ label: "幅",
51
+ type: "number",
52
+ required: "save",
53
+ description: "px"
54
+ },
55
+ {
56
+ key: "height",
57
+ label: "高さ",
58
+ type: "number",
59
+ required: "save",
60
+ description: "px"
61
+ },
62
+ {
63
+ key: "alt",
64
+ label: "代替テキスト",
65
+ type: "text",
66
+ description: "使用する側の既定の値、使用する場所ごとに上書き可"
67
+ }
68
+ ];
69
+ /** 画像の型 */
70
+ const IMAGE_TYPE = {
71
+ id: "image",
72
+ label: "画像",
73
+ description: "見せたい点(hotspot)と切り抜き(crop)を画像ごとに1つ保持、切り抜く場所すべてで共通",
74
+ core: file,
75
+ fields: []
76
+ };
77
+ /** 動画の型(配信と再生は最初に使用する場所が決まったときに実装) */
78
+ const VIDEO_TYPE = {
79
+ id: "video",
80
+ label: "動画",
81
+ core: [
82
+ ...file,
83
+ {
84
+ key: "duration",
85
+ label: "長さ",
86
+ type: "number",
87
+ description: "秒"
88
+ },
89
+ {
90
+ key: "poster",
91
+ label: "ポスター画像",
92
+ type: "media",
93
+ accept: ["image"],
94
+ description: "読み込み前・動きを抑える設定・OG で代わりに表示"
95
+ }
96
+ ],
97
+ fields: []
98
+ };
99
+ // ../cms-contract/src/transfer.ts
100
+ /** スキーマと設定の形式の名前 */
101
+ const CONFIG_FORMAT = "cms-config";
102
+ // src/defaults.ts
103
+ /** 中立の既定のスキーマ(新しい媒体の作成で適用する型: post・author・tag・image・video、設定の移入の土台) */
104
+ const DEFAULT_SCHEMA = { "types": [
105
+ {
106
+ "id": "post",
107
+ "label": "投稿",
108
+ "table": "articles",
109
+ "publishing": "review",
110
+ "core": [
111
+ {
112
+ "key": "title",
113
+ "label": "タイトル",
114
+ "type": "text",
115
+ "required": "save"
116
+ },
117
+ {
118
+ "key": "writers",
119
+ "label": "著者",
120
+ "type": "reference",
121
+ "to": "author",
122
+ "multiple": true
123
+ },
124
+ {
125
+ "key": "image",
126
+ "label": "メインの画像",
127
+ "type": "media",
128
+ "accept": ["image"]
129
+ },
130
+ {
131
+ "key": "body",
132
+ "label": "本文",
133
+ "type": "body",
134
+ "profile": {
135
+ "headings": [
136
+ 2,
137
+ 3,
138
+ 4
139
+ ],
140
+ "marks": [
141
+ "strong",
142
+ "em",
143
+ "code",
144
+ "strike"
145
+ ],
146
+ "list": {
147
+ "ordered": true,
148
+ "nested": true
149
+ },
150
+ "quote": {
151
+ "multiple": true,
152
+ "cite": true
153
+ },
154
+ "code": { "caption": false },
155
+ "table": {
156
+ "head": "optional",
157
+ "caption": "optional"
158
+ },
159
+ "divider": true,
160
+ "components": ["gallery", "link-card"]
161
+ }
162
+ },
163
+ {
164
+ "key": "published_at",
165
+ "label": "公開日時",
166
+ "type": "datetime"
167
+ },
168
+ {
169
+ "key": "revised_at",
170
+ "label": "更新日時",
171
+ "type": "datetime"
172
+ }
173
+ ],
174
+ "fields": [{
175
+ "key": "tags",
176
+ "label": "タグ",
177
+ "type": "reference",
178
+ "to": "tag",
179
+ "multiple": true
180
+ }],
181
+ "pages": [{
182
+ "path": "/posts/{id}",
183
+ "public": [{
184
+ "key": "status",
185
+ "in": ["published"]
186
+ }, {
187
+ "key": "published_at",
188
+ "notFuture": true
189
+ }]
190
+ }]
191
+ },
192
+ {
193
+ "id": "author",
194
+ "label": "著者",
195
+ "fields": [
196
+ {
197
+ "key": "name",
198
+ "label": "名前",
199
+ "type": "text",
200
+ "required": "save"
201
+ },
202
+ {
203
+ "key": "bio",
204
+ "label": "紹介",
205
+ "type": "textarea"
206
+ },
207
+ {
208
+ "key": "portrait",
209
+ "label": "顔の画像",
210
+ "type": "media",
211
+ "accept": ["image"]
212
+ }
213
+ ],
214
+ "pages": [{ "path": "/authors/{id}" }]
215
+ },
216
+ {
217
+ "id": "tag",
218
+ "label": "タグ",
219
+ "fields": [{
220
+ "key": "label",
221
+ "label": "名前",
222
+ "type": "text",
223
+ "required": "save"
224
+ }]
225
+ },
226
+ {
227
+ "id": "image",
228
+ "label": "画像",
229
+ "description": "見せたい点(hotspot)と切り抜き(crop)を画像ごとに1つ保持、切り抜く場所すべてで共通",
230
+ "core": [
231
+ {
232
+ "key": "mime_type",
233
+ "label": "形式",
234
+ "type": "text",
235
+ "required": "save",
236
+ "description": "MIME タイプ"
237
+ },
238
+ {
239
+ "key": "size",
240
+ "label": "サイズ",
241
+ "type": "number",
242
+ "required": "save",
243
+ "description": "バイト数"
244
+ },
245
+ {
246
+ "key": "width",
247
+ "label": "幅",
248
+ "type": "number",
249
+ "required": "save",
250
+ "description": "px"
251
+ },
252
+ {
253
+ "key": "height",
254
+ "label": "高さ",
255
+ "type": "number",
256
+ "required": "save",
257
+ "description": "px"
258
+ },
259
+ {
260
+ "key": "alt",
261
+ "label": "代替テキスト",
262
+ "type": "text",
263
+ "description": "使用する側の既定の値、使用する場所ごとに上書き可"
264
+ }
265
+ ],
266
+ "fields": []
267
+ },
268
+ {
269
+ "id": "video",
270
+ "label": "動画",
271
+ "core": [
272
+ {
273
+ "key": "mime_type",
274
+ "label": "形式",
275
+ "type": "text",
276
+ "required": "save",
277
+ "description": "MIME タイプ"
278
+ },
279
+ {
280
+ "key": "size",
281
+ "label": "サイズ",
282
+ "type": "number",
283
+ "required": "save",
284
+ "description": "バイト数"
285
+ },
286
+ {
287
+ "key": "width",
288
+ "label": "幅",
289
+ "type": "number",
290
+ "required": "save",
291
+ "description": "px"
292
+ },
293
+ {
294
+ "key": "height",
295
+ "label": "高さ",
296
+ "type": "number",
297
+ "required": "save",
298
+ "description": "px"
299
+ },
300
+ {
301
+ "key": "alt",
302
+ "label": "代替テキスト",
303
+ "type": "text",
304
+ "description": "使用する側の既定の値、使用する場所ごとに上書き可"
305
+ },
306
+ {
307
+ "key": "duration",
308
+ "label": "長さ",
309
+ "type": "number",
310
+ "description": "秒"
311
+ },
312
+ {
313
+ "key": "poster",
314
+ "label": "ポスター画像",
315
+ "type": "media",
316
+ "accept": ["image"],
317
+ "description": "読み込み前・動きを抑える設定・OG で代わりに表示"
318
+ }
319
+ ],
320
+ "fields": []
321
+ }
322
+ ] };
323
+ /** 既定の画面設定(新しい媒体の作成で適用する中立の値、移入する screens の土台) */
324
+ const DEFAULT_SCREENS = {
325
+ "menu": [
326
+ "dashboard",
327
+ "calendar",
328
+ {
329
+ "id": "content",
330
+ "label": "コンテンツ",
331
+ "items": ["type:post", "media"]
332
+ },
333
+ {
334
+ "id": "masters",
335
+ "label": "マスタ",
336
+ "items": ["type:tag", "type:author"]
337
+ }
338
+ ],
339
+ "editors": { "post": { "sections": [
340
+ {
341
+ "id": "classify",
342
+ "label": "分類",
343
+ "items": ["writers", "tags"]
344
+ },
345
+ {
346
+ "id": "publish",
347
+ "label": "公開",
348
+ "items": ["published_at", "revised_at"]
349
+ },
350
+ {
351
+ "id": "excerpt",
352
+ "label": "説明",
353
+ "items": ["excerpt"]
354
+ },
355
+ {
356
+ "id": "image",
357
+ "label": "メインの画像",
358
+ "items": ["image"]
359
+ }
360
+ ] } },
361
+ "columns": { "post": [
362
+ "status",
363
+ "writers",
364
+ "assignee",
365
+ "published_at",
366
+ "updated_at"
367
+ ] }
368
+ };
369
+ export { CONFIG_FORMAT, DEFAULT_SCHEMA, DEFAULT_SCREENS, IMAGE_TYPE, VIDEO_TYPE, coverPosition, coverStart, createClock, cropRect, croppedHotspot, isDateOnly, mediaSchema, objectPosition, pagePathOf, publicPathOf, stillOf, typeOf };
package/dist/preview.d.ts CHANGED
@@ -1,44 +1,2 @@
1
- /** 経路ごとのヘッダーの名前(署名と主体) */
2
- export declare const SIGNED_HEADERS: {
3
- /** MCP → CMS の署名(鍵は MCP_TOKEN) */
4
- readonly mcp: "x-mcp-signature";
5
- /** MCP → CMS のログインのメールアドレス(署名の主体) */
6
- readonly mcpEmail: "x-mcp-email";
7
- /** CMS ⇄ サイトのプレビューと下書きの署名(鍵は PREVIEW_TOKEN) */
8
- readonly preview: "x-preview-signature";
9
- /** CMS → 外の送信先の Webhook・外の関数の署名(鍵は登録ごと、受け手は verifyParts と同じ手順で照合) */
10
- readonly outbound: "x-cms-signature";
11
- };
12
- /** 署名の対象(body は本文、なしは空) */
13
- type SignedParts = Readonly<{
14
- method: string;
15
- url: string;
16
- body?: string | ArrayBuffer | Uint8Array | null;
17
- /** 主体(MCP のログインのメールアドレスなど、なしは空) */
18
- subject?: string | null;
19
- }>;
20
- /** 署名のヘッダーの値(t=<秒>,v1=<署名>) */
21
- export declare function signParts(secret: string, parts: SignedParts, now?: number): Promise<string>;
22
- /** 署名の照合(形の誤り・時刻のずれの超過・不一致は false) */
23
- export declare function verifyParts(secret: string, header: string | null | undefined, parts: SignedParts, { now, maxSkewSec }?: {
24
- now?: number;
25
- maxSkewSec?: number;
26
- }): Promise<boolean>;
27
- /** 署名を付けたヘッダー(init のヘッダーに署名と主体を追加した新しい Headers) */
28
- export declare function signedHeaders(secret: string, { method, url, body, subject }: SignedParts, { header, subjectHeader, init, now }: Readonly<{
29
- header: string;
30
- subjectHeader?: string;
31
- init?: HeadersInit;
32
- now?: number;
33
- }>): Promise<Headers>;
34
- /** 署名つきのリクエストを照合する設定 */
35
- type VerifyRequestOptions = Readonly<{
36
- /** 署名のヘッダーの名前 */
37
- header: string;
38
- /** 主体のヘッダーの名前(なしは主体なしで照合) */
39
- subjectHeader?: string;
40
- now?: number;
41
- }>;
42
- /** リクエストの署名の照合(本文は複製から読み込み、元のリクエストの本文は残す) */
43
- export declare function verifyRequest(secret: string | undefined, request: Request, { header, subjectHeader, now }: VerifyRequestOptions): Promise<boolean>;
44
- export type { SignedParts, VerifyRequestOptions };
1
+ import { a as signedHeaders, i as signParts, n as SignedParts, o as verifyParts, r as VerifyRequestOptions, s as verifyRequest, t as SIGNED_HEADERS } from "./signed-B_58gV21.js";
2
+ export { SIGNED_HEADERS, type SignedParts, type VerifyRequestOptions, signParts, signedHeaders, verifyParts, verifyRequest };
package/dist/preview.js CHANGED
@@ -1,103 +1 @@
1
- // ../access/src/jwt.ts
2
- const CERTS_TTL_MS = 3e5;
3
- const REFRESH_INTERVAL_MS = 3e4;
4
- /** 公開鍵の一覧を保持する取得(取得の失敗は保持しない) */
5
- function cachedCerts(fetcher, clock = Date.now) {
6
- const cache = /* @__PURE__ */ new Map();
7
- return async (url, refresh = false) => {
8
- const hit = cache.get(url);
9
- const age = hit ? clock() - hit.at : Infinity;
10
- if (hit && age < (refresh ? REFRESH_INTERVAL_MS : CERTS_TTL_MS)) return hit;
11
- const fresh = await fetcher(url);
12
- cache.set(url, {
13
- keys: fresh.keys,
14
- at: clock()
15
- });
16
- return fresh;
17
- };
18
- }
19
- cachedCerts(async (url) => {
20
- const res = await fetch(url);
21
- if (!res.ok) throw new Error(`JWKS の取得に失敗(${res.status})`);
22
- return res.json();
23
- });
24
- /** base64url の復号 */
25
- const decodeBase64Url = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(s.length / 4) * 4, "=")), (c) => c.charCodeAt(0));
26
- // ../access/src/signed.ts
27
- const enc = new TextEncoder();
28
- /** 既定で許す時刻のずれ(秒) */
29
- const MAX_SKEW_SEC = 300;
30
- /** 経路ごとのヘッダーの名前(署名と主体) */
31
- const SIGNED_HEADERS = {
32
- /** MCP → CMS の署名(鍵は MCP_TOKEN) */
33
- mcp: "x-mcp-signature",
34
- /** MCP → CMS のログインのメールアドレス(署名の主体) */
35
- mcpEmail: "x-mcp-email",
36
- /** CMS ⇄ サイトのプレビューと下書きの署名(鍵は PREVIEW_TOKEN) */
37
- preview: "x-preview-signature",
38
- /** CMS → 外の送信先の Webhook・外の関数の署名(鍵は登録ごと、受け手は verifyParts と同じ手順で照合) */
39
- outbound: "x-cms-signature"
40
- };
41
- const base64Url = (bytes) => btoa(String.fromCharCode(...new Uint8Array(bytes))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
42
- const hmacKey = (secret) => crypto.subtle.importKey("raw", enc.encode(secret), {
43
- name: "HMAC",
44
- hash: "SHA-256"
45
- }, false, ["sign", "verify"]);
46
- async function messageOf(t, { method, url, body, subject }) {
47
- const { pathname, search } = new URL(url);
48
- const bytes = typeof body === "string" ? enc.encode(body) : body ? Uint8Array.from(new Uint8Array(body)) : /* @__PURE__ */ new Uint8Array();
49
- const digest = base64Url(await crypto.subtle.digest("SHA-256", bytes));
50
- return enc.encode([
51
- "v1",
52
- t,
53
- method.toUpperCase(),
54
- `${pathname}${search}`,
55
- subject ?? "",
56
- digest
57
- ].join("\n"));
58
- }
59
- /** 署名のヘッダーの値(t=<秒>,v1=<署名>) */
60
- async function signParts(secret, parts, now = Date.now()) {
61
- const t = Math.floor(now / 1e3);
62
- const sig = await crypto.subtle.sign("HMAC", await hmacKey(secret), await messageOf(t, parts));
63
- return `t=${t},v1=${base64Url(sig)}`;
64
- }
65
- /** 署名の照合(形の誤り・時刻のずれの超過・不一致は false) */
66
- async function verifyParts(secret, header, parts, { now = Date.now(), maxSkewSec = MAX_SKEW_SEC } = {}) {
67
- if (!secret || !header) return false;
68
- const fields = Object.fromEntries(header.split(",").map((f) => f.trim().split("=", 2)));
69
- const t = Number(fields.t);
70
- if (!Number.isInteger(t) || !fields.v1 || Math.abs(now / 1e3 - t) > maxSkewSec) return false;
71
- try {
72
- return await crypto.subtle.verify("HMAC", await hmacKey(secret), decodeBase64Url(fields.v1), await messageOf(t, parts));
73
- } catch {
74
- return false;
75
- }
76
- }
77
- /** 署名を付けたヘッダー(init のヘッダーに署名と主体を追加した新しい Headers) */
78
- async function signedHeaders(secret, { method, url, body, subject }, { header, subjectHeader, init, now }) {
79
- const headers = new Headers(init);
80
- if (subjectHeader && subject) headers.set(subjectHeader, subject);
81
- headers.set(header, await signParts(secret, {
82
- method,
83
- url,
84
- body: body ?? null,
85
- subject: subjectHeader ? subject : null
86
- }, now));
87
- return headers;
88
- }
89
- /** リクエストの署名の照合(本文は複製から読み込み、元のリクエストの本文は残す) */
90
- async function verifyRequest(secret, request, { header, subjectHeader, now }) {
91
- const value = request.headers.get(header);
92
- if (!secret || !value) return false;
93
- const body = request.body ? await request.clone().arrayBuffer() : null;
94
- return verifyParts(secret, value, {
95
- method: request.method,
96
- url: request.url,
97
- body,
98
- subject: subjectHeader ? request.headers.get(subjectHeader) : null
99
- }, now === void 0 ? {} : { now });
100
- }
101
- "0".repeat(32);
102
- "0".repeat(16);
103
- export { SIGNED_HEADERS, signParts, signedHeaders, verifyParts, verifyRequest };
1
+ export { t as SIGNED_HEADERS, n as signParts, r as signedHeaders, i as verifyParts, a as verifyRequest } from "./src-CA_67dWy.js";
@@ -0,0 +1,44 @@
1
+ /** 経路ごとのヘッダーの名前(署名と主体) */
2
+ declare const SIGNED_HEADERS: {
3
+ /** MCP → CMS の署名(鍵は MCP_TOKEN) */
4
+ readonly mcp: "x-mcp-signature";
5
+ /** MCP → CMS のログインのメールアドレス(署名の主体) */
6
+ readonly mcpEmail: "x-mcp-email";
7
+ /** CMS ⇄ サイトのプレビューと下書きの署名(鍵は PREVIEW_TOKEN) */
8
+ readonly preview: "x-preview-signature";
9
+ /** CMS → 外の送信先の Webhook・外の関数の署名(鍵は登録ごと、受け手は verifyParts と同じ手順で照合) */
10
+ readonly outbound: "x-cms-signature";
11
+ };
12
+ /** 署名の対象(body は本文、なしは空) */
13
+ type SignedParts = Readonly<{
14
+ method: string;
15
+ url: string;
16
+ body?: string | ArrayBuffer | Uint8Array | null;
17
+ /** 主体(MCP のログインのメールアドレスなど、なしは空) */
18
+ subject?: string | null;
19
+ }>;
20
+ /** 署名のヘッダーの値(t=<秒>,v1=<署名>) */
21
+ declare function signParts(secret: string, parts: SignedParts, now?: number): Promise<string>;
22
+ /** 署名の照合(形の誤り・時刻のずれの超過・不一致は false) */
23
+ declare function verifyParts(secret: string, header: string | null | undefined, parts: SignedParts, { now, maxSkewSec }?: {
24
+ now?: number;
25
+ maxSkewSec?: number;
26
+ }): Promise<boolean>;
27
+ /** 署名を付けたヘッダー(init のヘッダーに署名と主体を追加した新しい Headers) */
28
+ declare function signedHeaders(secret: string, { method, url, body, subject }: SignedParts, { header, subjectHeader, init, now }: Readonly<{
29
+ header: string;
30
+ subjectHeader?: string;
31
+ init?: HeadersInit;
32
+ now?: number;
33
+ }>): Promise<Headers>;
34
+ /** 署名つきのリクエストを照合する設定 */
35
+ type VerifyRequestOptions = Readonly<{
36
+ /** 署名のヘッダーの名前 */
37
+ header: string;
38
+ /** 主体のヘッダーの名前(なしは主体なしで照合) */
39
+ subjectHeader?: string;
40
+ now?: number;
41
+ }>;
42
+ /** リクエストの署名の照合(本文は複製から読み込み、元のリクエストの本文は残す) */
43
+ declare function verifyRequest(secret: string | undefined, request: Request, { header, subjectHeader, now }: VerifyRequestOptions): Promise<boolean>;
44
+ export { signedHeaders as a, signParts as i, SignedParts as n, verifyParts as o, VerifyRequestOptions as r, verifyRequest as s, SIGNED_HEADERS as t };
@@ -0,0 +1,7 @@
1
+ Object.fromEntries([
2
+ "read",
3
+ "create",
4
+ "update",
5
+ "publish",
6
+ "delete"
7
+ ].map((a) => [a, "all"]));
@@ -0,0 +1,103 @@
1
+ // ../access/src/jwt.ts
2
+ const CERTS_TTL_MS = 3e5;
3
+ const REFRESH_INTERVAL_MS = 3e4;
4
+ /** 公開鍵の一覧を保持する取得(取得の失敗は保持しない) */
5
+ function cachedCerts(fetcher, clock = Date.now) {
6
+ const cache = /* @__PURE__ */ new Map();
7
+ return async (url, refresh = false) => {
8
+ const hit = cache.get(url);
9
+ const age = hit ? clock() - hit.at : Infinity;
10
+ if (hit && age < (refresh ? REFRESH_INTERVAL_MS : CERTS_TTL_MS)) return hit;
11
+ const fresh = await fetcher(url);
12
+ cache.set(url, {
13
+ keys: fresh.keys,
14
+ at: clock()
15
+ });
16
+ return fresh;
17
+ };
18
+ }
19
+ cachedCerts(async (url) => {
20
+ const res = await fetch(url);
21
+ if (!res.ok) throw new Error(`JWKS の取得に失敗(${res.status})`);
22
+ return res.json();
23
+ });
24
+ /** base64url の復号 */
25
+ const decodeBase64Url = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(s.length / 4) * 4, "=")), (c) => c.charCodeAt(0));
26
+ // ../access/src/signed.ts
27
+ const enc = new TextEncoder();
28
+ /** 既定で許す時刻のずれ(秒) */
29
+ const MAX_SKEW_SEC = 300;
30
+ /** 経路ごとのヘッダーの名前(署名と主体) */
31
+ const SIGNED_HEADERS = {
32
+ /** MCP → CMS の署名(鍵は MCP_TOKEN) */
33
+ mcp: "x-mcp-signature",
34
+ /** MCP → CMS のログインのメールアドレス(署名の主体) */
35
+ mcpEmail: "x-mcp-email",
36
+ /** CMS ⇄ サイトのプレビューと下書きの署名(鍵は PREVIEW_TOKEN) */
37
+ preview: "x-preview-signature",
38
+ /** CMS → 外の送信先の Webhook・外の関数の署名(鍵は登録ごと、受け手は verifyParts と同じ手順で照合) */
39
+ outbound: "x-cms-signature"
40
+ };
41
+ const base64Url = (bytes) => btoa(String.fromCharCode(...new Uint8Array(bytes))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
42
+ const hmacKey = (secret) => crypto.subtle.importKey("raw", enc.encode(secret), {
43
+ name: "HMAC",
44
+ hash: "SHA-256"
45
+ }, false, ["sign", "verify"]);
46
+ async function messageOf(t, { method, url, body, subject }) {
47
+ const { pathname, search } = new URL(url);
48
+ const bytes = typeof body === "string" ? enc.encode(body) : body ? Uint8Array.from(new Uint8Array(body)) : /* @__PURE__ */ new Uint8Array();
49
+ const digest = base64Url(await crypto.subtle.digest("SHA-256", bytes));
50
+ return enc.encode([
51
+ "v1",
52
+ t,
53
+ method.toUpperCase(),
54
+ `${pathname}${search}`,
55
+ subject ?? "",
56
+ digest
57
+ ].join("\n"));
58
+ }
59
+ /** 署名のヘッダーの値(t=<秒>,v1=<署名>) */
60
+ async function signParts(secret, parts, now = Date.now()) {
61
+ const t = Math.floor(now / 1e3);
62
+ const sig = await crypto.subtle.sign("HMAC", await hmacKey(secret), await messageOf(t, parts));
63
+ return `t=${t},v1=${base64Url(sig)}`;
64
+ }
65
+ /** 署名の照合(形の誤り・時刻のずれの超過・不一致は false) */
66
+ async function verifyParts(secret, header, parts, { now = Date.now(), maxSkewSec = MAX_SKEW_SEC } = {}) {
67
+ if (!secret || !header) return false;
68
+ const fields = Object.fromEntries(header.split(",").map((f) => f.trim().split("=", 2)));
69
+ const t = Number(fields.t);
70
+ if (!Number.isInteger(t) || !fields.v1 || Math.abs(now / 1e3 - t) > maxSkewSec) return false;
71
+ try {
72
+ return await crypto.subtle.verify("HMAC", await hmacKey(secret), decodeBase64Url(fields.v1), await messageOf(t, parts));
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+ /** 署名を付けたヘッダー(init のヘッダーに署名と主体を追加した新しい Headers) */
78
+ async function signedHeaders(secret, { method, url, body, subject }, { header, subjectHeader, init, now }) {
79
+ const headers = new Headers(init);
80
+ if (subjectHeader && subject) headers.set(subjectHeader, subject);
81
+ headers.set(header, await signParts(secret, {
82
+ method,
83
+ url,
84
+ body: body ?? null,
85
+ subject: subjectHeader ? subject : null
86
+ }, now));
87
+ return headers;
88
+ }
89
+ /** リクエストの署名の照合(本文は複製から読み込み、元のリクエストの本文は残す) */
90
+ async function verifyRequest(secret, request, { header, subjectHeader, now }) {
91
+ const value = request.headers.get(header);
92
+ if (!secret || !value) return false;
93
+ const body = request.body ? await request.clone().arrayBuffer() : null;
94
+ return verifyParts(secret, value, {
95
+ method: request.method,
96
+ url: request.url,
97
+ body,
98
+ subject: subjectHeader ? request.headers.get(subjectHeader) : null
99
+ }, now === void 0 ? {} : { now });
100
+ }
101
+ "0".repeat(32);
102
+ "0".repeat(16);
103
+ export { verifyRequest as a, verifyParts as i, signParts as n, signedHeaders as r, SIGNED_HEADERS as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryusenkei/maezae",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "maezae の導入先のサイト向けの SDK(配信 API の取得・本文の型・プレビューの署名)",
5
5
  "license": "UNLICENSED",
6
6
  "author": "RYUSENKEI",
@@ -27,6 +27,10 @@
27
27
  "./preview": {
28
28
  "types": "./dist/preview.d.ts",
29
29
  "default": "./dist/preview.js"
30
+ },
31
+ "./integrations": {
32
+ "types": "./dist/integrations.d.ts",
33
+ "default": "./dist/integrations.js"
30
34
  }
31
35
  },
32
36
  "files": [