@exabugs/dynamodb-client 1.10.0 → 1.11.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -0
  3. package/dist/notify/channels/email.d.ts +32 -0
  4. package/dist/notify/channels/email.d.ts.map +1 -0
  5. package/dist/notify/channels/email.js +67 -0
  6. package/dist/notify/channels/email.js.map +1 -0
  7. package/dist/notify/channels/slack.d.ts +12 -0
  8. package/dist/notify/channels/slack.d.ts.map +1 -0
  9. package/dist/notify/channels/slack.js +46 -0
  10. package/dist/notify/channels/slack.js.map +1 -0
  11. package/dist/notify/format.d.ts +7 -0
  12. package/dist/notify/format.d.ts.map +1 -0
  13. package/dist/notify/format.js +29 -0
  14. package/dist/notify/format.js.map +1 -0
  15. package/dist/notify/handler.d.ts +42 -0
  16. package/dist/notify/handler.d.ts.map +1 -0
  17. package/dist/notify/handler.js +175 -0
  18. package/dist/notify/handler.js.map +1 -0
  19. package/dist/notify/index.d.ts +18 -0
  20. package/dist/notify/index.d.ts.map +1 -0
  21. package/dist/notify/index.js +14 -0
  22. package/dist/notify/index.js.map +1 -0
  23. package/dist/notify/logger.d.ts +6 -0
  24. package/dist/notify/logger.d.ts.map +1 -0
  25. package/dist/notify/logger.js +11 -0
  26. package/dist/notify/logger.js.map +1 -0
  27. package/dist/notify/normalize.d.ts +11 -0
  28. package/dist/notify/normalize.d.ts.map +1 -0
  29. package/dist/notify/normalize.js +245 -0
  30. package/dist/notify/normalize.js.map +1 -0
  31. package/dist/notify/policy.d.ts +15 -0
  32. package/dist/notify/policy.d.ts.map +1 -0
  33. package/dist/notify/policy.js +36 -0
  34. package/dist/notify/policy.js.map +1 -0
  35. package/dist/notify/store.d.ts +22 -0
  36. package/dist/notify/store.d.ts.map +1 -0
  37. package/dist/notify/store.js +119 -0
  38. package/dist/notify/store.js.map +1 -0
  39. package/dist/notify/types.d.ts +63 -0
  40. package/dist/notify/types.d.ts.map +1 -0
  41. package/dist/notify/types.js +8 -0
  42. package/dist/notify/types.js.map +1 -0
  43. package/dist/server/handler.cjs +3 -3
  44. package/dist/server/media-handler.cjs +2 -2
  45. package/dist/server/media-process-handler-dlq.cjs +2 -2
  46. package/dist/server/media-process-handler.cjs +2 -2
  47. package/dist/sharp-layer.zip +0 -0
  48. package/docs/NOTIFY.md +103 -0
  49. package/package.json +8 -3
package/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.11.0] - 2026-10-12
11
+
12
+ ### Added
13
+
14
+ - **運用通知 `@exabugs/dynamodb-client/notify`**(#59): ログ(`warn` 以上)・CloudWatch アラーム・直接呼び出しを `Notice` にそろえ、重複抑制・流量制限のうえ、Slack・メールへ配信する部品。サブパスの追加のみで、既存の挙動は変わらない(Records Lambda のバンドルも変わらない)。`@aws-sdk/client-sesv2` は、アプリが静的に import して渡す(依存に追加しない)。詳細は [docs/NOTIFY.md](docs/NOTIFY.md)
15
+
10
16
  ## [1.10.0] - 2026-10-12
11
17
 
12
18
  結果を黙って欠く・変える経路を直した (#55)。**挙動が変わる変更を含む**ので、アップグレード時はこの節を確認すること。
package/README.md CHANGED
@@ -196,6 +196,7 @@ const article = await articles.findOne({ title: 'Hello DynamoDB' });
196
196
  - **[React Admin Integration](docs/react-admin-integration.md)** - Admin UI setup and many-to-many relationships
197
197
  - **[Authorization & Secure Setup](docs/AUTHORIZATION.md)** - 認可モデルと安全な構成(必読。JWT の有効性しか検証しない/管理者専用ユーザープール/`aud` 検証)
198
198
  - **[疎な検索の警告と対応](docs/SCAN_WARNING.md)** - 検索を打ち切らず、警告(SPARSE_SCAN)で気づいてインデックス・スキーマ・ロジックを見直す
199
+ - **[運用通知](docs/NOTIFY.md)** - ログ・アラームを、Slack・メールで管理者へ通知する部品(`@exabugs/dynamodb-client/notify`)
199
200
  - **[Deployment](docs/DEPLOYMENT.md)** - Production deployment guide
200
201
  - **[Terraform Modules](terraform/README.md)** - Infrastructure as Code
201
202
 
@@ -0,0 +1,32 @@
1
+ import type { Channel, Notice } from '../types.js';
2
+ export interface EmailMessage {
3
+ from: string;
4
+ /** 宛先は Bcc だけ(管理者どうしにアドレスが見えない。To に送信元を入れない: 受信できないアドレスだとバウンスする) */
5
+ bcc: string[];
6
+ subject: string;
7
+ text: string;
8
+ }
9
+ export type EmailSender = (message: EmailMessage) => Promise<void>;
10
+ export declare function emailText(notices: Notice[]): string;
11
+ export interface EmailOptions {
12
+ from: string;
13
+ send: EmailSender;
14
+ }
15
+ export declare function createEmailChannel(options: EmailOptions): Channel;
16
+ interface SesCommandCtor {
17
+ new (input: unknown): unknown;
18
+ }
19
+ interface SesClientLike {
20
+ send(command: unknown): Promise<unknown>;
21
+ }
22
+ /**
23
+ * SES v2 で送る。アプリが `@aws-sdk/client-sesv2` を静的に import して渡す(バンドラが依存を解決でき、
24
+ * Lambda のバンドルに含まれる):
25
+ * sesSender({ client: new SESv2Client({ region: 'us-east-1' }), SendEmailCommand })
26
+ */
27
+ export declare function sesSender(options: {
28
+ client: SesClientLike;
29
+ SendEmailCommand: SesCommandCtor;
30
+ }): EmailSender;
31
+ export {};
32
+ //# sourceMappingURL=email.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../../../src/notify/channels/email.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAEnD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAKnE,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAKnD;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAoCjE;AAED,UAAU,cAAc;IACtB,KAAK,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;CAC/B;AACD,UAAU,aAAa;IACrB,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE;IACjC,MAAM,EAAE,aAAa,CAAC;IACtB,gBAAgB,EAAE,cAAc,CAAC;CAClC,GAAG,WAAW,CAed"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * メール(SES v2)。宛先(管理者)は Bcc にまとめて送る(管理者どうしにアドレスが見えない)。
3
+ * 送信は注入できる(`sesSender` は @aws-sdk/client-sesv2 を使う。アプリが渡す)。
4
+ */
5
+ import { bodyOf, subjectOf } from '../format.js';
6
+ const BCC_BATCH = 50;
7
+ const EMAIL = /^[^\s@<>,;]+@[^\s@<>,;]+\.[^\s@<>,;]+$/;
8
+ export function emailText(notices) {
9
+ return (notices.map((n) => `${subjectOf(n)}\n\n${bodyOf(n)}`).join('\n\n----------\n\n') +
10
+ '\n\n(このメールは送信専用です。返信はできません)\n');
11
+ }
12
+ export function createEmailChannel(options) {
13
+ return {
14
+ name: 'email',
15
+ needsRecipients: true,
16
+ async send(notices, recipients) {
17
+ if (notices.length === 0)
18
+ return;
19
+ const valid = [...new Set(recipients.map((r) => r.trim().toLowerCase()))].filter((r) => EMAIL.test(r));
20
+ if (valid.length === 0)
21
+ throw new Error('メールの宛先がありません(管理者のメールアドレスが空・不正)');
22
+ const subject = notices.length === 1
23
+ ? subjectOf(notices[0])
24
+ : `[${notices[0].app}/${notices[0].env}] 通知 ${notices.length} 件`;
25
+ const text = emailText(notices);
26
+ let failed = 0;
27
+ let firstError;
28
+ for (let i = 0; i < valid.length; i += BCC_BATCH) {
29
+ try {
30
+ await options.send({
31
+ from: options.from,
32
+ bcc: valid.slice(i, i + BCC_BATCH),
33
+ subject,
34
+ text,
35
+ });
36
+ }
37
+ catch (e) {
38
+ failed += 1;
39
+ // 原因(SES のエラー名など)を残す。宛先のアドレスは含めない
40
+ firstError ??= e instanceof Error ? `${e.name}: ${e.message}` : 'unknown error';
41
+ }
42
+ }
43
+ if (failed > 0)
44
+ throw new Error(`メールの送信に失敗した組があります (${failed} 組): ${firstError}`);
45
+ },
46
+ };
47
+ }
48
+ /**
49
+ * SES v2 で送る。アプリが `@aws-sdk/client-sesv2` を静的に import して渡す(バンドラが依存を解決でき、
50
+ * Lambda のバンドルに含まれる):
51
+ * sesSender({ client: new SESv2Client({ region: 'us-east-1' }), SendEmailCommand })
52
+ */
53
+ export function sesSender(options) {
54
+ return async (m) => {
55
+ await options.client.send(new options.SendEmailCommand({
56
+ FromEmailAddress: m.from,
57
+ Destination: { BccAddresses: m.bcc },
58
+ Content: {
59
+ Simple: {
60
+ Subject: { Data: m.subject, Charset: 'UTF-8' },
61
+ Body: { Text: { Data: m.text, Charset: 'UTF-8' } },
62
+ },
63
+ },
64
+ }));
65
+ };
66
+ }
67
+ //# sourceMappingURL=email.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.js","sourceRoot":"","sources":["../../../src/notify/channels/email.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { Channel, Notice } from '../types.js';
2
+ export declare function slackText(notices: Notice[]): string;
3
+ export interface SlackOptions {
4
+ /** Webhook の URL(SSM など。呼び出しごとに取得してよい) */
5
+ getWebhookUrl: () => Promise<string>;
6
+ /** Webhook の URL が、この文字列で始まること(既定 https://hooks.slack.com/)。不正な URL への送信を防ぐ */
7
+ urlPrefix?: string;
8
+ fetchImpl?: typeof fetch;
9
+ timeoutMs?: number;
10
+ }
11
+ export declare function createSlackChannel(options: SlackOptions): Channel;
12
+ //# sourceMappingURL=slack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../../src/notify/channels/slack.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAQnD,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAKnD;AAED,MAAM,WAAW,YAAY;IAC3B,0CAA0C;IAC1C,aAAa,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACrC,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CA0BjE"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Slack(Incoming Webhook)。チャンネル宛なので、宛先(管理者)は不要。
3
+ * Webhook の URL は秘密。エラーにも出さない。
4
+ */
5
+ import { bodyOf, mark, subjectOf } from '../format.js';
6
+ const MAX_PER_MESSAGE = 10;
7
+ /** Slack の mrkdwn で特別な文字を無害にする */
8
+ const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
9
+ export function slackText(notices) {
10
+ const shown = notices.slice(0, MAX_PER_MESSAGE);
11
+ const parts = shown.map((n) => `${mark(n)} *${esc(subjectOf(n))}*\n${esc(bodyOf(n))}`);
12
+ if (notices.length > shown.length)
13
+ parts.push(`…ほか ${notices.length - shown.length} 件`);
14
+ return parts.join('\n\n');
15
+ }
16
+ export function createSlackChannel(options) {
17
+ const doFetch = options.fetchImpl ?? fetch;
18
+ return {
19
+ name: 'slack',
20
+ needsRecipients: false,
21
+ async send(notices) {
22
+ if (notices.length === 0)
23
+ return;
24
+ const url = await options.getWebhookUrl();
25
+ if (!url.startsWith(options.urlPrefix ?? 'https://hooks.slack.com/')) {
26
+ throw new Error('Slack の Webhook の URL が不正です(許可した接頭辞で始まっていません)');
27
+ }
28
+ let res;
29
+ try {
30
+ res = await doFetch(url, {
31
+ method: 'POST',
32
+ headers: { 'Content-Type': 'application/json' },
33
+ body: JSON.stringify({ text: slackText(notices) }),
34
+ signal: AbortSignal.timeout(options.timeoutMs ?? 5000),
35
+ });
36
+ }
37
+ catch (e) {
38
+ // fetch のエラーには URL(秘密)が入ることがあるので、原因の種類だけを残す
39
+ throw new Error(`Slack への送信に失敗しました (${e instanceof Error ? e.name : 'error'})`);
40
+ }
41
+ if (!res.ok)
42
+ throw new Error(`Slack への送信に失敗しました (HTTP ${res.status})`);
43
+ },
44
+ };
45
+ }
46
+ //# sourceMappingURL=slack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack.js","sourceRoot":"","sources":["../../../src/notify/channels/slack.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAGvD,MAAM,eAAe,GAAG,EAAE,CAAC;AAE3B,kCAAkC;AAClC,MAAM,GAAG,GAAG,CAAC,CAAS,EAAU,EAAE,CAChC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAEvE,MAAM,UAAU,SAAS,CAAC,OAAiB;IACzC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACvF,IAAI,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,OAAO,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IACxF,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAWD,MAAM,UAAU,kBAAkB,CAAC,OAAqB;IACtD,MAAM,OAAO,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC;IAC3C,OAAO;QACL,IAAI,EAAE,OAAO;QACb,eAAe,EAAE,KAAK;QACtB,KAAK,CAAC,IAAI,CAAC,OAAO;YAChB,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO;YACjC,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,aAAa,EAAE,CAAC;YAC1C,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,IAAI,0BAA0B,CAAC,EAAE,CAAC;gBACrE,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;YACnE,CAAC;YACD,IAAI,GAAa,CAAC;YAClB,IAAI,CAAC;gBACH,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE;oBACvB,MAAM,EAAE,MAAM;oBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;oBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;oBAClD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC;iBACvD,CAAC,CAAC;YACL,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,4CAA4C;gBAC5C,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC;YAClF,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;QACzE,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { Notice } from './types.js';
2
+ export declare const LEVEL_LABEL: Record<Notice['level'], string>;
3
+ export declare const mark: (n: Notice) => string;
4
+ export declare function subjectOf(n: Notice): string;
5
+ /** 本文(プレーンテキスト)。Slack・メールで共通 */
6
+ export declare function bodyOf(n: Notice): string;
7
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/notify/format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAKvD,CAAC;AASF,eAAO,MAAM,IAAI,GAAI,GAAG,MAAM,KAAG,MAA6B,CAAC;AAE/D,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED,gCAAgC;AAChC,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAOxC"}
@@ -0,0 +1,29 @@
1
+ export const LEVEL_LABEL = {
2
+ warn: 'WARNING',
3
+ error: 'ERROR',
4
+ fatal: 'FATAL',
5
+ ok: '復旧',
6
+ };
7
+ const LEVEL_MARK = {
8
+ warn: '⚠️',
9
+ error: '🚨',
10
+ fatal: '🔥',
11
+ ok: '✅',
12
+ };
13
+ export const mark = (n) => LEVEL_MARK[n.level];
14
+ export function subjectOf(n) {
15
+ return `[${n.app}/${n.env}] ${LEVEL_LABEL[n.level]} ${n.event}`.replace(/[\r\n]+/g, ' ');
16
+ }
17
+ /** 本文(プレーンテキスト)。Slack・メールで共通 */
18
+ export function bodyOf(n) {
19
+ const lines = [n.message];
20
+ if (n.target)
21
+ lines.push(`対象: ${n.target}`);
22
+ if (n.action)
23
+ lines.push(`対策: ${n.action}`);
24
+ if (n.suppressed > 0)
25
+ lines.push(`直近の窓で抑制した同種の通知: ${n.suppressed} 件`);
26
+ lines.push(`発生: ${n.occurredAt}`);
27
+ return lines.join('\n');
28
+ }
29
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/notify/format.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,WAAW,GAAoC;IAC1D,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,OAAO;IACd,KAAK,EAAE,OAAO;IACd,EAAE,EAAE,IAAI;CACT,CAAC;AAEF,MAAM,UAAU,GAAoC;IAClD,IAAI,EAAE,IAAI;IACV,KAAK,EAAE,IAAI;IACX,KAAK,EAAE,IAAI;IACX,EAAE,EAAE,GAAG;CACR,CAAC;AAEF,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAE/D,MAAM,UAAU,SAAS,CAAC,CAAS;IACjC,OAAO,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,KAAK,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;AAC3F,CAAC;AAED,gCAAgC;AAChC,MAAM,UAAU,MAAM,CAAC,CAAS;IAC9B,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAC1B,IAAI,CAAC,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5C,IAAI,CAAC,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5C,IAAI,CAAC,CAAC,UAAU,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC;IACtE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC;IAClC,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,42 @@
1
+ import type { AdminDirectory, Channel, Notice, NoticeLevel, NoticeStore, NotifyLogger } from './types.js';
2
+ export interface NotifyHandlerOptions {
3
+ app: string;
4
+ env: string;
5
+ channels: Channel[];
6
+ store: NoticeStore;
7
+ /** メールなど、宛先が必要なチャネルがあるときに必要 */
8
+ admins?: AdminDirectory;
9
+ windows?: Partial<Record<NoticeLevel, number>>;
10
+ rate?: {
11
+ windowSec: number;
12
+ limit: number;
13
+ };
14
+ /** 管理者の一覧のキャッシュ(秒)。既定 300 */
15
+ adminCacheSec?: number;
16
+ /** 送らず、計画(何を・どこへ)だけを返す。store には触れない */
17
+ dryRun?: boolean;
18
+ /** 1 つでもチャネルが失敗したら例外にする(既定: 全チャネルが失敗したときだけ) */
19
+ failOnAnyChannelError?: boolean;
20
+ now?: () => Date;
21
+ logger?: NotifyLogger;
22
+ }
23
+ export interface NotifyResult {
24
+ /** 正規化した通知の数(同じバッチの重複をまとめる前) */
25
+ received: number;
26
+ /** 1 つ以上のチャネルに届いた通知の数(省略の通知は含まない) */
27
+ sent: number;
28
+ /** 重複抑制で送らなかった通知の数 */
29
+ suppressed: number;
30
+ /** 流量制限で、あるチャネルに送らなかった通知の延べ数 */
31
+ throttled: number;
32
+ /** 失敗したチャネル */
33
+ failedChannels: string[];
34
+ /** dry-run のときの計画 */
35
+ plan?: Array<{
36
+ channel: string;
37
+ recipients: string[];
38
+ notices: Notice[];
39
+ }>;
40
+ }
41
+ export declare function createNotifyHandler(options: NotifyHandlerOptions): (event: unknown) => Promise<NotifyResult>;
42
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/notify/handler.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,cAAc,EACd,OAAO,EACP,MAAM,EACN,WAAW,EACX,WAAW,EACX,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,KAAK,EAAE,WAAW,CAAC;IACnB,+BAA+B;IAC/B,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,IAAI,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,6BAA6B;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uCAAuC;IACvC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,+CAA+C;IAC/C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,sBAAsB;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe;IACf,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,qBAAqB;IACrB,IAAI,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CAC5E;AASD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,IAoCjD,OAAO,OAAO,KAAG,OAAO,CAAC,YAAY,CAAC,CA6HrD"}
@@ -0,0 +1,175 @@
1
+ /**
2
+ * 通知ハンドラ(Lambda)。入力 → Notice → 同じバッチの重複をまとめる → 重複抑制 → 流量制限 → チャネルへ配信。
3
+ *
4
+ * - 重複抑制: 先に確保 → 送信 → どのチャネルにも届かなかった分は確保を解放(次の発生で、窓を待たずに通知できる)
5
+ * - 流量制限: チャネルごとに、窓の中の件数の上限。超えたら「省略」の 1 通だけ送る
6
+ * - 全チャネルが失敗したら例外を投げる(Lambda の失敗先に流す)。一部の失敗は `failedChannels` に入る
7
+ * - アラームの状態変化(`ALARM:` で始まる event)は、変化したときにしか届かないので、抑制しない
8
+ * - dry-run は記録(store)に触れない
9
+ * - このハンドラのロググループは購読しないこと(ループになる)
10
+ */
11
+ import { consoleNotifyLogger } from './logger.js';
12
+ import { normalize } from './normalize.js';
13
+ import { DEFAULT_RATE, DEFAULT_WINDOWS, fingerprintOf } from './policy.js';
14
+ export function createNotifyHandler(options) {
15
+ const now = options.now ?? (() => new Date());
16
+ const logger = options.logger ?? consoleNotifyLogger;
17
+ const windows = { ...DEFAULT_WINDOWS, ...options.windows };
18
+ const rate = options.rate ?? DEFAULT_RATE;
19
+ const adminCacheSec = options.adminCacheSec ?? 300;
20
+ const ctx = { app: options.app, env: options.env, now };
21
+ if (!options.admins && options.channels.some((c) => c.needsRecipients)) {
22
+ throw new Error('宛先が必要なチャネルがあるのに、admins が指定されていません');
23
+ }
24
+ let cache;
25
+ const recipients = async () => {
26
+ if (!options.admins)
27
+ throw new Error('admins が指定されていません');
28
+ const t = now().getTime();
29
+ if (cache && cache.until > t)
30
+ return cache.emails;
31
+ const emails = await options.admins.listEmails();
32
+ cache = { emails, until: t + adminCacheSec * 1000 };
33
+ return emails;
34
+ };
35
+ const windowOf = (n) => (n.event.startsWith('ALARM:') ? 0 : windows[n.level]);
36
+ /** 同じ指紋・同じレベルの通知を、バッチの中で 1 つにまとめる */
37
+ const group = (notices) => {
38
+ const map = new Map();
39
+ for (const n of notices) {
40
+ const fp = fingerprintOf(n);
41
+ const key = `${n.level}#${fp}`;
42
+ const g = map.get(key);
43
+ if (g)
44
+ g.count += 1;
45
+ else
46
+ map.set(key, { notice: n, count: 1, fp });
47
+ }
48
+ return [...map.values()];
49
+ };
50
+ return async (event) => {
51
+ const notices = normalize(event, ctx);
52
+ const result = {
53
+ received: notices.length,
54
+ sent: 0,
55
+ suppressed: 0,
56
+ throttled: 0,
57
+ failedChannels: [],
58
+ };
59
+ const nowSec = Math.floor(now().getTime() / 1000);
60
+ const groups = group(notices);
61
+ const candidates = [];
62
+ const delivered = new Set();
63
+ try {
64
+ for (const g of groups) {
65
+ const windowSec = windowOf(g.notice);
66
+ if (windowSec > 0 && !options.dryRun) {
67
+ const c = await options.store.claim(g.fp, windowSec, nowSec, g.count);
68
+ if (!c.granted) {
69
+ result.suppressed += g.count;
70
+ continue;
71
+ }
72
+ g.claim = { token: c.token, prev: c.suppressed };
73
+ g.notice = { ...g.notice, suppressed: c.suppressed + g.count - 1 };
74
+ }
75
+ else {
76
+ g.notice = { ...g.notice, suppressed: g.count - 1 };
77
+ }
78
+ candidates.push(g);
79
+ }
80
+ if (candidates.length === 0)
81
+ return result;
82
+ const plan = [];
83
+ for (const ch of options.channels) {
84
+ let to = [];
85
+ if (ch.needsRecipients) {
86
+ // 宛先の取得の失敗は、そのチャネルの失敗にする(他のチャネルは送る)
87
+ try {
88
+ to = await recipients();
89
+ }
90
+ catch (e) {
91
+ result.failedChannels.push(ch.name);
92
+ logger.error('宛先(管理者)の取得に失敗しました', {
93
+ channel: ch.name,
94
+ error: e instanceof Error ? e.message : String(e),
95
+ });
96
+ continue;
97
+ }
98
+ }
99
+ let allowed = candidates;
100
+ let extra;
101
+ if (!options.dryRun) {
102
+ const total = await options.store.consume(ch.name, rate.windowSec, nowSec, candidates.length);
103
+ const before = total - candidates.length;
104
+ allowed = candidates.slice(0, Math.max(0, rate.limit - before));
105
+ const over = candidates.length - allowed.length;
106
+ if (over > 0) {
107
+ result.throttled += over;
108
+ // 上限を超えた最初のバッチにだけ、省略の通知を 1 通出す
109
+ if (before <= rate.limit) {
110
+ extra = {
111
+ level: 'warn',
112
+ event: 'NOTIFY_THROTTLED',
113
+ message: `通知が多いため、${rate.windowSec} 秒間の上限(${rate.limit} 件)を超えた分を省略します(このバッチで ${over} 件)`,
114
+ app: options.app,
115
+ env: options.env,
116
+ occurredAt: now().toISOString(),
117
+ suppressed: 0,
118
+ };
119
+ }
120
+ }
121
+ }
122
+ const batch = [...allowed.map((g) => g.notice), ...(extra ? [extra] : [])];
123
+ if (batch.length === 0)
124
+ continue;
125
+ if (options.dryRun) {
126
+ plan.push({ channel: ch.name, recipients: to, notices: batch });
127
+ continue;
128
+ }
129
+ try {
130
+ await ch.send(batch, to);
131
+ for (const g of allowed)
132
+ delivered.add(g);
133
+ }
134
+ catch (e) {
135
+ result.failedChannels.push(ch.name);
136
+ logger.error('通知の送信に失敗しました', {
137
+ channel: ch.name,
138
+ error: e instanceof Error ? e.message : String(e),
139
+ });
140
+ }
141
+ }
142
+ if (options.dryRun)
143
+ return { ...result, plan };
144
+ result.sent = delivered.size;
145
+ // どのチャネルにも届かなかった通知は、確保を解放する(流量制限で省かれた分も)
146
+ await releaseUndelivered(candidates, delivered);
147
+ const failedAll = options.channels.length > 0 && result.failedChannels.length === options.channels.length;
148
+ if (failedAll || (options.failOnAnyChannelError && result.failedChannels.length > 0)) {
149
+ throw new Error(`通知チャネルの送信に失敗しました: ${result.failedChannels.join(', ')}`);
150
+ }
151
+ return result;
152
+ }
153
+ catch (e) {
154
+ // 途中で例外になったときも、まだ送っていない確保を残さない(その指紋が窓の間、黙ってしまう)
155
+ await releaseUndelivered(groups, delivered).catch(() => undefined);
156
+ throw e;
157
+ }
158
+ };
159
+ async function releaseUndelivered(all, delivered) {
160
+ for (const g of all) {
161
+ if (delivered.has(g) || !g.claim)
162
+ continue;
163
+ try {
164
+ await options.store.release(g.fp, g.claim.token, g.claim.prev + g.count);
165
+ }
166
+ catch (e) {
167
+ logger.error('確保の解放に失敗しました', {
168
+ error: e instanceof Error ? e.message : String(e),
169
+ });
170
+ }
171
+ g.claim = undefined;
172
+ }
173
+ }
174
+ }
175
+ //# sourceMappingURL=handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/notify/handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 運用通知(`@exabugs/dynamodb-client/notify`)
3
+ *
4
+ * ログ(warn 以上)・CloudWatch アラーム・直接呼び出しを Notice にそろえ、重複抑制のうえ、
5
+ * Slack・メールなどのチャネルへ配信する部品。配線(購読フィルタ・Lambda・Terraform)は各アプリが行う。
6
+ */
7
+ export { createEmailChannel, sesSender } from './channels/email.js';
8
+ export type { EmailMessage, EmailOptions, EmailSender } from './channels/email.js';
9
+ export { createSlackChannel } from './channels/slack.js';
10
+ export type { SlackOptions } from './channels/slack.js';
11
+ export { createNotifyHandler } from './handler.js';
12
+ export type { NotifyHandlerOptions, NotifyResult } from './handler.js';
13
+ export { consoleNotifyLogger, logFatal } from './logger.js';
14
+ export { noticeFromLogLine, normalize } from './normalize.js';
15
+ export { DEFAULT_RATE, DEFAULT_WINDOWS, fingerprintOf } from './policy.js';
16
+ export { DynamoNoticeStore, MemoryNoticeStore } from './store.js';
17
+ export type { AdminDirectory, Channel, ClaimResult, Notice, NoticeLevel, NoticeStore, NotifyLogger, } from './types.js';
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notify/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACpE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACnF,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAClE,YAAY,EACV,cAAc,EACd,OAAO,EACP,WAAW,EACX,MAAM,EACN,WAAW,EACX,WAAW,EACX,YAAY,GACb,MAAM,YAAY,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 運用通知(`@exabugs/dynamodb-client/notify`)
3
+ *
4
+ * ログ(warn 以上)・CloudWatch アラーム・直接呼び出しを Notice にそろえ、重複抑制のうえ、
5
+ * Slack・メールなどのチャネルへ配信する部品。配線(購読フィルタ・Lambda・Terraform)は各アプリが行う。
6
+ */
7
+ export { createEmailChannel, sesSender } from './channels/email.js';
8
+ export { createSlackChannel } from './channels/slack.js';
9
+ export { createNotifyHandler } from './handler.js';
10
+ export { consoleNotifyLogger, logFatal } from './logger.js';
11
+ export { noticeFromLogLine, normalize } from './normalize.js';
12
+ export { DEFAULT_RATE, DEFAULT_WINDOWS, fingerprintOf } from './policy.js';
13
+ export { DynamoNoticeStore, MemoryNoticeStore } from './store.js';
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/notify/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { NotifyLogger } from './types.js';
2
+ /** 通知ハンドラ自身のログ(JSON 1 行)。通知ハンドラのログ(ロググループ)は購読しないこと(ループになる) */
3
+ export declare const consoleNotifyLogger: NotifyLogger;
4
+ /** アプリ側で fatal を記録するためのヘルパー(既存の logger は変えない)。購読フィルタが `fatal` を拾う */
5
+ export declare function logFatal(message: string, meta?: Record<string, unknown>): void;
6
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/notify/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,8DAA8D;AAC9D,eAAO,MAAM,mBAAmB,EAAE,YAOjC,CAAC;AAEF,qEAAqE;AACrE,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,IAAI,CAElF"}
@@ -0,0 +1,11 @@
1
+ /** 通知ハンドラ自身のログ(JSON 1 行)。通知ハンドラのログ(ロググループ)は購読しないこと(ループになる) */
2
+ export const consoleNotifyLogger = {
3
+ info: (message, meta) => console.log(JSON.stringify({ level: 'info', message, service: 'notify', ...meta })),
4
+ warn: (message, meta) => console.warn(JSON.stringify({ level: 'warn', message, service: 'notify', ...meta })),
5
+ error: (message, meta) => console.error(JSON.stringify({ level: 'error', message, service: 'notify', ...meta })),
6
+ };
7
+ /** アプリ側で fatal を記録するためのヘルパー(既存の logger は変えない)。購読フィルタが `fatal` を拾う */
8
+ export function logFatal(message, meta = {}) {
9
+ console.error(JSON.stringify({ level: 'fatal', message, ...meta }));
10
+ }
11
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/notify/logger.ts"],"names":[],"mappings":"AAEA,8DAA8D;AAC9D,MAAM,CAAC,MAAM,mBAAmB,GAAiB;IAC/C,IAAI,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,CACtB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;IACrF,IAAI,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,CACtB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;IACtF,KAAK,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,CACvB,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;CACzF,CAAC;AAEF,qEAAqE;AACrE,MAAM,UAAU,QAAQ,CAAC,OAAe,EAAE,OAAgC,EAAE;IAC1E,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;AACtE,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { Notice } from './types.js';
2
+ export interface NormalizeContext {
3
+ app: string;
4
+ env: string;
5
+ now: () => Date;
6
+ }
7
+ /** ログ 1 行 → Notice(通知対象でなければ undefined) */
8
+ export declare function noticeFromLogLine(line: string, ctx: NormalizeContext, at?: number): Notice | undefined;
9
+ /** どの入力かを判別して Notice[] にする。未知の入力は空配列 */
10
+ export declare function normalize(event: unknown, ctx: NormalizeContext): Notice[];
11
+ //# sourceMappingURL=normalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../src/notify/normalize.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,YAAY,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AA+HD,2CAA2C;AAC3C,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,gBAAgB,EACrB,EAAE,CAAC,EAAE,MAAM,GACV,MAAM,GAAG,SAAS,CAiEpB;AAyED,wCAAwC;AACxC,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,gBAAgB,GAAG,MAAM,EAAE,CAUzE"}