@ryusenkei/maezae 0.20.0 → 0.21.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/CHANGELOG.md +23 -0
- package/README.md +24 -10
- package/dist/analytics.js +11 -17
- package/dist/dev.js +3 -2
- package/dist/integrations.js +2 -2
- package/dist/preview.js +1 -1
- package/dist/src-BCTmQ5n-.js +37 -0
- package/dist/types/access/src/index.d.ts +1 -3
- package/dist/types/access/src/signed.d.ts +1 -33
- package/dist/types/analytics/src/index.d.ts +1 -1
- package/dist/types/analytics/src/view.d.ts +9 -0
- package/dist/types/sdk/src/analytics.d.ts +2 -12
- package/dist/types/sdk/src/integrations.d.ts +2 -2
- package/dist/types/sdk/src/preview.d.ts +1 -2
- package/package.json +4 -6
- package/dist/src-BMOqN7Xs.js +0 -193
- package/dist/trace.d.ts +0 -1
- package/dist/trace.js +0 -1
- package/dist/types/access/src/span.d.ts +0 -23
- package/dist/types/access/src/traceparent.d.ts +0 -21
- package/dist/types/sdk/src/trace.d.ts +0 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
## 0.21.0
|
|
4
|
+
|
|
5
|
+
互換のない変更。Webhook の署名と検証、トレース、閲覧の記録の仕組みを SDK から外した。導入先は toolkit(`@ryusenkei/web` 0.7.0 以上、`@ryusenkei/core` 0.4.0 以上)を直接 import する。署名の値と閲覧の列は 0.20.0 と同じなので、CMS との間で同時に切り替える必要はない
|
|
6
|
+
|
|
7
|
+
| 0.20.0 まで | 0.21.0 から | 差 |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `signedHeaders`(`/preview`) | `webhookHeaders`(`@ryusenkei/web/webhook`) | 引数は同じ。鍵が空なら `ConfigError` |
|
|
10
|
+
| `signParts`(`/preview`) | `signWebhook`(`@ryusenkei/web/webhook`) | 3番目の引数が `{ now }` |
|
|
11
|
+
| `verifyRequest`(`/preview`、`/integrations`) | `verifyWebhookRequest`(`@ryusenkei/web/webhook`) | `boolean` ではなく `Result`(`.ok`)。鍵が空なら `ConfigError`。鍵が任意なら呼ぶ前に確かめる |
|
|
12
|
+
| `verifyParts`(`/preview`) | `verifyWebhook`(`@ryusenkei/web/webhook`) | 同上。`maxSkewSec` は `toleranceSec` |
|
|
13
|
+
| `SignedParts`、`VerifyRequestOptions` | `WebhookParts`、`VerifyWebhookRequestOptions`(`@ryusenkei/web/webhook`) | — |
|
|
14
|
+
| `withEdgeSpan`(`/trace`) | `traceRequest(logTracer({ sampleRate }), name, request, (span) => …)`(`@ryusenkei/core/trace`、`logTracer` は `@ryusenkei/core`) | `span.traceparent` は `formatTraceparent(span.context)`。ログの属性が `method`・`path`・`status` から `http.method`・`http.path`・`http.status` に。Workers は互換のフラグ `nodejs_als` が要る |
|
|
15
|
+
| `TRACEPARENT_HEADER`、`sampleRateOf`、`TraceContext`(`/trace`) | 同じ名前(`@ryusenkei/core`) | — |
|
|
16
|
+
| `EdgeSpan`、`EdgeSpanOptions`、`EdgeSpanStatus`(`/trace`) | `Span`、`SpanStatus`(`@ryusenkei/core`) | — |
|
|
17
|
+
| `recordView(dataset, request, response, options)`(`/analytics`) | `viewOf(request, response, options)` と `analyticsEngineRecorder(dataset, VIEW_SCHEMA).record(view)`(`analyticsEngineRecorder` は `@ryusenkei/web/cloudflare`) | 閲覧の判定と列は SDK の `viewOf`・`VIEW_SCHEMA`。書き込みの失敗は警告のログ |
|
|
18
|
+
| `Dataset`(`/analytics`) | `AnalyticsEngineDataset`(`@ryusenkei/web/cloudflare`) | — |
|
|
19
|
+
|
|
20
|
+
- 残したもの: `SIGNED_HEADERS`(`/preview`、`/integrations`)、`openValue`・`openEmbedContext`(`/integrations`)、`VIEW_COLUMNS`・`viewOf`・`Classify`(`/analytics`)
|
|
21
|
+
- 足したもの: `VIEW_SCHEMA`、`ViewEvent`、`ViewOptions`(`/analytics`)
|
|
22
|
+
- 消したもの: エントリポイント `/trace`
|
|
23
|
+
- 依存に `@ryusenkei/web` を足した(`/dev` の下書きの署名と `VIEW_SCHEMA`)
|
package/README.md
CHANGED
|
@@ -9,12 +9,23 @@ maezae の SDK(導入先のサイト向け)
|
|
|
9
9
|
| `@ryusenkei/maezae/delivery` | 配信 API `/content/v1` の取得(一覧の絞り込み・`/bundle`・ETag) |
|
|
10
10
|
| `@ryusenkei/maezae/body` | 本文のブロックの型と描画の補助 |
|
|
11
11
|
| `@ryusenkei/maezae/model` | コンテンツスキーマの型・ページのパス・画像の切り抜き |
|
|
12
|
-
| `@ryusenkei/maezae/preview` |
|
|
13
|
-
| `@ryusenkei/maezae/analytics` |
|
|
14
|
-
| `@ryusenkei/maezae/
|
|
15
|
-
| `@ryusenkei/maezae/integrations` | 外の関数・Webhook が受け取る本文の型と署名の検証(ヘッダー `x-cms-signature`) |
|
|
12
|
+
| `@ryusenkei/maezae/preview` | プレビューと下書きの取得の署名のヘッダーの名前(`x-preview-signature`) |
|
|
13
|
+
| `@ryusenkei/maezae/analytics` | サイトの閲覧の判定と、CMS の閲覧数の集計と同じ列の形 |
|
|
14
|
+
| `@ryusenkei/maezae/integrations` | 外の関数・Webhook が受け取る本文の型と署名のヘッダーの名前(`x-cms-signature`)、外の画面の文脈 |
|
|
16
15
|
| `@ryusenkei/maezae/schema` | 配信 API の値の型の定義の生成とずれの検査(CLI `maezae-types`、Node) |
|
|
17
16
|
|
|
17
|
+
署名と検証、トレース、閲覧の記録の仕組みは toolkit(`@ryusenkei/web`、`@ryusenkei/core`)を直接 import する(0.21.0 から、置き換えは [CHANGELOG.md](CHANGELOG.md))。SDK はヘッダーの名前と閲覧の列の形だけを持つ
|
|
18
|
+
|
|
19
|
+
## 閲覧の記録
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { VIEW_SCHEMA, viewOf } from '@ryusenkei/maezae/analytics';
|
|
23
|
+
import { analyticsEngineRecorder } from '@ryusenkei/web/cloudflare';
|
|
24
|
+
|
|
25
|
+
const view = viewOf(request, response, { classify, timeZone: 'Asia/Tokyo' });
|
|
26
|
+
if (view) analyticsEngineRecorder(env.VIEWS, VIEW_SCHEMA).record(view);
|
|
27
|
+
```
|
|
28
|
+
|
|
18
29
|
## 取得(Cloudflare Workers の service binding)
|
|
19
30
|
|
|
20
31
|
- バインディング: `services: [{ "binding": "CMS", "service": "<CMS のサーバーの Worker>", "entrypoint": "Delivery" }]`
|
|
@@ -239,9 +250,11 @@ const thumb = withImageParams(src, { w: 400, auto: 'format' });
|
|
|
239
250
|
- 書きかけの値の画像: 配信 API の1件と同じ値(`id`・`kind`・`url`・`width`・`height`・`alt`・`hotspot`・`crop`)に参照の `asset` を足した形(0.17.0 から)
|
|
240
251
|
|
|
241
252
|
```ts
|
|
242
|
-
import { SIGNED_HEADERS
|
|
253
|
+
import { SIGNED_HEADERS } from '@ryusenkei/maezae/preview';
|
|
254
|
+
import { verifyWebhookRequest } from '@ryusenkei/web/webhook';
|
|
243
255
|
|
|
244
|
-
|
|
256
|
+
// 鍵が空なら verifyWebhookRequest は ConfigError を投げるので、鍵が任意なら先に確かめる
|
|
257
|
+
const isPreview = !!env.PREVIEW_TOKEN && (await verifyWebhookRequest(env.PREVIEW_TOKEN, request, { header: SIGNED_HEADERS.preview })).ok;
|
|
245
258
|
```
|
|
246
259
|
|
|
247
260
|
## 外の関数・Webhook の受け手
|
|
@@ -249,9 +262,10 @@ const isPreview = await verifyRequest(env.PREVIEW_TOKEN, request, { header: SIGN
|
|
|
249
262
|
- 鍵: 媒体の管理画面の「設定 → 連携」の登録ごとの署名の鍵
|
|
250
263
|
|
|
251
264
|
```ts
|
|
252
|
-
import { type PublishCheckRequest, SIGNED_HEADERS
|
|
265
|
+
import { type PublishCheckRequest, SIGNED_HEADERS } from '@ryusenkei/maezae/integrations';
|
|
266
|
+
import { verifyWebhookRequest } from '@ryusenkei/web/webhook';
|
|
253
267
|
|
|
254
|
-
if (!(await
|
|
268
|
+
if (!(await verifyWebhookRequest(env.CMS_FUNCTION_SECRET, request, { header: SIGNED_HEADERS.outbound })).ok) return new Response(null, { status: 401 });
|
|
255
269
|
const body = (await request.json()) as PublishCheckRequest;
|
|
256
270
|
```
|
|
257
271
|
|
|
@@ -326,8 +340,8 @@ if (!context) return new Response(null, { status: 401 });
|
|
|
326
340
|
- 外の画面: クエリ `ctx` は古い鍵、`ctx_next` は新しい鍵で封印
|
|
327
341
|
- 古い SDK(0.10.0 より前): 古い鍵の署名だけを照合するため、古い鍵のままなら移行の期間も通る
|
|
328
342
|
- 導入先の手順
|
|
329
|
-
1.
|
|
343
|
+
1. 鍵を並べて照合する受け手にしてデプロイ(照合は toolkit の `verifyWebhookRequest`、外の画面は `openEmbedContext`)
|
|
330
344
|
2. 管理画面で「鍵を再発行」し、表示された新しい鍵を受け手の秘密値(例: `CMS_FUNCTION_SECRET`)に設定してデプロイ
|
|
331
345
|
3. 管理画面の「送信の記録」で、差し替えの後の送信が成功していることを確かめる
|
|
332
346
|
4. 管理画面で「古い鍵を失効」
|
|
333
|
-
- 受け手が新旧の鍵を並べて持つこともできる: `
|
|
347
|
+
- 受け手が新旧の鍵を並べて持つこともできる: `verifyWebhookRequest([env.NEW_SECRET, env.OLD_SECRET], request, …)`
|
package/dist/analytics.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { defineEventSchema, isBotUserAgent } from "@ryusenkei/web/analytics";
|
|
1
2
|
// ../analytics/src/day.ts
|
|
2
3
|
/** 時間帯 timeZone での日付(YYYY-MM-DD、例: timeZone は Asia/Tokyo) */
|
|
3
4
|
const dayIn = (timeZone, at = /* @__PURE__ */ new Date()) => new Intl.DateTimeFormat("en-CA", {
|
|
@@ -16,13 +17,18 @@ const VIEW_COLUMNS = [
|
|
|
16
17
|
"event",
|
|
17
18
|
"project"
|
|
18
19
|
];
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* 閲覧の出来事の形(toolkit の analyticsEngineRecorder・analyticsEngineCounts に渡す)
|
|
22
|
+
* - index: 媒体、指定なしは種類(Analytics Engine は index ごとに標本を抽出するため、共有のデータセットで小さな媒体の記録が間引かれないように)
|
|
23
|
+
*/
|
|
24
|
+
const VIEW_SCHEMA = defineEventSchema({
|
|
25
|
+
blobs: VIEW_COLUMNS,
|
|
26
|
+
index: (view) => view.project ?? view.kind ?? ""
|
|
27
|
+
});
|
|
22
28
|
/** 閲覧の出来事(対象: GET で応答が200の HTML・ボット以外、ほかは null) */
|
|
23
29
|
function viewOf(request, response, { classify, timeZone, project }) {
|
|
24
30
|
if (request.method !== "GET" || response.status !== 200) return null;
|
|
25
|
-
if (!response.headers.get("content-type")?.includes("text/html") ||
|
|
31
|
+
if (!response.headers.get("content-type")?.includes("text/html") || isBotUserAgent(request.headers.get("user-agent"))) return null;
|
|
26
32
|
const path = new URL(request.url).pathname;
|
|
27
33
|
const { kind, id } = classify(path);
|
|
28
34
|
return {
|
|
@@ -34,16 +40,4 @@ function viewOf(request, response, { classify, timeZone, project }) {
|
|
|
34
40
|
...project !== void 0 && { project }
|
|
35
41
|
};
|
|
36
42
|
}
|
|
37
|
-
|
|
38
|
-
/** 閲覧を1件記録(対象: GET で応答が200の HTML・ボット以外、binding なしと記録の失敗は無視) */
|
|
39
|
-
function recordView(dataset, request, response, options) {
|
|
40
|
-
const view = dataset && viewOf(request, response, options);
|
|
41
|
-
if (!view) return;
|
|
42
|
-
try {
|
|
43
|
-
dataset.writeDataPoint({
|
|
44
|
-
blobs: VIEW_COLUMNS.map((key) => view[key] ?? ""),
|
|
45
|
-
indexes: [view.project ?? view.kind]
|
|
46
|
-
});
|
|
47
|
-
} catch {}
|
|
48
|
-
}
|
|
49
|
-
export { recordView };
|
|
43
|
+
export { VIEW_COLUMNS, VIEW_SCHEMA, viewOf };
|
package/dist/dev.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as SIGNED_HEADERS } from "./src-BCTmQ5n-.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { webhookHeaders } from "@ryusenkei/web/webhook";
|
|
6
7
|
// src/dev.ts
|
|
7
8
|
/** 呼び出しの記録のファイル名(メソッド・パス・If-None-Match・本文のハッシュ) */
|
|
8
9
|
const fixtureName = (method, path, ifNoneMatch = "", body = "") => `${createHash("sha256").update(`${method.toUpperCase()} ${path}\n${ifNoneMatch}\n${body}`).digest("hex").slice(0, 16)}.json`;
|
|
@@ -36,7 +37,7 @@ function relayDelivery({ origin, apiKey, previewSecret }) {
|
|
|
36
37
|
});
|
|
37
38
|
const url = `${origin.replace(/\/$/, "")}${path}`;
|
|
38
39
|
const headers = new Headers(init.headers);
|
|
39
|
-
if (previewSecret && headers.has(SIGNED_HEADERS.preview)) headers.set(SIGNED_HEADERS.preview, (await
|
|
40
|
+
if (previewSecret && headers.has(SIGNED_HEADERS.preview)) headers.set(SIGNED_HEADERS.preview, (await webhookHeaders(previewSecret, {
|
|
40
41
|
method,
|
|
41
42
|
url,
|
|
42
43
|
body: null
|
package/dist/integrations.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as openValue, t as SIGNED_HEADERS } from "./src-BCTmQ5n-.js";
|
|
2
2
|
import "./src-BrLwIWaT.js";
|
|
3
3
|
// ../cms-contract/src/embeds.ts
|
|
4
4
|
/** URL のクエリの名前(署名つきの文脈) */
|
|
@@ -25,4 +25,4 @@ async function openEmbedContext(secret, url, now = Date.now()) {
|
|
|
25
25
|
}
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
|
-
export { EMBED_CONTEXT_NEXT_PARAM, EMBED_CONTEXT_PARAM, EMBED_MESSAGES, FUNCTION_HOOKS, SIGNED_HEADERS, openEmbedContext, openValue
|
|
28
|
+
export { EMBED_CONTEXT_NEXT_PARAM, EMBED_CONTEXT_PARAM, EMBED_MESSAGES, FUNCTION_HOOKS, SIGNED_HEADERS, openEmbedContext, openValue };
|
package/dist/preview.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { t as SIGNED_HEADERS } from "./src-BCTmQ5n-.js";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// ../access/src/signed.ts
|
|
2
|
+
const enc = new TextEncoder();
|
|
3
|
+
/** base64url の復号 */
|
|
4
|
+
const decodeBase64Url = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(s.length / 4) * 4, "=")), (c) => c.charCodeAt(0));
|
|
5
|
+
/** 経路ごとのヘッダーの名前(署名と主体) */
|
|
6
|
+
const SIGNED_HEADERS = {
|
|
7
|
+
/** MCP → CMS の署名(鍵は MCP_TOKEN) */
|
|
8
|
+
mcp: "x-mcp-signature",
|
|
9
|
+
/** MCP → CMS のログインのメールアドレス(署名の主体) */
|
|
10
|
+
mcpEmail: "x-mcp-email",
|
|
11
|
+
/** CMS ⇄ サイトのプレビューと下書きの署名(鍵は PREVIEW_TOKEN) */
|
|
12
|
+
preview: "x-preview-signature",
|
|
13
|
+
/** CMS → 外の送信先の Webhook・外の関数の署名(鍵は登録ごと、受け手は @ryusenkei/web/webhook の verifyWebhookRequest で照合) */
|
|
14
|
+
outbound: "x-cms-signature"
|
|
15
|
+
};
|
|
16
|
+
const hmacKey = (secret) => crypto.subtle.importKey("raw", enc.encode(secret), {
|
|
17
|
+
name: "HMAC",
|
|
18
|
+
hash: "SHA-256"
|
|
19
|
+
}, false, ["sign", "verify"]);
|
|
20
|
+
/** 鍵の一覧(空の値を除く) */
|
|
21
|
+
const secretsOf = (secret) => (typeof secret === "string" ? [secret] : [...secret ?? []]).filter(Boolean);
|
|
22
|
+
/** 封印した値(鍵のどれかで署名が合えば値、署名の不一致・期限切れ・形の誤りは null) */
|
|
23
|
+
async function openValue(secret, token, now = Date.now()) {
|
|
24
|
+
const [body, sig] = token?.split(".") ?? [];
|
|
25
|
+
const secrets = secretsOf(secret);
|
|
26
|
+
if (!secrets.length || !body || !sig) return null;
|
|
27
|
+
try {
|
|
28
|
+
let matched = false;
|
|
29
|
+
for (const s of secrets) if (!matched) matched = await crypto.subtle.verify("HMAC", await hmacKey(s), decodeBase64Url(sig), enc.encode(body));
|
|
30
|
+
if (!matched) return null;
|
|
31
|
+
const { v, exp } = JSON.parse(new TextDecoder().decode(decodeBase64Url(body)));
|
|
32
|
+
return typeof exp === "number" && exp * 1e3 > now ? v : null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export { openValue as n, SIGNED_HEADERS as t };
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
export { accessJwtOptions, type FetchCerts, type JwtClaims, type JwtOptions, verifyAccessJwt, verifyJwt } from './jwt.js';
|
|
2
2
|
export { cookieValue, eitherLogin, handoffLogin, type HandoffLoginOptions, openSession, sessionCookie, sessionCookieName } from './handoff.js';
|
|
3
3
|
export { emailOfClaims, type LoginAuth, oidcLogin, type OidcLoginOptions, safeNext, tokenAuth, type TokenAuthOptions } from './oidc.js';
|
|
4
|
-
export {
|
|
5
|
-
export { openValue, sealValue, SIGNED_HEADERS, signedHeaders, type SigningSecrets, signParts, type SignedParts, verifyParts, verifyRequest, type VerifyRequestOptions } from './signed.js';
|
|
6
|
-
export { childTraceContext, formatTraceparent, newSpanId, newTraceId, parseTraceparent, type TraceContext, TRACEPARENT_HEADER } from './traceparent.js';
|
|
4
|
+
export { openValue, sealValue, SIGNED_HEADERS, type SigningSecrets } from './signed.js';
|
|
@@ -6,43 +6,11 @@ export declare const SIGNED_HEADERS: {
|
|
|
6
6
|
readonly mcpEmail: "x-mcp-email";
|
|
7
7
|
/** CMS ⇄ サイトのプレビューと下書きの署名(鍵は PREVIEW_TOKEN) */
|
|
8
8
|
readonly preview: "x-preview-signature";
|
|
9
|
-
/** CMS → 外の送信先の Webhook・外の関数の署名(鍵は登録ごと、受け手は
|
|
9
|
+
/** CMS → 外の送信先の Webhook・外の関数の署名(鍵は登録ごと、受け手は @ryusenkei/web/webhook の verifyWebhookRequest で照合) */
|
|
10
10
|
readonly outbound: "x-cms-signature";
|
|
11
11
|
};
|
|
12
|
-
/** 署名の対象(body は本文、なしは空) */
|
|
13
|
-
export type SignedParts = Readonly<{
|
|
14
|
-
method: string;
|
|
15
|
-
url: string;
|
|
16
|
-
body?: string | ArrayBuffer | Uint8Array | null;
|
|
17
|
-
/** 主体(MCP のログインのメールアドレスなど、なしは空) */
|
|
18
|
-
subject?: string | null;
|
|
19
|
-
}>;
|
|
20
12
|
/** 署名の鍵(1つか、移行の期間は新しい鍵を先・古い鍵を最後に並べた複数) */
|
|
21
13
|
export type SigningSecrets = string | readonly string[];
|
|
22
|
-
/** 署名のヘッダーの値(t=<秒>,v1=<署名>、鍵が複数なら v1 を鍵の順に並べる) */
|
|
23
|
-
export declare function signParts(secret: SigningSecrets, parts: SignedParts, now?: number): Promise<string>;
|
|
24
|
-
/** 署名の照合(鍵のどれかと v1 のどれかが合えば true、形の誤り・時刻のずれの超過・不一致は false) */
|
|
25
|
-
export declare function verifyParts(secret: SigningSecrets | null | undefined, header: string | null | undefined, parts: SignedParts, { now, maxSkewSec }?: {
|
|
26
|
-
now?: number;
|
|
27
|
-
maxSkewSec?: number;
|
|
28
|
-
}): Promise<boolean>;
|
|
29
|
-
/** 署名を付けたヘッダー(init のヘッダーに署名と主体を追加した新しい Headers) */
|
|
30
|
-
export declare function signedHeaders(secret: SigningSecrets, { method, url, body, subject }: SignedParts, { header, subjectHeader, init, now }: Readonly<{
|
|
31
|
-
header: string;
|
|
32
|
-
subjectHeader?: string;
|
|
33
|
-
init?: HeadersInit;
|
|
34
|
-
now?: number;
|
|
35
|
-
}>): Promise<Headers>;
|
|
36
|
-
/** 署名つきのリクエストを照合する設定 */
|
|
37
|
-
export type VerifyRequestOptions = Readonly<{
|
|
38
|
-
/** 署名のヘッダーの名前 */
|
|
39
|
-
header: string;
|
|
40
|
-
/** 主体のヘッダーの名前(なしは主体なしで照合) */
|
|
41
|
-
subjectHeader?: string;
|
|
42
|
-
now?: number;
|
|
43
|
-
}>;
|
|
44
|
-
/** リクエストの署名の照合(本文は複製から読み込み、元のリクエストの本文は残す、鍵の移行の期間は受け手が新旧の鍵を並べて渡せる) */
|
|
45
|
-
export declare function verifyRequest(secret: SigningSecrets | undefined, request: Request, { header, subjectHeader, now }: VerifyRequestOptions): Promise<boolean>;
|
|
46
14
|
/** 期限と署名を付けた値(JSON にできる値、ttlSec は有効な秒数) */
|
|
47
15
|
export declare function sealValue(secret: string, value: unknown, ttlSec: number, now?: number): Promise<string>;
|
|
48
16
|
/** 封印した値(鍵のどれかで署名が合えば値、署名の不一致・期限切れ・形の誤りは null) */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { type Classify, VIEW_COLUMNS, type ViewEvent, viewOf, type ViewOptions } from './view.js';
|
|
1
|
+
export { type Classify, VIEW_COLUMNS, VIEW_SCHEMA, type ViewEvent, viewOf, type ViewOptions } from './view.js';
|
|
2
2
|
export { syncCron } from './schedule.js';
|
|
3
3
|
export { isViewsCron, sharedViews, viewsFor, viewsSince, viewsSyncOf } from './shared.js';
|
|
4
4
|
export { type DailyRow, fixedSource, type ProjectSource, renamedSource, type Source } from './source.js';
|
|
@@ -14,6 +14,15 @@ export type ViewEvent = Readonly<{
|
|
|
14
14
|
}>;
|
|
15
15
|
/** 位置で持つ記録先の列の順 */
|
|
16
16
|
export declare const VIEW_COLUMNS: readonly ["kind", "id", "path", "date", "event", "project"];
|
|
17
|
+
/**
|
|
18
|
+
* 閲覧の出来事の形(toolkit の analyticsEngineRecorder・analyticsEngineCounts に渡す)
|
|
19
|
+
* - index: 媒体、指定なしは種類(Analytics Engine は index ごとに標本を抽出するため、共有のデータセットで小さな媒体の記録が間引かれないように)
|
|
20
|
+
*/
|
|
21
|
+
export declare const VIEW_SCHEMA: Readonly<{
|
|
22
|
+
blobs: readonly ("id" | "kind" | "date" | "path" | "project" | "event")[];
|
|
23
|
+
doubles?: readonly never[] | undefined;
|
|
24
|
+
index?: ((event: Readonly<Partial<Record<"id" | "kind" | "date" | "path" | "project" | "event", string>> & Partial<Record<never, number>>>) => string) | undefined;
|
|
25
|
+
}>;
|
|
17
26
|
/** 出来事にする設定(project は媒体の識別子) */
|
|
18
27
|
export type ViewOptions = Readonly<{
|
|
19
28
|
classify: Classify;
|
|
@@ -1,12 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export type { Classify };
|
|
3
|
-
/** Analytics Engine のデータセットの binding(書き込みだけ) */
|
|
4
|
-
export type Dataset = {
|
|
5
|
-
writeDataPoint(point: {
|
|
6
|
-
blobs?: string[];
|
|
7
|
-
doubles?: number[];
|
|
8
|
-
indexes?: string[];
|
|
9
|
-
}): void;
|
|
10
|
-
};
|
|
11
|
-
/** 閲覧を1件記録(対象: GET で応答が200の HTML・ボット以外、binding なしと記録の失敗は無視) */
|
|
12
|
-
export declare function recordView(dataset: Dataset | undefined, request: Request, response: Response, options: ViewOptions): void;
|
|
1
|
+
export { VIEW_COLUMNS, VIEW_SCHEMA, viewOf } from '../../analytics/src/index.js';
|
|
2
|
+
export type { Classify, ViewEvent, ViewOptions } from '../../analytics/src/index.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type SigningSecrets } from '../../access/src/index.js';
|
|
2
2
|
import { type EmbedContext } from '../../cms-contract/src/index.js';
|
|
3
|
-
export { openValue, SIGNED_HEADERS
|
|
4
|
-
export type { SigningSecrets
|
|
3
|
+
export { openValue, SIGNED_HEADERS } from '../../access/src/index.js';
|
|
4
|
+
export type { SigningSecrets } from '../../access/src/index.js';
|
|
5
5
|
export { EMBED_CONTEXT_NEXT_PARAM, EMBED_CONTEXT_PARAM, EMBED_MESSAGES, FUNCTION_HOOKS } from '../../cms-contract/src/index.js';
|
|
6
6
|
export type { EmbedContext, EmbedMessage, FunctionHook, FunctionRequest, PublishCheckRequest, PublishCheckResponse, SaveCheckRequest, SaveCheckResponse, WebhookAction, WebhookBody, WebhookEvent, WebhookModuleBody, WebhookVersion } from '../../cms-contract/src/index.js';
|
|
7
7
|
/** 外の画面の URL の文脈(クエリ ctx と、鍵の移行の期間の ctx_next のどちらかが鍵で開けば文脈、なければ null) */
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export { SIGNED_HEADERS
|
|
2
|
-
export type { SignedParts, SigningSecrets, VerifyRequestOptions } from '../../access/src/index.js';
|
|
1
|
+
export { SIGNED_HEADERS } from '../../access/src/index.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryusenkei/maezae",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "maezae の導入先のサイト向けの SDK(配信 API の取得・本文の型・プレビューの署名・手元の開発の配信 API)",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "RYUSENKEI",
|
|
@@ -36,10 +36,6 @@
|
|
|
36
36
|
"types": "./dist/analytics.d.ts",
|
|
37
37
|
"default": "./dist/analytics.js"
|
|
38
38
|
},
|
|
39
|
-
"./trace": {
|
|
40
|
-
"types": "./dist/trace.d.ts",
|
|
41
|
-
"default": "./dist/trace.js"
|
|
42
|
-
},
|
|
43
39
|
"./dev": {
|
|
44
40
|
"types": "./dist/dev.d.ts",
|
|
45
41
|
"default": "./dist/dev.js"
|
|
@@ -53,12 +49,14 @@
|
|
|
53
49
|
"maezae-types": "./dist/types-cli.js"
|
|
54
50
|
},
|
|
55
51
|
"files": [
|
|
56
|
-
"dist"
|
|
52
|
+
"dist",
|
|
53
|
+
"CHANGELOG.md"
|
|
57
54
|
],
|
|
58
55
|
"publishConfig": {
|
|
59
56
|
"access": "public"
|
|
60
57
|
},
|
|
61
58
|
"dependencies": {
|
|
59
|
+
"@ryusenkei/web": "^0.7.0",
|
|
62
60
|
"zod": "^4.6.7"
|
|
63
61
|
},
|
|
64
62
|
"devDependencies": {
|
package/dist/src-BMOqN7Xs.js
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
// ../access/src/signed.ts
|
|
2
|
-
const enc = new TextEncoder();
|
|
3
|
-
/** 既定で許す時刻のずれ(秒) */
|
|
4
|
-
const MAX_SKEW_SEC = 300;
|
|
5
|
-
/** base64url の復号 */
|
|
6
|
-
const decodeBase64Url = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(s.length / 4) * 4, "=")), (c) => c.charCodeAt(0));
|
|
7
|
-
/** 経路ごとのヘッダーの名前(署名と主体) */
|
|
8
|
-
const SIGNED_HEADERS = {
|
|
9
|
-
/** MCP → CMS の署名(鍵は MCP_TOKEN) */
|
|
10
|
-
mcp: "x-mcp-signature",
|
|
11
|
-
/** MCP → CMS のログインのメールアドレス(署名の主体) */
|
|
12
|
-
mcpEmail: "x-mcp-email",
|
|
13
|
-
/** CMS ⇄ サイトのプレビューと下書きの署名(鍵は PREVIEW_TOKEN) */
|
|
14
|
-
preview: "x-preview-signature",
|
|
15
|
-
/** CMS → 外の送信先の Webhook・外の関数の署名(鍵は登録ごと、受け手は verifyParts と同じ手順で照合) */
|
|
16
|
-
outbound: "x-cms-signature"
|
|
17
|
-
};
|
|
18
|
-
const base64Url = (bytes) => btoa(String.fromCharCode(...new Uint8Array(bytes))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
19
|
-
const hmacKey = (secret) => crypto.subtle.importKey("raw", enc.encode(secret), {
|
|
20
|
-
name: "HMAC",
|
|
21
|
-
hash: "SHA-256"
|
|
22
|
-
}, false, ["sign", "verify"]);
|
|
23
|
-
async function messageOf(t, { method, url, body, subject }) {
|
|
24
|
-
const { pathname, search } = new URL(url);
|
|
25
|
-
const bytes = typeof body === "string" ? enc.encode(body) : body ? Uint8Array.from(new Uint8Array(body)) : /* @__PURE__ */ new Uint8Array();
|
|
26
|
-
const digest = base64Url(await crypto.subtle.digest("SHA-256", bytes));
|
|
27
|
-
return enc.encode([
|
|
28
|
-
"v1",
|
|
29
|
-
t,
|
|
30
|
-
method.toUpperCase(),
|
|
31
|
-
`${pathname}${search}`,
|
|
32
|
-
subject ?? "",
|
|
33
|
-
digest
|
|
34
|
-
].join("\n"));
|
|
35
|
-
}
|
|
36
|
-
/** 鍵の一覧(空の値を除く) */
|
|
37
|
-
const secretsOf = (secret) => (typeof secret === "string" ? [secret] : [...secret ?? []]).filter(Boolean);
|
|
38
|
-
/** 署名のヘッダーの値(t=<秒>,v1=<署名>、鍵が複数なら v1 を鍵の順に並べる) */
|
|
39
|
-
async function signParts(secret, parts, now = Date.now()) {
|
|
40
|
-
const t = Math.floor(now / 1e3);
|
|
41
|
-
const message = await messageOf(t, parts);
|
|
42
|
-
const sigs = await Promise.all(secretsOf(secret).map(async (s) => base64Url(await crypto.subtle.sign("HMAC", await hmacKey(s), message))));
|
|
43
|
-
return [`t=${t}`, ...sigs.map((sig) => `v1=${sig}`)].join(",");
|
|
44
|
-
}
|
|
45
|
-
/** 署名の照合(鍵のどれかと v1 のどれかが合えば true、形の誤り・時刻のずれの超過・不一致は false) */
|
|
46
|
-
async function verifyParts(secret, header, parts, { now = Date.now(), maxSkewSec = MAX_SKEW_SEC } = {}) {
|
|
47
|
-
const secrets = secretsOf(secret);
|
|
48
|
-
if (!secrets.length || !header) return false;
|
|
49
|
-
const fields = header.split(",").map((f) => f.trim().split("=", 2));
|
|
50
|
-
const t = Number(fields.find(([k]) => k === "t")?.[1]);
|
|
51
|
-
const sigs = fields.flatMap(([k, v]) => k === "v1" && v ? [v] : []);
|
|
52
|
-
if (!Number.isInteger(t) || !sigs.length || Math.abs(now / 1e3 - t) > maxSkewSec) return false;
|
|
53
|
-
const message = await messageOf(t, parts);
|
|
54
|
-
for (const s of secrets) {
|
|
55
|
-
const key = await hmacKey(s);
|
|
56
|
-
for (const sig of sigs) try {
|
|
57
|
-
if (await crypto.subtle.verify("HMAC", key, decodeBase64Url(sig), message)) return true;
|
|
58
|
-
} catch {}
|
|
59
|
-
}
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
/** 署名を付けたヘッダー(init のヘッダーに署名と主体を追加した新しい Headers) */
|
|
63
|
-
async function signedHeaders(secret, { method, url, body, subject }, { header, subjectHeader, init, now }) {
|
|
64
|
-
const headers = new Headers(init);
|
|
65
|
-
if (subjectHeader && subject) headers.set(subjectHeader, subject);
|
|
66
|
-
headers.set(header, await signParts(secret, {
|
|
67
|
-
method,
|
|
68
|
-
url,
|
|
69
|
-
body: body ?? null,
|
|
70
|
-
subject: subjectHeader ? subject : null
|
|
71
|
-
}, now));
|
|
72
|
-
return headers;
|
|
73
|
-
}
|
|
74
|
-
/** リクエストの署名の照合(本文は複製から読み込み、元のリクエストの本文は残す、鍵の移行の期間は受け手が新旧の鍵を並べて渡せる) */
|
|
75
|
-
async function verifyRequest(secret, request, { header, subjectHeader, now }) {
|
|
76
|
-
const value = request.headers.get(header);
|
|
77
|
-
if (!secretsOf(secret).length || !value) return false;
|
|
78
|
-
const body = request.body ? await request.clone().arrayBuffer() : null;
|
|
79
|
-
return verifyParts(secret, value, {
|
|
80
|
-
method: request.method,
|
|
81
|
-
url: request.url,
|
|
82
|
-
body,
|
|
83
|
-
subject: subjectHeader ? request.headers.get(subjectHeader) : null
|
|
84
|
-
}, now === void 0 ? {} : { now });
|
|
85
|
-
}
|
|
86
|
-
/** 封印した値(鍵のどれかで署名が合えば値、署名の不一致・期限切れ・形の誤りは null) */
|
|
87
|
-
async function openValue(secret, token, now = Date.now()) {
|
|
88
|
-
const [body, sig] = token?.split(".") ?? [];
|
|
89
|
-
const secrets = secretsOf(secret);
|
|
90
|
-
if (!secrets.length || !body || !sig) return null;
|
|
91
|
-
try {
|
|
92
|
-
let matched = false;
|
|
93
|
-
for (const s of secrets) if (!matched) matched = await crypto.subtle.verify("HMAC", await hmacKey(s), decodeBase64Url(sig), enc.encode(body));
|
|
94
|
-
if (!matched) return null;
|
|
95
|
-
const { v, exp } = JSON.parse(new TextDecoder().decode(decodeBase64Url(body)));
|
|
96
|
-
return typeof exp === "number" && exp * 1e3 > now ? v : null;
|
|
97
|
-
} catch {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// ../access/src/traceparent.ts
|
|
102
|
-
/** traceparent のヘッダーの名前 */
|
|
103
|
-
const TRACEPARENT_HEADER = "traceparent";
|
|
104
|
-
const PATTERN = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(-.*)?$/;
|
|
105
|
-
const ZERO_TRACE = "0".repeat(32);
|
|
106
|
-
const ZERO_SPAN = "0".repeat(16);
|
|
107
|
-
/** 受け取った traceparent の解釈(なし・形の違反は null) */
|
|
108
|
-
function parseTraceparent(value) {
|
|
109
|
-
const m = value ? PATTERN.exec(value.trim()) : null;
|
|
110
|
-
if (!m) return null;
|
|
111
|
-
const [, version, traceId, spanId, flags, rest] = m;
|
|
112
|
-
if (version === "ff" || version === "00" && rest !== void 0) return null;
|
|
113
|
-
if (traceId === ZERO_TRACE || spanId === ZERO_SPAN) return null;
|
|
114
|
-
return {
|
|
115
|
-
traceId,
|
|
116
|
-
spanId,
|
|
117
|
-
sampled: (parseInt(flags, 16) & 1) === 1
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
/** traceparent の値(版は00) */
|
|
121
|
-
const formatTraceparent = (ctx) => `00-${ctx.traceId}-${ctx.spanId}-${ctx.sampled ? "01" : "00"}`;
|
|
122
|
-
function randomHexId(bytes) {
|
|
123
|
-
const values = crypto.getRandomValues(new Uint8Array(bytes));
|
|
124
|
-
let hex = "";
|
|
125
|
-
for (const b of values) hex += b.toString(16).padStart(2, "0");
|
|
126
|
-
return hex;
|
|
127
|
-
}
|
|
128
|
-
/** 新しいトレースの ID(32桁) */
|
|
129
|
-
const newTraceId = () => randomHexId(16);
|
|
130
|
-
/** 新しい区間の ID(16桁) */
|
|
131
|
-
const newSpanId = () => randomHexId(8);
|
|
132
|
-
// ../access/src/span.ts
|
|
133
|
-
/** 記録の割合(0〜1、なし・範囲の外・数でない値は1) */
|
|
134
|
-
const sampleRateOf = (value) => {
|
|
135
|
-
const rate = value === void 0 || value.trim() === "" ? NaN : Number(value);
|
|
136
|
-
return rate >= 0 && rate <= 1 ? rate : 1;
|
|
137
|
-
};
|
|
138
|
-
/** エントリポイントの区間の開始(受け取った traceparent の子か、新しいトレースの根) */
|
|
139
|
-
function startEdgeSpan({ name, request, sampleRate = 1, write = (line) => console.info(line) }) {
|
|
140
|
-
const parent = parseTraceparent(request.headers.get(TRACEPARENT_HEADER));
|
|
141
|
-
const context = {
|
|
142
|
-
traceId: parent?.traceId ?? newTraceId(),
|
|
143
|
-
spanId: newSpanId(),
|
|
144
|
-
sampled: parent ? parent.sampled : Math.random() < sampleRate
|
|
145
|
-
};
|
|
146
|
-
const attributes = {
|
|
147
|
-
method: request.method,
|
|
148
|
-
path: new URL(request.url).pathname
|
|
149
|
-
};
|
|
150
|
-
const started = performance.now();
|
|
151
|
-
let ended = false;
|
|
152
|
-
return {
|
|
153
|
-
context,
|
|
154
|
-
traceparent: formatTraceparent(context),
|
|
155
|
-
end(result) {
|
|
156
|
-
if (ended) return;
|
|
157
|
-
ended = true;
|
|
158
|
-
if (!context.sampled) return;
|
|
159
|
-
const status = result === "error" || result >= 500 ? "error" : "ok";
|
|
160
|
-
const durationMs = Math.round((performance.now() - started) * 10) / 10;
|
|
161
|
-
const span = {
|
|
162
|
-
traceId: context.traceId,
|
|
163
|
-
spanId: context.spanId,
|
|
164
|
-
parentSpanId: parent?.spanId ?? null,
|
|
165
|
-
name,
|
|
166
|
-
durationMs,
|
|
167
|
-
status,
|
|
168
|
-
attributes: {
|
|
169
|
-
...attributes,
|
|
170
|
-
...result !== "error" && { status: result }
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
write(JSON.stringify({
|
|
174
|
-
level: "info",
|
|
175
|
-
event: "span",
|
|
176
|
-
...span
|
|
177
|
-
}));
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
/** 処理をエントリポイントの区間で包む(応答のステータスか例外で終了、例外は再送出) */
|
|
182
|
-
async function withEdgeSpan(options, handle) {
|
|
183
|
-
const span = startEdgeSpan(options);
|
|
184
|
-
try {
|
|
185
|
-
const res = await handle(span);
|
|
186
|
-
span.end(res.status);
|
|
187
|
-
return res;
|
|
188
|
-
} catch (e) {
|
|
189
|
-
span.end("error");
|
|
190
|
-
throw e;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
export { openValue as a, verifyParts as c, SIGNED_HEADERS as i, verifyRequest as l, withEdgeSpan as n, signParts as o, TRACEPARENT_HEADER as r, signedHeaders as s, sampleRateOf as t };
|
package/dist/trace.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './types/sdk/src/trace.js';
|
package/dist/trace.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { r as TRACEPARENT_HEADER, t as sampleRateOf, n as withEdgeSpan } from "./src-BMOqN7Xs.js";
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { type TraceContext } from './traceparent.js';
|
|
2
|
-
/** 区間の結果(ok・error、logTracer と同じ値) */
|
|
3
|
-
export type EdgeSpanStatus = 'ok' | 'error';
|
|
4
|
-
/** エントリポイントの区間(context は転送先へ受け渡す自身の区間、traceparent はその値) */
|
|
5
|
-
export type EdgeSpan = Readonly<{
|
|
6
|
-
context: TraceContext;
|
|
7
|
-
traceparent: string;
|
|
8
|
-
/** 応答のステータスの数か例外で終了(500以上・例外は error、2回目以降は無視) */
|
|
9
|
-
end: (result: number | 'error') => void;
|
|
10
|
-
}>;
|
|
11
|
-
/** エントリポイントの区間の引数(write は1行の出力先、既定は console.info) */
|
|
12
|
-
export type EdgeSpanOptions = Readonly<{
|
|
13
|
-
name: string;
|
|
14
|
-
request: Request;
|
|
15
|
-
sampleRate?: number;
|
|
16
|
-
write?: (line: string) => void;
|
|
17
|
-
}>;
|
|
18
|
-
/** 記録の割合(0〜1、なし・範囲の外・数でない値は1) */
|
|
19
|
-
export declare const sampleRateOf: (value: string | undefined) => number;
|
|
20
|
-
/** エントリポイントの区間の開始(受け取った traceparent の子か、新しいトレースの根) */
|
|
21
|
-
export declare function startEdgeSpan({ name, request, sampleRate, write }: EdgeSpanOptions): EdgeSpan;
|
|
22
|
-
/** 処理をエントリポイントの区間で包む(応答のステータスか例外で終了、例外は再送出) */
|
|
23
|
-
export declare function withEdgeSpan(options: EdgeSpanOptions, handle: (span: EdgeSpan) => Promise<Response>): Promise<Response>;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/** traceparent のヘッダーの名前 */
|
|
2
|
-
export declare const TRACEPARENT_HEADER = "traceparent";
|
|
3
|
-
/** トレースの文脈(traceId は32桁、spanId は呼び出し元の区間の16桁、sampled は記録の対象か) */
|
|
4
|
-
export type TraceContext = Readonly<{
|
|
5
|
-
traceId: string;
|
|
6
|
-
spanId: string;
|
|
7
|
-
sampled: boolean;
|
|
8
|
-
}>;
|
|
9
|
-
/** 受け取った traceparent の解釈(なし・形の違反は null) */
|
|
10
|
-
export declare function parseTraceparent(value: string | null | undefined): TraceContext | null;
|
|
11
|
-
/** traceparent の値(版は00) */
|
|
12
|
-
export declare const formatTraceparent: (ctx: TraceContext) => string;
|
|
13
|
-
/** 新しいトレースの ID(32桁) */
|
|
14
|
-
export declare const newTraceId: () => string;
|
|
15
|
-
/** 新しい区間の ID(16桁) */
|
|
16
|
-
export declare const newSpanId: () => string;
|
|
17
|
-
/**
|
|
18
|
-
* 呼び出し元の区間の文脈(受け取った traceparent があれば同じトレースの子、なければ新しいトレース)
|
|
19
|
-
* - sampled: 受け取った値のフラグ、新しいトレースは引数(既定は記録の対象)
|
|
20
|
-
*/
|
|
21
|
-
export declare function childTraceContext(incoming: string | null | undefined, sampled?: boolean): TraceContext;
|