@ryusenkei/maezae 0.0.0-stage → 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 +48 -2
- package/dist/body.d.ts +24 -0
- package/dist/body.js +679 -0
- package/dist/delivery.d.ts +211 -0
- package/dist/delivery.js +199 -0
- package/dist/index-C8duwjns.d.ts +157 -0
- package/dist/index-CLDe_wBc.d.ts +340 -0
- package/dist/integrations.d.ts +72 -0
- package/dist/integrations.js +6 -0
- package/dist/model.d.ts +82 -0
- package/dist/model.js +369 -0
- package/dist/page-UEKb7_gu.js +152 -0
- package/dist/preview.d.ts +2 -0
- package/dist/preview.js +1 -0
- package/dist/signed-B_58gV21.d.ts +44 -0
- package/dist/src-BrLwIWaT.js +7 -0
- package/dist/src-CA_67dWy.js +103 -0
- package/package.json +51 -3
package/dist/model.js
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
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";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
// ../model/src/asset.ts
|
|
5
|
+
const unit = z.number().min(0).max(1);
|
|
6
|
+
/** 見せたい点の検査 */
|
|
7
|
+
const hotspotSchema = z.object({
|
|
8
|
+
x: unit,
|
|
9
|
+
y: unit,
|
|
10
|
+
width: unit.optional(),
|
|
11
|
+
height: unit.optional()
|
|
12
|
+
});
|
|
13
|
+
z.object({
|
|
14
|
+
top: unit,
|
|
15
|
+
bottom: unit,
|
|
16
|
+
left: unit,
|
|
17
|
+
right: unit
|
|
18
|
+
}).refine((c) => c.left + c.right <= .9 && c.top + c.bottom <= .9, "切り抜きが大きすぎる(残りは1割以上)");
|
|
19
|
+
const mediaBase = z.object({
|
|
20
|
+
src: z.string().min(1),
|
|
21
|
+
width: z.number().int().positive(),
|
|
22
|
+
height: z.number().int().positive(),
|
|
23
|
+
alt: z.string(),
|
|
24
|
+
hotspot: hotspotSchema.optional()
|
|
25
|
+
});
|
|
26
|
+
/** 解決後の素材の検査(src は URL、ビルドで読み込んだ画像は JSON に含めないため対象外) */
|
|
27
|
+
const mediaSchema = z.union([mediaBase.extend({ kind: z.literal("image") }), mediaBase.extend({
|
|
28
|
+
kind: z.literal("video"),
|
|
29
|
+
poster: mediaBase
|
|
30
|
+
})]);
|
|
31
|
+
z.object({ asset: z.string().min(1) });
|
|
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 };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// ../model/src/time.ts
|
|
2
|
+
const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
3
|
+
const SQLITE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$/;
|
|
4
|
+
const DAY_MS = 864e5;
|
|
5
|
+
/** 日付だけの値(`YYYY-MM-DD`)か */
|
|
6
|
+
const isDateOnly = (value) => DATE_ONLY.test(value);
|
|
7
|
+
/** タイムゾーンの日時の変換を作成 */
|
|
8
|
+
function createClock(timeZone) {
|
|
9
|
+
const format = new Intl.DateTimeFormat("en-CA", {
|
|
10
|
+
timeZone,
|
|
11
|
+
year: "numeric",
|
|
12
|
+
month: "2-digit",
|
|
13
|
+
day: "2-digit",
|
|
14
|
+
hour: "2-digit",
|
|
15
|
+
minute: "2-digit",
|
|
16
|
+
second: "2-digit",
|
|
17
|
+
hourCycle: "h23"
|
|
18
|
+
});
|
|
19
|
+
const partsOf = (ms) => Object.fromEntries(format.formatToParts(new Date(ms)).map((p) => [p.type, p.value]));
|
|
20
|
+
const offsetOf = (ms) => {
|
|
21
|
+
const p = partsOf(ms);
|
|
22
|
+
return Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second) - Math.floor(ms / 1e3) * 1e3;
|
|
23
|
+
};
|
|
24
|
+
const fromLocal = (date, time = "00:00") => {
|
|
25
|
+
const wall = Date.parse(`${date}T${time}:00Z`);
|
|
26
|
+
const first = wall - offsetOf(wall);
|
|
27
|
+
return new Date(wall - offsetOf(first)).toISOString();
|
|
28
|
+
};
|
|
29
|
+
const toDateTime = (value) => {
|
|
30
|
+
const ms = DATE_ONLY.test(value) ? Date.parse(fromLocal(value)) : SQLITE.test(value) ? Date.parse(`${value.replace(" ", "T")}Z`) : Date.parse(value);
|
|
31
|
+
return Number.isNaN(ms) ? null : new Date(ms).toISOString();
|
|
32
|
+
};
|
|
33
|
+
const local = (value) => partsOf(Date.parse(toDateTime(value) ?? value));
|
|
34
|
+
const dateOf = (value) => {
|
|
35
|
+
if (DATE_ONLY.test(value)) return value;
|
|
36
|
+
const p = local(value);
|
|
37
|
+
return `${p.year}-${p.month}-${p.day}`;
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
nowIso: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
41
|
+
fromLocal,
|
|
42
|
+
toDateTime,
|
|
43
|
+
dateOf,
|
|
44
|
+
timeOf: (value) => {
|
|
45
|
+
const p = local(value);
|
|
46
|
+
return `${p.hour}:${p.minute}`;
|
|
47
|
+
},
|
|
48
|
+
endOfDay: (date) => (/* @__PURE__ */ new Date(Date.parse(fromLocal(new Date(Date.parse(`${date}T00:00:00Z`) + DAY_MS).toISOString().slice(0, 10))) - 1)).toISOString(),
|
|
49
|
+
today: (now = Date.now()) => dateOf(new Date(now).toISOString())
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// ../model/src/image.ts
|
|
53
|
+
/** 静止画として表示する画像(動画は代わりの画像 poster) */
|
|
54
|
+
const stillOf = (media) => media.kind === "video" ? media.poster : media;
|
|
55
|
+
/** 見せたい点を CSS の object-position に(なしは undefined で中央、枠の比が分からない表示の代わり) */
|
|
56
|
+
const objectPosition = (hotspot) => hotspot && `${Math.round(hotspot.x * 1e3) / 10}% ${Math.round(hotspot.y * 1e3) / 10}%`;
|
|
57
|
+
/**
|
|
58
|
+
* 枠を覆う表示で、枠に見える範囲(窓)の始まりの位置: 1次元、画像に対する割合で0〜1 - window
|
|
59
|
+
* - 見せたい範囲の中心を窓の中央に寄せ、画像の端を越えるときは端と同じ位置
|
|
60
|
+
* - 範囲が窓に収まる大きさなら、この位置で範囲は必ず窓に入る(範囲が窓より大きいときは範囲の中央を表示)
|
|
61
|
+
* - center なしは中央、CMS のプレビュー(apps/admin の crop.ts)とサイトの表示で共通
|
|
62
|
+
*/
|
|
63
|
+
function coverStart(center, window) {
|
|
64
|
+
const max = Math.max(0, 1 - window);
|
|
65
|
+
return center === void 0 ? max / 2 : Math.min(max, Math.max(0, center - window / 2));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 枠を覆う表示(object-fit: cover)の object-position: 見せたい範囲が枠に入る位置
|
|
69
|
+
* - size: 画像の幅と高さ、frame: 枠の幅 / 高さ(なしは見せたい点の百分率で代用)
|
|
70
|
+
* - 帯や余白はなし、枠いっぱいに画像を敷いたまま位置だけを合わせる
|
|
71
|
+
*/
|
|
72
|
+
function coverPosition(hotspot, size, frame) {
|
|
73
|
+
if (!hotspot) return void 0;
|
|
74
|
+
if (!frame) return objectPosition(hotspot);
|
|
75
|
+
const ratio = size.width / size.height;
|
|
76
|
+
const [ww, wh] = ratio > frame ? [frame / ratio, 1] : [1, ratio / frame];
|
|
77
|
+
const percent = (center, window) => window >= 1 ? 50 : Math.round(coverStart(center, window) / (1 - window) * 1e3) / 10;
|
|
78
|
+
return `${percent(hotspot.x, ww)}% ${percent(hotspot.y, wh)}%`;
|
|
79
|
+
}
|
|
80
|
+
/** 切り抜きの割合から、元画像での画素の範囲 */
|
|
81
|
+
function cropRect(width, height, crop) {
|
|
82
|
+
return {
|
|
83
|
+
left: Math.round(width * crop.left),
|
|
84
|
+
top: Math.round(height * crop.top),
|
|
85
|
+
width: Math.max(1, Math.round(width * (1 - crop.left - crop.right))),
|
|
86
|
+
height: Math.max(1, Math.round(height * (1 - crop.top - crop.bottom)))
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const unit = (v) => Math.round(Math.min(1, Math.max(0, v)) * 1e4) / 1e4;
|
|
90
|
+
/** 元の画像に対する見せたい点を、切り抜いた後の画像に対する割合に変換(範囲の外は端に寄せる) */
|
|
91
|
+
function croppedHotspot(hotspot, crop) {
|
|
92
|
+
const w = 1 - crop.left - crop.right;
|
|
93
|
+
const h = 1 - crop.top - crop.bottom;
|
|
94
|
+
return {
|
|
95
|
+
x: unit((hotspot.x - crop.left) / w),
|
|
96
|
+
y: unit((hotspot.y - crop.top) / h),
|
|
97
|
+
...hotspot.width !== void 0 && hotspot.height !== void 0 ? {
|
|
98
|
+
width: unit(hotspot.width / w),
|
|
99
|
+
height: unit(hotspot.height / h)
|
|
100
|
+
} : {}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// ../model/src/model.ts
|
|
104
|
+
/** コンテンツスキーマの型(id から、なければ例外: 導入先の設定の誤り) */
|
|
105
|
+
function typeOf(model, id) {
|
|
106
|
+
const type = model.types.find((t) => t.id === id);
|
|
107
|
+
if (!type) throw new Error(`コンテンツスキーマに型 ${id} がない`);
|
|
108
|
+
return type;
|
|
109
|
+
}
|
|
110
|
+
/** 選択肢の項目の選択肢(コアの項目と追加の項目から、まとまりの中の項目は `links.kind` の形、選択肢の項目でなければ空) */
|
|
111
|
+
function optionsOf(type, key) {
|
|
112
|
+
const [head, ...rest] = key.split(".");
|
|
113
|
+
let field = [...type.core ?? [], ...type.fields].find((f) => f.key === head);
|
|
114
|
+
for (const k of rest) field = field?.type === "group" ? field.fields.find((f) => f.key === k) : void 0;
|
|
115
|
+
return field?.type === "select" ? field.options : [];
|
|
116
|
+
}
|
|
117
|
+
// ../model/src/page.ts
|
|
118
|
+
const matches = (when, entry) => !when || when.in.includes(String(entry[when.key]));
|
|
119
|
+
const passes = (rule, entry, now) => {
|
|
120
|
+
const v = entry[rule.key];
|
|
121
|
+
if ("in" in rule) return rule.in.includes(String(v ?? ""));
|
|
122
|
+
if ("notIn" in rule) return !rule.notIn.includes(String(v ?? ""));
|
|
123
|
+
if ("truthy" in rule) return Boolean(v);
|
|
124
|
+
return v === void 0 || v === null || v === "" || String(v) <= now;
|
|
125
|
+
};
|
|
126
|
+
/** 行に当てはまるページの宣言(なければ null) */
|
|
127
|
+
const pageOf = (type, entry) => type.pages?.find((p) => matches(p.when, entry)) ?? null;
|
|
128
|
+
/** 雛形の {key} を行の値に置き換え(値がなければ null) */
|
|
129
|
+
const fill = (path, entry) => {
|
|
130
|
+
let missing = false;
|
|
131
|
+
const filled = path.replace(/\{([A-Za-z0-9_-]+)\}/g, (_, key) => {
|
|
132
|
+
const v = entry[key];
|
|
133
|
+
if (v === void 0 || v === null) missing = true;
|
|
134
|
+
return String(v ?? "");
|
|
135
|
+
});
|
|
136
|
+
return missing ? null : filled;
|
|
137
|
+
};
|
|
138
|
+
/** ページのパス(ページのない型・行は null、公開の状態は見ない) */
|
|
139
|
+
function pagePathOf(type, entry) {
|
|
140
|
+
const page = pageOf(type, entry);
|
|
141
|
+
return page ? fill(page.path, entry) : null;
|
|
142
|
+
}
|
|
143
|
+
/** 行がサイトで公開中のページを持つか(雛形の値は見ない、作成前の入力にも使用、now は今の時刻の ISO 文字列) */
|
|
144
|
+
function hasPublicPage(type, entry, now) {
|
|
145
|
+
const page = pageOf(type, entry);
|
|
146
|
+
return page !== null && (page.public ?? []).every((r) => passes(r, entry, now));
|
|
147
|
+
}
|
|
148
|
+
/** 公開中のページのパス(公開前・予約中・ページのない行は null、now は今の時刻の ISO 文字列) */
|
|
149
|
+
function publicPathOf(type, entry, now) {
|
|
150
|
+
return hasPublicPage(type, entry, now) ? pagePathOf(type, entry) : null;
|
|
151
|
+
}
|
|
152
|
+
export { coverPosition as a, croppedHotspot as c, createClock as d, isDateOnly as f, typeOf as i, objectPosition as l, publicPathOf as n, coverStart as o, optionsOf as r, cropRect as s, pagePathOf as t, stillOf as u };
|
|
@@ -0,0 +1,2 @@
|
|
|
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
ADDED
|
@@ -0,0 +1 @@
|
|
|
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,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 };
|