@ryusenkei/maezae 0.0.0-stage → 0.1.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.
@@ -0,0 +1,44 @@
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 };
@@ -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 { SIGNED_HEADERS, signParts, signedHeaders, verifyParts, verifyRequest };
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@ryusenkei/maezae",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "maezae の導入先のサイト向けの SDK(配信 API の取得・本文の型・プレビューの署名)",
5
+ "license": "UNLICENSED",
6
+ "author": "RYUSENKEI",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ryusenkei-inc/maezae.git",
10
+ "directory": "packages/sdk"
11
+ },
12
+ "type": "module",
13
+ "sideEffects": false,
14
+ "exports": {
15
+ "./delivery": {
16
+ "types": "./dist/delivery.d.ts",
17
+ "default": "./dist/delivery.js"
18
+ },
19
+ "./body": {
20
+ "types": "./dist/body.d.ts",
21
+ "default": "./dist/body.js"
22
+ },
23
+ "./model": {
24
+ "types": "./dist/model.d.ts",
25
+ "default": "./dist/model.js"
26
+ },
27
+ "./preview": {
28
+ "types": "./dist/preview.d.ts",
29
+ "default": "./dist/preview.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "zod": "^4.6.7"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^26.6.6",
43
+ "tsdown": "^0.23.7",
44
+ "typescript": "^5.9.3"
45
+ },
46
+ "scripts": {
47
+ "build": "node --max-old-space-size=6144 scripts/build.ts && node scripts/check-dist.ts",
48
+ "typecheck": "tsc"
49
+ }
6
50
  }