@mulmoclaude/core 4.8.0 → 4.9.1
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/assets/helps/bug-report-faq.md +12 -0
- package/assets/helps/custom-view.md +1 -1
- package/assets/helps/error-recovery.md +234 -26
- package/assets/helps/index.md +1 -1
- package/assets/helps/sandbox.md +2 -2
- package/assets/helps/telegram.md +10 -4
- package/dist/collection/index.cjs +3 -2
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.js +3 -2
- package/dist/collection/index.js.map +1 -1
- package/dist/collection/registry/server/index.cjs +3 -3
- package/dist/collection/registry/server/index.js +3 -3
- package/dist/collection/server/index.cjs +2 -2
- package/dist/collection/server/index.js +2 -2
- package/dist/collection-watchers/index.cjs +4 -4
- package/dist/collection-watchers/index.js +4 -4
- package/dist/{discovery-BY-nMCuh.js → discovery-BkIKvdh0.js} +3 -3
- package/dist/{discovery-BY-nMCuh.js.map → discovery-BkIKvdh0.js.map} +1 -1
- package/dist/{discovery-2TvulIz8.cjs → discovery-tvFo-bUJ.cjs} +3 -3
- package/dist/{discovery-2TvulIz8.cjs.map → discovery-tvFo-bUJ.cjs.map} +1 -1
- package/dist/{dist-D8zokgGo.js → dist-BzoA9pDR.js} +6 -1
- package/dist/dist-BzoA9pDR.js.map +1 -0
- package/dist/{dist-CsgSfWwR.cjs → dist-Gj7ygaW2.cjs} +6 -1
- package/dist/dist-Gj7ygaW2.cjs.map +1 -0
- package/dist/errorMessage-BQNpSYdT.js +1 -0
- package/dist/errorMessage-CT-va_Pv.cjs +1 -0
- package/dist/errors-C7TI_t_4.js +10 -0
- package/dist/errors-C7TI_t_4.js.map +1 -0
- package/dist/errors-CdXsfFCF.cjs +15 -0
- package/dist/errors-CdXsfFCF.cjs.map +1 -0
- package/dist/feeds/server/index.cjs +4 -4
- package/dist/feeds/server/index.js +4 -4
- package/dist/google/index.cjs +3 -2
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.js +3 -2
- package/dist/google/index.js.map +1 -1
- package/dist/{listen-UZA68Upt.cjs → listen-BXthb-9C.cjs} +2 -2
- package/dist/{listen-UZA68Upt.cjs.map → listen-BXthb-9C.cjs.map} +1 -1
- package/dist/{listen-D5av7GzB.js → listen-Dca-NoBi.js} +2 -2
- package/dist/{listen-D5av7GzB.js.map → listen-Dca-NoBi.js.map} +1 -1
- package/dist/notifier/index.cjs +1 -1
- package/dist/notifier/index.js +1 -1
- package/dist/notifier/types.d.ts +20 -11
- package/dist/{notifier-DZJj0sOh.cjs → notifier-B65Mvngb.cjs} +12 -7
- package/dist/notifier-B65Mvngb.cjs.map +1 -0
- package/dist/{notifier-DhnwR82U.js → notifier-vAp5t6wL.js} +12 -7
- package/dist/notifier-vAp5t6wL.js.map +1 -0
- package/dist/plugin-vue/index.cjs +31 -12
- package/dist/plugin-vue/index.cjs.map +1 -1
- package/dist/plugin-vue/index.js +31 -12
- package/dist/plugin-vue/index.js.map +1 -1
- package/dist/{promptSafety-Bte62OvU.js → promptSafety-BpBTh5xO.js} +2 -2
- package/dist/{promptSafety-Bte62OvU.js.map → promptSafety-BpBTh5xO.js.map} +1 -1
- package/dist/{promptSafety-Cv-SoTd4.cjs → promptSafety-caJWd1ZQ.cjs} +2 -2
- package/dist/{promptSafety-Cv-SoTd4.cjs.map → promptSafety-caJWd1ZQ.cjs.map} +1 -1
- package/dist/remote-host/index.cjs +1 -1
- package/dist/remote-host/index.js +1 -1
- package/dist/remote-host/server/index.cjs +30 -7
- package/dist/remote-host/server/index.cjs.map +1 -1
- package/dist/remote-host/server/index.js +30 -7
- package/dist/remote-host/server/index.js.map +1 -1
- package/dist/remote-host/server/sessionPersistence.d.ts +7 -2
- package/dist/{remote-host-CFnl1I-L.js → remote-host-Cjox2F2J.js} +2 -2
- package/dist/{remote-host-CFnl1I-L.js.map → remote-host-Cjox2F2J.js.map} +1 -1
- package/dist/{remote-host-EQPlccB9.cjs → remote-host-DhAHBYzN.cjs} +2 -2
- package/dist/{remote-host-EQPlccB9.cjs.map → remote-host-DhAHBYzN.cjs.map} +1 -1
- package/dist/remote-view/index.cjs +1 -1
- package/dist/remote-view/index.js +1 -1
- package/dist/scheduler/index.cjs +2 -1
- package/dist/scheduler/index.cjs.map +1 -1
- package/dist/scheduler/index.js +2 -1
- package/dist/scheduler/index.js.map +1 -1
- package/dist/{server-DXUt34Rt.cjs → server-Div1i_k5.cjs} +5 -4
- package/dist/{server-DXUt34Rt.cjs.map → server-Div1i_k5.cjs.map} +1 -1
- package/dist/{server-DyYwfNB6.js → server-Dk9x1VLG.js} +5 -4
- package/dist/{server-DyYwfNB6.js.map → server-Dk9x1VLG.js.map} +1 -1
- package/dist/utils/index.cjs +3 -10
- package/dist/utils/index.js +2 -9
- package/dist/whisper/index.cjs +2 -1
- package/dist/whisper/index.cjs.map +1 -1
- package/dist/whisper/index.js +2 -1
- package/dist/whisper/index.js.map +1 -1
- package/dist/wiki/index.cjs +1 -1
- package/dist/wiki/index.js +1 -1
- package/dist/workspace-setup/index.js +5 -0
- package/dist/workspace-setup/index.js.map +1 -1
- package/package.json +7 -7
- package/dist/dist-CsgSfWwR.cjs.map +0 -1
- package/dist/dist-D8zokgGo.js.map +0 -1
- package/dist/notifier-DZJj0sOh.cjs.map +0 -1
- package/dist/notifier-DhnwR82U.js.map +0 -1
- package/dist/utils/index.cjs.map +0 -1
- package/dist/utils/index.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_dist = require("./dist-
|
|
1
|
+
const require_dist = require("./dist-Gj7ygaW2.cjs");
|
|
2
2
|
//#region src/firestore/listen.ts
|
|
3
3
|
var TRANSIENT_LISTEN_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
4
4
|
"aborted",
|
|
@@ -42,4 +42,4 @@ Object.defineProperty(exports, "shouldGiveUpListening", {
|
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
//# sourceMappingURL=listen-
|
|
45
|
+
//# sourceMappingURL=listen-BXthb-9C.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listen-
|
|
1
|
+
{"version":3,"file":"listen-BXthb-9C.cjs","names":[],"sources":["../src/firestore/listen.ts"],"sourcesContent":["// What is true of a Firestore LISTENER, whatever it is listening to.\n//\n// A `onSnapshot` error terminates that listener and never recovers on its own,\n// so every long-lived listener in this package needs the same three answers:\n// is this error worth re-subscribing for, how long to wait before trying, and\n// when to stop trying. Those answers are about Firestore, not about the feature\n// on top of it.\n//\n// Extracted from `remote-host/server/hostRunner.ts` when the shared-collection\n// store grew a listener of its own. Kept in ONE place deliberately: the two\n// would drift, and drift here is silent — one subsystem would keep retrying a\n// revoked grant forever while the other gave up on a network blip. `hostRunner`\n// re-exports these under their original names, so its published surface (which\n// MulmoTerminal imports) is unchanged.\n\nimport { isRecord } from \"@mulmoclaude/common\";\n\n// Firestore listen errors worth re-subscribing for: network / backend blips, plus\n// `unauthenticated` — the SDK refreshes tokens on its own, so an expired one is\n// fixed by trying again, and stopping the host at the first expiry was far too\n// strong (#2633). Everything else — permission-denied and any unrecognized code —\n// is fatal: re-listening can't restore a revoked grant, and an open-ended retry on\n// an unknown code would loop forever. Retrying is bounded by LISTEN_RETRY_WINDOW_MS\n// either way, so even a doomed retry ends in an escalation rather than a spin.\nconst TRANSIENT_LISTEN_ERROR_CODES = new Set([\"aborted\", \"cancelled\", \"deadline-exceeded\", \"internal\", \"resource-exhausted\", \"unauthenticated\", \"unavailable\"]);\n\nconst listenErrorCode = (error: unknown): string => (isRecord(error) && typeof error.code === \"string\" ? error.code : \"\");\n\nexport const classifyListenerError = (error: unknown): \"transient\" | \"fatal\" =>\n TRANSIENT_LISTEN_ERROR_CODES.has(listenErrorCode(error)) ? \"transient\" : \"fatal\";\n\nconst BASE_LISTEN_RETRY_MS = 1_000;\nconst MAX_LISTEN_RETRY_MS = 30_000;\n// How long a listener may keep failing before its owner stops retrying in place\n// and escalates (re-auth, re-mount). Bounding this by a RETRY COUNT instead made\n// it ~31s of wall clock — shorter than any laptop sleep or network move, after\n// which the host never re-subscribed (#2633).\nexport const LISTEN_RETRY_WINDOW_MS = 5 * 60_000;\n\n// The outage is measured from its first failure, not from the last attempt: a\n// backoff ladder that keeps failing must not extend its own deadline.\nexport const shouldGiveUpListening = (downSinceMs: number, now: number, windowMs: number = LISTEN_RETRY_WINDOW_MS): boolean => now - downSinceMs >= windowMs;\n\n// Exponential backoff, capped: attempt 0 → 1s, 1 → 2s, … saturating at 30s.\nexport const backoffDelayMs = (attempt: number): number => Math.min(MAX_LISTEN_RETRY_MS, BASE_LISTEN_RETRY_MS * 2 ** attempt);\n"],"mappings":";;AAwBA,IAAM,+CAA+B,IAAI,IAAI;CAAC;CAAW;CAAa;CAAqB;CAAY;CAAsB;CAAmB;AAAa,CAAC;AAE9J,IAAM,mBAAmB,UAA4B,aAAA,SAAS,KAAK,KAAK,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO;AAEtH,IAAa,yBAAyB,UACpC,6BAA6B,IAAI,gBAAgB,KAAK,CAAC,IAAI,cAAc;AAE3E,IAAM,uBAAuB;AAC7B,IAAM,sBAAsB;AAK5B,IAAa,yBAAyB;AAItC,IAAa,yBAAyB,aAAqB,KAAa,WAAmB,2BAAoC,MAAM,eAAe;AAGpJ,IAAa,kBAAkB,YAA4B,KAAK,IAAI,qBAAqB,uBAAuB,KAAK,OAAO"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { s as isRecord } from "./dist-
|
|
1
|
+
import { s as isRecord } from "./dist-BzoA9pDR.js";
|
|
2
2
|
//#region src/firestore/listen.ts
|
|
3
3
|
var TRANSIENT_LISTEN_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
4
4
|
"aborted",
|
|
@@ -19,4 +19,4 @@ var backoffDelayMs = (attempt) => Math.min(MAX_LISTEN_RETRY_MS, BASE_LISTEN_RETR
|
|
|
19
19
|
//#endregion
|
|
20
20
|
export { shouldGiveUpListening as i, backoffDelayMs as n, classifyListenerError as r, LISTEN_RETRY_WINDOW_MS as t };
|
|
21
21
|
|
|
22
|
-
//# sourceMappingURL=listen-
|
|
22
|
+
//# sourceMappingURL=listen-Dca-NoBi.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listen-
|
|
1
|
+
{"version":3,"file":"listen-Dca-NoBi.js","names":[],"sources":["../src/firestore/listen.ts"],"sourcesContent":["// What is true of a Firestore LISTENER, whatever it is listening to.\n//\n// A `onSnapshot` error terminates that listener and never recovers on its own,\n// so every long-lived listener in this package needs the same three answers:\n// is this error worth re-subscribing for, how long to wait before trying, and\n// when to stop trying. Those answers are about Firestore, not about the feature\n// on top of it.\n//\n// Extracted from `remote-host/server/hostRunner.ts` when the shared-collection\n// store grew a listener of its own. Kept in ONE place deliberately: the two\n// would drift, and drift here is silent — one subsystem would keep retrying a\n// revoked grant forever while the other gave up on a network blip. `hostRunner`\n// re-exports these under their original names, so its published surface (which\n// MulmoTerminal imports) is unchanged.\n\nimport { isRecord } from \"@mulmoclaude/common\";\n\n// Firestore listen errors worth re-subscribing for: network / backend blips, plus\n// `unauthenticated` — the SDK refreshes tokens on its own, so an expired one is\n// fixed by trying again, and stopping the host at the first expiry was far too\n// strong (#2633). Everything else — permission-denied and any unrecognized code —\n// is fatal: re-listening can't restore a revoked grant, and an open-ended retry on\n// an unknown code would loop forever. Retrying is bounded by LISTEN_RETRY_WINDOW_MS\n// either way, so even a doomed retry ends in an escalation rather than a spin.\nconst TRANSIENT_LISTEN_ERROR_CODES = new Set([\"aborted\", \"cancelled\", \"deadline-exceeded\", \"internal\", \"resource-exhausted\", \"unauthenticated\", \"unavailable\"]);\n\nconst listenErrorCode = (error: unknown): string => (isRecord(error) && typeof error.code === \"string\" ? error.code : \"\");\n\nexport const classifyListenerError = (error: unknown): \"transient\" | \"fatal\" =>\n TRANSIENT_LISTEN_ERROR_CODES.has(listenErrorCode(error)) ? \"transient\" : \"fatal\";\n\nconst BASE_LISTEN_RETRY_MS = 1_000;\nconst MAX_LISTEN_RETRY_MS = 30_000;\n// How long a listener may keep failing before its owner stops retrying in place\n// and escalates (re-auth, re-mount). Bounding this by a RETRY COUNT instead made\n// it ~31s of wall clock — shorter than any laptop sleep or network move, after\n// which the host never re-subscribed (#2633).\nexport const LISTEN_RETRY_WINDOW_MS = 5 * 60_000;\n\n// The outage is measured from its first failure, not from the last attempt: a\n// backoff ladder that keeps failing must not extend its own deadline.\nexport const shouldGiveUpListening = (downSinceMs: number, now: number, windowMs: number = LISTEN_RETRY_WINDOW_MS): boolean => now - downSinceMs >= windowMs;\n\n// Exponential backoff, capped: attempt 0 → 1s, 1 → 2s, … saturating at 30s.\nexport const backoffDelayMs = (attempt: number): number => Math.min(MAX_LISTEN_RETRY_MS, BASE_LISTEN_RETRY_MS * 2 ** attempt);\n"],"mappings":";;AAwBA,IAAM,+CAA+B,IAAI,IAAI;CAAC;CAAW;CAAa;CAAqB;CAAY;CAAsB;CAAmB;AAAa,CAAC;AAE9J,IAAM,mBAAmB,UAA4B,SAAS,KAAK,KAAK,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO;AAEtH,IAAa,yBAAyB,UACpC,6BAA6B,IAAI,gBAAgB,KAAK,CAAC,IAAI,cAAc;AAE3E,IAAM,uBAAuB;AAC7B,IAAM,sBAAsB;AAK5B,IAAa,yBAAyB;AAItC,IAAa,yBAAyB,aAAqB,KAAa,WAAmB,2BAAoC,MAAM,eAAe;AAGpJ,IAAa,kBAAkB,YAA4B,KAAK,IAAI,qBAAqB,uBAAuB,KAAK,OAAO"}
|
package/dist/notifier/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_notifier = require("../notifier-
|
|
2
|
+
const require_notifier = require("../notifier-B65Mvngb.cjs");
|
|
3
3
|
exports.HISTORY_CAP = require_notifier.HISTORY_CAP;
|
|
4
4
|
exports.NOTIFIER_LIFECYCLES = require_notifier.NOTIFIER_LIFECYCLES;
|
|
5
5
|
exports.NOTIFIER_LIMITS = require_notifier.NOTIFIER_LIMITS;
|
package/dist/notifier/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as HISTORY_CAP, a as get, c as listFor, d as publish, f as resetNotifier, g as validatePublishInput, h as NOTIFIER_LIMITS, i as configureNotifier, l as listHistory, m as updateForPlugin, n as clear, o as getForPlugin, p as setNotifierFilePaths, r as clearForPlugin, s as listAll, t as cancel, u as onEvent, v as NOTIFIER_LIFECYCLES, y as NOTIFIER_SEVERITIES } from "../notifier-
|
|
1
|
+
import { _ as HISTORY_CAP, a as get, c as listFor, d as publish, f as resetNotifier, g as validatePublishInput, h as NOTIFIER_LIMITS, i as configureNotifier, l as listHistory, m as updateForPlugin, n as clear, o as getForPlugin, p as setNotifierFilePaths, r as clearForPlugin, s as listAll, t as cancel, u as onEvent, v as NOTIFIER_LIFECYCLES, y as NOTIFIER_SEVERITIES } from "../notifier-vAp5t6wL.js";
|
|
2
2
|
export { HISTORY_CAP, NOTIFIER_LIFECYCLES, NOTIFIER_LIMITS, NOTIFIER_SEVERITIES, cancel, clear, clearForPlugin, configureNotifier, get, getForPlugin, listAll, listFor, listHistory, onEvent, publish, resetNotifier, setNotifierFilePaths, updateForPlugin, validatePublishInput };
|
package/dist/notifier/types.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/** Two notification shapes, distinguished by who fires the close call:
|
|
2
2
|
*
|
|
3
3
|
* `fyi` — informational. The host (bell panel) clears it when the
|
|
4
|
-
* user dismisses the row.
|
|
4
|
+
* user dismisses the row. MAY carry a `navigateTarget`
|
|
5
|
+
* (the legacy wrapper publishes fyi entries with one);
|
|
6
|
+
* the row then navigates and clears.
|
|
5
7
|
* `action` — pending obligation. The plugin clears it when the
|
|
6
8
|
* underlying domain state changes (the user paid the tax,
|
|
7
9
|
* viewed the digest, etc.). The bell row navigates to
|
|
@@ -17,8 +19,9 @@
|
|
|
17
19
|
* is incoherent — fyi if it's a ping, `nudge`/`urgent` if it's a
|
|
18
20
|
* real obligation worth a landing page.
|
|
19
21
|
*
|
|
20
|
-
* Both rules
|
|
21
|
-
*
|
|
22
|
+
* Both rules live in `validate.ts`, which `publish` and
|
|
23
|
+
* `updateForPlugin` share — there is no HTTP publish path for them to
|
|
24
|
+
* be mirrored into (see `server/api/routes/notifier.ts`). */
|
|
22
25
|
export declare const NOTIFIER_LIFECYCLES: readonly ["fyi", "action"];
|
|
23
26
|
export type NotifierLifecycle = (typeof NOTIFIER_LIFECYCLES)[number];
|
|
24
27
|
/** Severity drives badge color (gray / amber / red, worst-wins) and
|
|
@@ -32,18 +35,22 @@ export interface NotifierEntry<TPluginData = unknown> {
|
|
|
32
35
|
/** Engine-assigned UUID. Generated synchronously inside `publish()`
|
|
33
36
|
* so the caller can use it before persistence completes. */
|
|
34
37
|
id: string;
|
|
35
|
-
/** Plugin namespace (e.g. `"encore"`, `"debug__system"`)
|
|
36
|
-
*
|
|
37
|
-
* and
|
|
38
|
+
/** Plugin namespace (e.g. `"encore"`, `"debug__system"`), and the
|
|
39
|
+
* key the per-plugin isolation is built on: `updateForPlugin`,
|
|
40
|
+
* `getForPlugin` and `clearForPlugin` each compare it and no-op on
|
|
41
|
+
* a mismatch, so one plugin cannot touch another's entries.
|
|
42
|
+
* `listFor` filters on it, and the UI groups by it. */
|
|
38
43
|
pluginPkg: string;
|
|
39
44
|
severity: NotifierSeverity;
|
|
40
45
|
lifecycle?: NotifierLifecycle;
|
|
41
46
|
title: string;
|
|
42
47
|
body?: string;
|
|
43
48
|
/** Optional in-app deep-link target (relative URL). The bell popup
|
|
44
|
-
* routes here on row click,
|
|
45
|
-
*
|
|
46
|
-
*
|
|
49
|
+
* routes here on row click, splicing in `notificationId=<id>` so the
|
|
50
|
+
* landing page can identify which entry to clear — with `?` or `&`
|
|
51
|
+
* as the target requires, and before any `#fragment`
|
|
52
|
+
* (`appendNotificationId` in `NotificationBell.vue`). The engine
|
|
53
|
+
* doesn't read this — it's a UI hint stored on the entry. */
|
|
47
54
|
navigateTarget?: string;
|
|
48
55
|
/** Opaque to the engine. Round-trips through JSON unchanged; only
|
|
49
56
|
* the originating plugin's UI knows the shape. */
|
|
@@ -92,8 +99,10 @@ export interface NotifierFile {
|
|
|
92
99
|
export interface NotifierHistoryFile {
|
|
93
100
|
entries: NotifierHistoryEntry[];
|
|
94
101
|
}
|
|
95
|
-
/** History size cap
|
|
96
|
-
* many
|
|
102
|
+
/** History size cap — how many terminated entries the FILE keeps, not
|
|
103
|
+
* how many the UI shows: older ones fall off when new terminations
|
|
104
|
+
* land. The bell popup renders `HISTORY_INITIAL_VISIBLE` of them and
|
|
105
|
+
* reveals the rest behind "Show more" (`NotificationBell.vue`). */
|
|
97
106
|
export declare const HISTORY_CAP = 50;
|
|
98
107
|
/** Pub-sub event published on the host's notifier channel after every
|
|
99
108
|
* successful state change. Discriminated union — subscribers switch
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
const require_dist = require("./dist-
|
|
1
|
+
const require_dist = require("./dist-Gj7ygaW2.cjs");
|
|
2
2
|
let node_fs = require("node:fs");
|
|
3
3
|
let node_crypto = require("node:crypto");
|
|
4
4
|
//#region src/notifier/types.ts
|
|
5
5
|
/** Two notification shapes, distinguished by who fires the close call:
|
|
6
6
|
*
|
|
7
7
|
* `fyi` — informational. The host (bell panel) clears it when the
|
|
8
|
-
* user dismisses the row.
|
|
8
|
+
* user dismisses the row. MAY carry a `navigateTarget`
|
|
9
|
+
* (the legacy wrapper publishes fyi entries with one);
|
|
10
|
+
* the row then navigates and clears.
|
|
9
11
|
* `action` — pending obligation. The plugin clears it when the
|
|
10
12
|
* underlying domain state changes (the user paid the tax,
|
|
11
13
|
* viewed the digest, etc.). The bell row navigates to
|
|
@@ -21,8 +23,9 @@ let node_crypto = require("node:crypto");
|
|
|
21
23
|
* is incoherent — fyi if it's a ping, `nudge`/`urgent` if it's a
|
|
22
24
|
* real obligation worth a landing page.
|
|
23
25
|
*
|
|
24
|
-
* Both rules
|
|
25
|
-
*
|
|
26
|
+
* Both rules live in `validate.ts`, which `publish` and
|
|
27
|
+
* `updateForPlugin` share — there is no HTTP publish path for them to
|
|
28
|
+
* be mirrored into (see `server/api/routes/notifier.ts`). */
|
|
26
29
|
var NOTIFIER_LIFECYCLES = ["fyi", "action"];
|
|
27
30
|
/** Severity drives badge color (gray / amber / red, worst-wins) and
|
|
28
31
|
* in a future iteration channel routing. Mostly stored verbatim by
|
|
@@ -34,8 +37,10 @@ var NOTIFIER_SEVERITIES = [
|
|
|
34
37
|
"nudge",
|
|
35
38
|
"urgent"
|
|
36
39
|
];
|
|
37
|
-
/** History size cap
|
|
38
|
-
* many
|
|
40
|
+
/** History size cap — how many terminated entries the FILE keeps, not
|
|
41
|
+
* how many the UI shows: older ones fall off when new terminations
|
|
42
|
+
* land. The bell popup renders `HISTORY_INITIAL_VISIBLE` of them and
|
|
43
|
+
* reveals the rest behind "Show more" (`NotificationBell.vue`). */
|
|
39
44
|
var HISTORY_CAP = 50;
|
|
40
45
|
//#endregion
|
|
41
46
|
//#region src/notifier/store.ts
|
|
@@ -597,4 +602,4 @@ Object.defineProperty(exports, "validatePublishInput", {
|
|
|
597
602
|
}
|
|
598
603
|
});
|
|
599
604
|
|
|
600
|
-
//# sourceMappingURL=notifier-
|
|
605
|
+
//# sourceMappingURL=notifier-B65Mvngb.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifier-B65Mvngb.cjs","names":[],"sources":["../src/notifier/types.ts","../src/notifier/store.ts","../src/notifier/validate.ts","../src/notifier/engine.ts"],"sourcesContent":["// Notifier value types. Kept dependency-free (no node, no fs, no\n// pubsub) so the host's API-route layer and any future browser\n// consumer can validate inbound payloads against the same enum\n// constants the engine accepts without pulling in the engine's I/O.\n\n/** Two notification shapes, distinguished by who fires the close call:\n *\n * `fyi` — informational. The host (bell panel) clears it when the\n * user dismisses the row. MAY carry a `navigateTarget`\n * (the legacy wrapper publishes fyi entries with one);\n * the row then navigates and clears.\n * `action` — pending obligation. The plugin clears it when the\n * underlying domain state changes (the user paid the tax,\n * viewed the digest, etc.). The bell row navigates to\n * `navigateTarget` on click.\n *\n * The engine reads `lifecycle` only to enforce two publish-time rules\n * (everything downstream — pubsub fan-out, persistence, history — is\n * lifecycle-blind):\n *\n * 1. `action` requires a non-empty `navigateTarget`. Without one,\n * clicking the row does nothing and the entry is a degraded fyi.\n * 2. `action` cannot use `info` severity. A low-priority obligation\n * is incoherent — fyi if it's a ping, `nudge`/`urgent` if it's a\n * real obligation worth a landing page.\n *\n * Both rules live in `validate.ts`, which `publish` and\n * `updateForPlugin` share — there is no HTTP publish path for them to\n * be mirrored into (see `server/api/routes/notifier.ts`). */\nexport const NOTIFIER_LIFECYCLES = [\"fyi\", \"action\"] as const;\nexport type NotifierLifecycle = (typeof NOTIFIER_LIFECYCLES)[number];\n\n/** Severity drives badge color (gray / amber / red, worst-wins) and\n * in a future iteration channel routing. Mostly stored verbatim by\n * the engine; the one engine-visible interaction is the rule that\n * `action` lifecycle cannot pair with `info` severity (see\n * `NotifierLifecycle` above). */\nexport const NOTIFIER_SEVERITIES = [\"info\", \"nudge\", \"urgent\"] as const;\nexport type NotifierSeverity = (typeof NOTIFIER_SEVERITIES)[number];\n\nexport interface NotifierEntry<TPluginData = unknown> {\n /** Engine-assigned UUID. Generated synchronously inside `publish()`\n * so the caller can use it before persistence completes. */\n id: string;\n /** Plugin namespace (e.g. `\"encore\"`, `\"debug__system\"`), and the\n * key the per-plugin isolation is built on: `updateForPlugin`,\n * `getForPlugin` and `clearForPlugin` each compare it and no-op on\n * a mismatch, so one plugin cannot touch another's entries.\n * `listFor` filters on it, and the UI groups by it. */\n pluginPkg: string;\n severity: NotifierSeverity;\n lifecycle?: NotifierLifecycle;\n title: string;\n body?: string;\n /** Optional in-app deep-link target (relative URL). The bell popup\n * routes here on row click, splicing in `notificationId=<id>` so the\n * landing page can identify which entry to clear — with `?` or `&`\n * as the target requires, and before any `#fragment`\n * (`appendNotificationId` in `NotificationBell.vue`). The engine\n * doesn't read this — it's a UI hint stored on the entry. */\n navigateTarget?: string;\n /** Opaque to the engine. Round-trips through JSON unchanged; only\n * the originating plugin's UI knows the shape. */\n pluginData?: TPluginData;\n /** ISO-8601 timestamp set at `publish()` time. */\n createdAt: string;\n}\n\n/** A history entry — a `NotifierEntry` after it has been cleared or\n * cancelled, with the terminal type and timestamp recorded. The\n * bell popup's \"History\" section renders these read-only. */\nexport interface NotifierHistoryEntry<TPluginData = unknown> extends NotifierEntry<TPluginData> {\n terminalType: \"cleared\" | \"cancelled\";\n terminalAt: string;\n}\n\n/** Caller-supplied input for `publish()`. The engine fills in `id`\n * and `createdAt`; everything else flows through verbatim.\n *\n * Two publish-time rules apply to `action` lifecycle (see\n * `NotifierLifecycle`):\n *\n * - `navigateTarget` MUST be a non-empty string.\n * - `severity` MUST NOT be `\"info\"`.\n *\n * Violations cause `publish()` to throw. Currently expressed as\n * runtime validation rather than a discriminated-union type, so the\n * fields below are all individually optional / loose at the\n * type-level. */\nexport interface PublishInput<TPluginData = unknown> {\n pluginPkg: string;\n severity: NotifierSeverity;\n title: string;\n body?: string | undefined;\n lifecycle?: NotifierLifecycle | undefined;\n navigateTarget?: string | undefined;\n pluginData?: TPluginData | undefined;\n}\n\n/** On-disk shape of `~/mulmoclaude/data/notifier/active.json`. Holds\n * only entries that haven't been cleared or cancelled — the file is\n * a snapshot, not an event log. */\nexport interface NotifierFile {\n entries: Record<string, NotifierEntry>;\n}\n\n/** On-disk shape of `~/mulmoclaude/data/notifier/history.json`. Array\n * of terminated entries newest-first, capped at `HISTORY_CAP` with\n * FIFO eviction (push at index 0, slice from the tail). */\nexport interface NotifierHistoryFile {\n entries: NotifierHistoryEntry[];\n}\n\n/** History size cap — how many terminated entries the FILE keeps, not\n * how many the UI shows: older ones fall off when new terminations\n * land. The bell popup renders `HISTORY_INITIAL_VISIBLE` of them and\n * reveals the rest behind \"Show more\" (`NotificationBell.vue`). */\nexport const HISTORY_CAP = 50;\n\n/** Pub-sub event published on the host's notifier channel after every\n * successful state change. Discriminated union — subscribers switch\n * on `type` to keep TypeScript narrowing the rest of the payload.\n *\n * `updated` carries the post-mutation entry — the receiver swaps\n * the matching `id` in their local active set. Reserved for in-\n * place edits via `updateForPlugin`; no history record is written\n * because the entry is still active, just with refreshed content. */\nexport type NotifierEvent =\n { type: \"published\"; entry: NotifierEntry } | { type: \"cleared\"; id: string } | { type: \"cancelled\"; id: string } | { type: \"updated\"; entry: NotifierEntry };\n","// Low-level file I/O for the notifier. Reads use node:fs directly;\n// writes go through an injected atomic-JSON writer (the host owns the\n// rename-based atomic write so it stays single-sourced with its other\n// writers). Kept separate from `engine.ts` so the path can be\n// overridden in tests without monkey-patching.\n\nimport { hasStringProp, isErrorWithCode, isRecord, isUnknownArray } from \"@mulmoclaude/common\";\nimport { promises as fsPromises } from \"node:fs\";\nimport {\n NOTIFIER_LIFECYCLES,\n NOTIFIER_SEVERITIES,\n type NotifierEntry,\n type NotifierFile,\n type NotifierHistoryEntry,\n type NotifierHistoryFile,\n} from \"./types.js\";\n\n/** Injected atomic JSON writer — the host's `writeJsonAtomic`. */\nexport type WriteJson = (filePath: string, data: unknown) => Promise<void>;\n\nconst TERMINAL_TYPES = [\"cleared\", \"cancelled\"] as const;\n\nfunction isOneOf<T extends string>(allowed: readonly T[], value: unknown): value is T {\n return allowed.some((candidate) => candidate === value);\n}\n\nfunction isOptionalString(value: unknown): boolean {\n return value === undefined || typeof value === \"string\";\n}\n\n// Checks every field `NotifierEntry` declares — `pluginData` is `unknown`,\n// so nothing is left unverified. A partial check would be an assertion\n// wearing a predicate's clothes.\nfunction isNotifierEntry(value: unknown): value is NotifierEntry {\n return (\n hasStringProp(value, \"id\") &&\n hasStringProp(value, \"pluginPkg\") &&\n hasStringProp(value, \"title\") &&\n hasStringProp(value, \"createdAt\") &&\n isOneOf(NOTIFIER_SEVERITIES, value.severity) &&\n (value.lifecycle === undefined || isOneOf(NOTIFIER_LIFECYCLES, value.lifecycle)) &&\n isOptionalString(value.body) &&\n isOptionalString(value.navigateTarget)\n );\n}\n\nfunction isNotifierHistoryEntry(value: unknown): value is NotifierHistoryEntry {\n return isNotifierEntry(value) && hasStringProp(value, \"terminalAt\") && isOneOf(TERMINAL_TYPES, value.terminalType);\n}\n\n// `isRecord` rejects `null` and arrays, so `{ entries: [] }` and\n// `{ entries: null }` stay malformed instead of reading as \"no entries\" —\n// downstream `engine.get` / `list*` mutations assume a plain object.\nfunction isNotifierFile(value: unknown): value is NotifierFile {\n return isRecord(value) && isRecord(value.entries) && Object.values(value.entries).every(isNotifierEntry);\n}\n\nfunction isNotifierHistoryFile(value: unknown): value is NotifierHistoryFile {\n return isRecord(value) && isUnknownArray(value.entries) && value.entries.every(isNotifierHistoryEntry);\n}\n\n/** Parsed JSON, or `undefined` when the file doesn't exist yet (first\n * ever call on a fresh workspace) — unambiguous as a sentinel because\n * no JSON document parses to `undefined`. */\nasync function readJsonIfPresent(filePath: string): Promise<unknown> {\n try {\n return JSON.parse(await fsPromises.readFile(filePath, \"utf-8\"));\n } catch (err) {\n if (isErrorWithCode(err) && err.code === \"ENOENT\") return undefined;\n throw err;\n }\n}\n\n/** Read the active-entries file. Returns an empty store when the file\n * doesn't exist yet. Any other read or parse failure throws — the\n * caller has to decide whether to surface or recover, since silently\n * treating \"malformed file\" as \"no entries\" would lose data. A rejected\n * file is never rewritten, so it stays on disk intact for repair. */\nexport async function loadActive(filePath: string): Promise<NotifierFile> {\n const parsed = await readJsonIfPresent(filePath);\n if (parsed === undefined) return { entries: {} };\n if (!isNotifierFile(parsed)) throw new Error(`notifier: malformed active.json at ${filePath}`);\n return parsed;\n}\n\n/** Write the active-entries file via the injected atomic writer so a\n * half-written file is never visible to readers. The caller serialises\n * writes (engine.ts queues mutations) — this function makes no\n * concurrency guarantees of its own. */\nexport async function saveActive(writeJson: WriteJson, filePath: string, state: NotifierFile): Promise<void> {\n await writeJson(filePath, state);\n}\n\n/** Read the history file. Empty array on first run. Same parse-error\n * policy as `loadActive`. */\nexport async function loadHistory(filePath: string): Promise<NotifierHistoryFile> {\n const parsed = await readJsonIfPresent(filePath);\n if (parsed === undefined) return { entries: [] };\n if (!isNotifierHistoryFile(parsed)) throw new Error(`notifier: malformed history.json at ${filePath}`);\n return parsed;\n}\n\nexport async function saveHistory(writeJson: WriteJson, filePath: string, state: NotifierHistoryFile): Promise<void> {\n await writeJson(filePath, state);\n}\n","// Publish-input validation — pure, dependency-free, so every caller\n// can share one rule set. Today that is `engine.publish`, which throws,\n// and `updateForPlugin`, which re-validates the MERGED entry and skips\n// the update with a warning rather than throwing — an update must not\n// be able to degrade an entry below publish-time invariants, but it is\n// also not the caller's error to handle. Publish is deliberately absent\n// from the HTTP surface (see `server/api/routes/notifier.ts`), so the\n// route layer this comment once also named no longer exists.\n\nimport type { PublishInput } from \"./types.js\";\n\n/** Hard caps on publish-input fields. The engine reads each entry on\n * every list/get call (no in-memory cache), so unbounded fields hurt\n * every reader. Caps chosen to be generous for legitimate UX copy\n * while bounding active.json growth: a notification fundamentally is\n * a short blurb, not a document. */\nexport const NOTIFIER_LIMITS = {\n titleMax: 200,\n bodyMax: 4000,\n navigateTargetMax: 1000,\n pluginDataMaxBytes: 16 * 1024,\n} as const;\n\nfunction validateTitle(title: string): string | null {\n if (typeof title !== \"string\" || title.length === 0) return \"title must be a non-empty string\";\n if (title.length > NOTIFIER_LIMITS.titleMax) return `title exceeds max length of ${NOTIFIER_LIMITS.titleMax} chars`;\n return null;\n}\n\nfunction validateBody(body: string | undefined): string | null {\n if (body === undefined) return null;\n if (body.length > NOTIFIER_LIMITS.bodyMax) return `body exceeds max length of ${NOTIFIER_LIMITS.bodyMax} chars`;\n return null;\n}\n\nfunction validateNavigateTarget(target: string | undefined): string | null {\n if (target === undefined) return null;\n if (target.length === 0) return \"navigateTarget must be a non-empty relative path when set\";\n if (target.length > NOTIFIER_LIMITS.navigateTargetMax) {\n return `navigateTarget exceeds max length of ${NOTIFIER_LIMITS.navigateTargetMax} chars`;\n }\n // Must be a same-origin relative path. Reject schemes\n // (`javascript:`, `https://...`) and scheme-relative URLs\n // (`//evil.com/...`, which an `<a href>` would resolve to the\n // attacker's origin). One leading \"/\" only.\n if (!target.startsWith(\"/\") || target.startsWith(\"//\")) {\n return \"navigateTarget must be a relative path beginning with a single '/' (no scheme, no '//')\";\n }\n return null;\n}\n\nfunction validatePluginData(pluginData: unknown): string | null {\n if (pluginData === undefined) return null;\n let serialized: string | undefined;\n try {\n serialized = JSON.stringify(pluginData);\n } catch (err) {\n return `pluginData is not JSON-serialisable: ${String(err)}`;\n }\n // `JSON.stringify` returns `undefined` for non-serialisable roots\n // (e.g. a bare function or symbol). Treat that as a serialisation\n // failure so it doesn't slip through as an empty-string size.\n if (typeof serialized !== \"string\") return \"pluginData is not JSON-serialisable\";\n if (serialized.length > NOTIFIER_LIMITS.pluginDataMaxBytes) {\n return `pluginData JSON exceeds ${NOTIFIER_LIMITS.pluginDataMaxBytes} bytes`;\n }\n return null;\n}\n\nfunction validateActionCoherence(input: PublishInput): string | null {\n if (input.lifecycle !== \"action\") return null;\n if (input.severity === \"info\") {\n return \"action lifecycle is incompatible with info severity (use fyi for low-priority pings)\";\n }\n if (typeof input.navigateTarget !== \"string\" || input.navigateTarget.length === 0) {\n return \"action lifecycle requires a non-empty navigateTarget\";\n }\n return null;\n}\n\n/** Validate a `PublishInput`. Returns `null` if OK, or a\n * human-readable error string. Order matters — shape/size errors are\n * reported before lifecycle/severity coherence errors so the message\n * the caller sees points at the most fundamental problem first. */\nexport function validatePublishInput(input: PublishInput): string | null {\n return (\n validateTitle(input.title) ??\n validateBody(input.body) ??\n validateNavigateTarget(input.navigateTarget) ??\n validatePluginData(input.pluginData) ??\n validateActionCoherence(input)\n );\n}\n","// Notifier engine — single-process, two-file (active + history),\n// single-channel. Host-agnostic: file paths, the atomic JSON writer,\n// the pub-sub event sink, and the logger are all injected via\n// `configureNotifier` + `setNotifierFilePaths` so MulmoClaude and\n// MulmoTerminal share one notification engine over their own\n// workspaces and pub-sub fabrics.\n//\n// API surface: publish / clear / cancel / get / listFor / listAll /\n// listHistory (+ plugin-scoped variants). Mutations queue through a\n// writing-flag + waiter-queue coordinator so concurrent callers can't\n// race on the atomic write's rename. Reads bypass the queue (rename\n// atomicity makes half-reads impossible) and trade strict\n// linearisability for simpler code: the contract is \"after\n// `await publish(x)` resolves, subsequent reads see x\" — which holds\n// because `publish` awaits the persist before returning.\n//\n// `clear` / `cancel` push to history *before* removing from active.\n// History persistence is best-effort: if it fails, the active write\n// still wins and the failure is logged. Active is the source of\n// truth; history is an audit aid.\n\nimport type { MinimalLogger } from \"@mulmoclaude/common\";\nimport { randomUUID } from \"node:crypto\";\nimport { loadActive, loadHistory, saveActive, saveHistory, type WriteJson } from \"./store.js\";\nimport { validatePublishInput } from \"./validate.js\";\nimport {\n HISTORY_CAP,\n type NotifierEntry,\n type NotifierEvent,\n type NotifierFile,\n type NotifierHistoryEntry,\n type NotifierSeverity,\n type PublishInput,\n} from \"./types.js\";\n\nexport { NOTIFIER_LIMITS, validatePublishInput } from \"./validate.js\";\n\n// ── Dependency injection ──────────────────────────────────────────\n\n/** Minimal logger the engine needs. The host passes its structured\n * logger; absent one, failures are swallowed (the engine never throws\n * on a fan-out/persist-best-effort path). */\nexport type NotifierLogger = Pick<MinimalLogger, \"warn\" | \"error\">;\n\nexport interface NotifierConfig {\n /** Atomic JSON writer (the host's `writeJsonAtomic`). */\n writeJson: WriteJson;\n /** Fan-out sink — the host binds this to `pubsub.publish(channel, event)`. */\n publishEvent: (event: NotifierEvent) => void;\n /** Optional logger. */\n log?: NotifierLogger;\n}\n\nconst NOOP_LOG: NotifierLogger = { warn: () => {}, error: () => {} };\n\nlet config: NotifierConfig | null = null;\nlet activeFilePath = \"\";\nlet historyFilePath = \"\";\n\nfunction logger(): NotifierLogger {\n return config?.log ?? NOOP_LOG;\n}\n\n/** Wire the engine's I/O deps. Call once at startup, before the first\n * mutation. Does NOT set file paths — those are set independently via\n * `setNotifierFilePaths` so a host can bind production paths at module\n * load and a test can override them without re-supplying the deps. */\nexport function configureNotifier(injected: NotifierConfig): void {\n config = injected;\n}\n\n// ── In-process event listeners ────────────────────────────────────\n//\n// Separate from the socket.io pubsub so server-side adapters (macOS\n// push, future Encore) can react to state changes without going\n// through a websocket round-trip. The host's pubsub is fan-out-only\n// with no server-side subscribe, so this listener registry is the\n// in-process equivalent. Listeners run synchronously inside `emit`,\n// before the pubsub fan-out.\n\ntype NotifierEventListener = (event: NotifierEvent) => void;\nconst listeners: NotifierEventListener[] = [];\n\n/** Register an in-process listener for engine events. Returns an\n * unsubscribe function the caller can use during teardown. */\nexport function onEvent(listener: NotifierEventListener): () => void {\n listeners.push(listener);\n return () => {\n const idx = listeners.indexOf(listener);\n if (idx >= 0) listeners.splice(idx, 1);\n };\n}\n\nfunction emit(event: NotifierEvent): void {\n // In-process fan-out first. Each listener is wrapped: a throwing\n // listener must not poison the rest, and must not propagate out of\n // `processBatch` and strand the still-unsettled waiters (their\n // resolve/reject is called *after* this emit loop). Fan-out is\n // best-effort by contract — losing one subscriber must not lose\n // the write that already committed.\n for (const listener of listeners) {\n try {\n listener(event);\n } catch (err) {\n logger().error(\"in-process listener failed\", { type: event.type, error: String(err) });\n }\n }\n if (!config) {\n logger().warn(\"emit before init\", { type: event.type });\n return;\n }\n try {\n config.publishEvent(event);\n } catch (err) {\n logger().error(\"emit failed\", { type: event.type, error: String(err) });\n }\n}\n\n// ── Write coordinator ─────────────────────────────────────────────\n\n/** A mutation function applied to the in-memory state object during\n * drain. Returns either:\n *\n * - `null` — no state change (e.g., `clear` on an unknown id).\n * The drainer skips the disk write and the emit if every\n * mutation in a batch returned `null`.\n * - `{ event, historyEntry? }` — state changed. The drainer emits\n * the event after the active write succeeds, and prepends\n * `historyEntry` to history (best-effort) when present.\n *\n * Mutations MUST NOT modify state when returning `null`. Violating\n * this invariant produces a write skip with stale on-disk state. */\ntype MutationOutcome = { event: NotifierEvent; historyEntry?: NotifierHistoryEntry } | null;\ntype Mutation = (state: NotifierFile) => MutationOutcome;\n\ninterface Waiter {\n mutate: Mutation;\n resolve: () => void;\n reject: (err: unknown) => void;\n}\n\ntype MutationResult = { ok: true; outcome: MutationOutcome } | { ok: false; error: unknown };\n\nlet writing = false;\nlet waiters: Waiter[] = [];\n\n/** Point the engine at its active/history files. Resets the write\n * queue, so callers must not have in-flight mutations. The host calls\n * this once with the workspace paths; tests call it per-case with temp\n * files. */\nexport function setNotifierFilePaths(paths: { active: string; history: string }): void {\n activeFilePath = paths.active;\n historyFilePath = paths.history;\n writing = false;\n waiters = [];\n}\n\n/** Test-only: clear config + queue so each suite starts clean. */\nexport function resetNotifier(): void {\n config = null;\n activeFilePath = \"\";\n historyFilePath = \"\";\n writing = false;\n waiters = [];\n listeners.length = 0;\n}\n\nfunction requireWriteJson(): WriteJson {\n if (!config) throw new Error(\"notifier: configureNotifier() not called\");\n return config.writeJson;\n}\n\n/** A waiter carried alongside the outcome of its own mutation, so settling can\n * never pair a result with the wrong caller's promise. */\ninterface SettledMutation {\n waiter: Waiter;\n result: MutationResult;\n}\n\nfunction applyBatchMutations(batch: Waiter[], state: NotifierFile): SettledMutation[] {\n return batch.map((waiter) => {\n try {\n return { waiter, result: { ok: true, outcome: waiter.mutate(state) } };\n } catch (err) {\n return { waiter, result: { ok: false, error: err } };\n }\n });\n}\n\nfunction collectEvents(settled: SettledMutation[]): NotifierEvent[] {\n const events: NotifierEvent[] = [];\n for (const { result } of settled) {\n if (result.ok && result.outcome !== null) events.push(result.outcome.event);\n }\n return events;\n}\n\nfunction collectHistoryEntries(settled: SettledMutation[]): NotifierHistoryEntry[] {\n const entries: NotifierHistoryEntry[] = [];\n for (const { result } of settled) {\n if (result.ok && result.outcome !== null && result.outcome.historyEntry) {\n entries.push(result.outcome.historyEntry);\n }\n }\n return entries;\n}\n\nfunction settleBatch(settled: SettledMutation[]): void {\n // Resolves come AFTER any emits so subscribers see the event\n // before the caller's `await` returns.\n for (const { waiter, result } of settled) {\n if (result.ok) waiter.resolve();\n else waiter.reject(result.error);\n }\n}\n\nfunction rejectBatch(batch: Waiter[], err: unknown): void {\n for (const waiter of batch) waiter.reject(err);\n}\n\nasync function persistHistory(newEntries: NotifierHistoryEntry[]): Promise<void> {\n const existing = await loadHistory(historyFilePath);\n // Newest-first ordering: a batch contains terminations in arrival\n // order; we want the last one to land at index 0 of history.\n const merged = [...newEntries.slice().reverse(), ...existing.entries].slice(0, HISTORY_CAP);\n await saveHistory(requireWriteJson(), historyFilePath, { entries: merged });\n}\n\nasync function processBatch(batch: Waiter[]): Promise<void> {\n let state: NotifierFile;\n try {\n state = await loadActive(activeFilePath);\n } catch (err) {\n logger().error(\"load failed\", { error: String(err) });\n rejectBatch(batch, err);\n return;\n }\n const settled = applyBatchMutations(batch, state);\n const events = collectEvents(settled);\n const historyEntries = collectHistoryEntries(settled);\n\n if (events.length > 0) {\n try {\n await saveActive(requireWriteJson(), activeFilePath, state);\n } catch (err) {\n logger().error(\"active write failed\", { error: String(err) });\n rejectBatch(batch, err);\n return;\n }\n if (historyEntries.length > 0) {\n // Best-effort: active is the source of truth, history is an\n // audit aid. A failed history write is logged but doesn't\n // unwind the active commit.\n try {\n await persistHistory(historyEntries);\n } catch (err) {\n logger().error(\"history write failed\", { error: String(err) });\n }\n }\n for (const event of events) emit(event);\n }\n settleBatch(settled);\n}\n\nasync function drain(): Promise<void> {\n writing = true;\n try {\n while (waiters.length > 0) {\n const batch = waiters;\n waiters = [];\n await processBatch(batch);\n }\n } finally {\n writing = false;\n }\n}\n\nfunction enqueue(mutate: Mutation): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n waiters.push({ mutate, resolve, reject });\n if (!writing) void drain();\n });\n}\n\nfunction removeEntry(state: NotifierFile, entryId: string): NotifierFile[\"entries\"] {\n // Object-rest excludes the key without invoking `delete`.\n const { [entryId]: __removed, ...remaining } = state.entries;\n return remaining;\n}\n\nfunction buildHistoryEntry(entry: NotifierEntry, terminalType: \"cleared\" | \"cancelled\"): NotifierHistoryEntry {\n return { ...entry, terminalType, terminalAt: new Date().toISOString() };\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\n/** The optional half of a publish input, with unset keys left OUT rather than\n * written as `undefined` — an entry is persisted and re-read through JSON,\n * where an unset field carries no key at all. */\nfunction optionalPublishFields<TPluginData>(input: PublishInput<TPluginData>): Partial<NotifierEntry<TPluginData>> {\n return {\n ...(input.lifecycle !== undefined ? { lifecycle: input.lifecycle } : {}),\n ...(input.body !== undefined ? { body: input.body } : {}),\n ...(input.navigateTarget !== undefined ? { navigateTarget: input.navigateTarget } : {}),\n ...(input.pluginData !== undefined ? { pluginData: input.pluginData } : {}),\n };\n}\n\nexport async function publish<TPluginData = unknown>(input: PublishInput<TPluginData>): Promise<{ id: string }> {\n // Validate at the engine boundary, so every caller shares one wall —\n // plugin runtime, legacy wrapper, host module. Publish has no HTTP\n // surface to validate separately (`server/api/routes/notifier.ts`).\n const validationError = validatePublishInput(input);\n if (validationError) {\n throw new Error(`notifier.publish: ${validationError}`);\n }\n const entryId = randomUUID();\n const entry: NotifierEntry<TPluginData> = {\n id: entryId,\n pluginPkg: input.pluginPkg,\n severity: input.severity,\n title: input.title,\n createdAt: new Date().toISOString(),\n ...optionalPublishFields(input),\n };\n await enqueue((state) => {\n state.entries[entryId] = entry;\n return { event: { type: \"published\", entry } };\n });\n return { id: entryId };\n}\n\n/** Remove an active entry and record its terminal history. `clear`\n * (user dismissed) and `cancel` (producer withdrew) differ only in the\n * emitted event / history reason. No-op when the id is unknown. */\nasync function terminateEntry(entryId: string, reason: \"cleared\" | \"cancelled\"): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: reason === \"cleared\" ? { type: \"cleared\", id: entryId } : { type: \"cancelled\", id: entryId },\n historyEntry: buildHistoryEntry(entry, reason),\n };\n });\n}\n\nexport async function clear(entryId: string): Promise<void> {\n await terminateEntry(entryId, \"cleared\");\n}\n\nexport async function cancel(entryId: string): Promise<void> {\n await terminateEntry(entryId, \"cancelled\");\n}\n\n/** In-place update for an active entry. Only the fields present on\n * `patch` are rewritten; `id`, `pluginPkg`, `lifecycle`, and\n * `createdAt` stay fixed. Emits a single `\"updated\"` event with the\n * post-mutation entry — no history record is written because the\n * entry is still active, just with refreshed content.\n *\n * No-ops (no throw) when the id is unknown, the entry belongs to a\n * different plugin, or the merged shape would violate\n * `validatePublishInput`. The silent skip matches `clearForPlugin`'s\n * isolation semantics; validation failures are logged for diagnosis. */\nexport async function updateForPlugin<TPluginData = unknown>(\n pluginPkg: string,\n entryId: string,\n patch: {\n severity?: NotifierSeverity;\n title?: string;\n body?: string;\n navigateTarget?: string;\n pluginData?: TPluginData;\n },\n): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n if (entry.pluginPkg !== pluginPkg) return null;\n const next: NotifierEntry = {\n ...entry,\n ...(patch.severity !== undefined ? { severity: patch.severity } : {}),\n ...(patch.title !== undefined ? { title: patch.title } : {}),\n ...(patch.body !== undefined ? { body: patch.body } : {}),\n ...(patch.navigateTarget !== undefined ? { navigateTarget: patch.navigateTarget } : {}),\n ...(patch.pluginData !== undefined ? { pluginData: patch.pluginData } : {}),\n };\n // Re-validate the merged shape so an update can't degrade the\n // entry below publish-time invariants.\n const validationError = validatePublishInput({\n pluginPkg: next.pluginPkg,\n severity: next.severity,\n title: next.title,\n body: next.body,\n lifecycle: next.lifecycle,\n navigateTarget: next.navigateTarget,\n pluginData: next.pluginData,\n });\n if (validationError) {\n logger().warn(\"update rejected by validation\", { entryId, pluginPkg, error: validationError });\n return null;\n }\n state.entries[entryId] = next;\n return { event: { type: \"updated\", entry: next } };\n });\n}\n\n/** Plugin-scoped point lookup. Returns the entry by id, but only if it\n * belongs to the caller's plugin; otherwise undefined. Cross-plugin\n * reads return undefined for isolation — same property as\n * `clearForPlugin` / `updateForPlugin`. */\nexport async function getForPlugin(pluginPkg: string, entryId: string): Promise<NotifierEntry | undefined> {\n const state = await loadActive(activeFilePath);\n const entry = state.entries[entryId];\n if (!entry) return undefined;\n if (entry.pluginPkg !== pluginPkg) return undefined;\n return entry;\n}\n\n/** Plugin-scoped clear. Same as `clear` but no-ops if the entry's\n * `pluginPkg` doesn't match the caller's, so a plugin can't dismiss\n * another plugin's notification by guessing or scraping its id. */\nexport async function clearForPlugin(pluginPkg: string, entryId: string): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n if (entry.pluginPkg !== pluginPkg) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: { type: \"cleared\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cleared\"),\n };\n });\n}\n\nexport async function get(entryId: string): Promise<NotifierEntry | undefined> {\n const state = await loadActive(activeFilePath);\n return state.entries[entryId];\n}\n\nexport async function listFor(pluginPkg: string): Promise<NotifierEntry[]> {\n const state = await loadActive(activeFilePath);\n return Object.values(state.entries).filter((entry) => entry.pluginPkg === pluginPkg);\n}\n\nexport async function listAll(): Promise<NotifierEntry[]> {\n const state = await loadActive(activeFilePath);\n return Object.values(state.entries);\n}\n\nexport async function listHistory(): Promise<NotifierHistoryEntry[]> {\n const state = await loadHistory(historyFilePath);\n return state.entries;\n}\n"],"mappings":"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"}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { a as isErrorWithCode, i as hasStringProp, l as isUnknownArray, s as isRecord } from "./dist-
|
|
1
|
+
import { a as isErrorWithCode, i as hasStringProp, l as isUnknownArray, s as isRecord } from "./dist-BzoA9pDR.js";
|
|
2
2
|
import { promises } from "node:fs";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
//#region src/notifier/types.ts
|
|
5
5
|
/** Two notification shapes, distinguished by who fires the close call:
|
|
6
6
|
*
|
|
7
7
|
* `fyi` — informational. The host (bell panel) clears it when the
|
|
8
|
-
* user dismisses the row.
|
|
8
|
+
* user dismisses the row. MAY carry a `navigateTarget`
|
|
9
|
+
* (the legacy wrapper publishes fyi entries with one);
|
|
10
|
+
* the row then navigates and clears.
|
|
9
11
|
* `action` — pending obligation. The plugin clears it when the
|
|
10
12
|
* underlying domain state changes (the user paid the tax,
|
|
11
13
|
* viewed the digest, etc.). The bell row navigates to
|
|
@@ -21,8 +23,9 @@ import { randomUUID } from "node:crypto";
|
|
|
21
23
|
* is incoherent — fyi if it's a ping, `nudge`/`urgent` if it's a
|
|
22
24
|
* real obligation worth a landing page.
|
|
23
25
|
*
|
|
24
|
-
* Both rules
|
|
25
|
-
*
|
|
26
|
+
* Both rules live in `validate.ts`, which `publish` and
|
|
27
|
+
* `updateForPlugin` share — there is no HTTP publish path for them to
|
|
28
|
+
* be mirrored into (see `server/api/routes/notifier.ts`). */
|
|
26
29
|
var NOTIFIER_LIFECYCLES = ["fyi", "action"];
|
|
27
30
|
/** Severity drives badge color (gray / amber / red, worst-wins) and
|
|
28
31
|
* in a future iteration channel routing. Mostly stored verbatim by
|
|
@@ -34,8 +37,10 @@ var NOTIFIER_SEVERITIES = [
|
|
|
34
37
|
"nudge",
|
|
35
38
|
"urgent"
|
|
36
39
|
];
|
|
37
|
-
/** History size cap
|
|
38
|
-
* many
|
|
40
|
+
/** History size cap — how many terminated entries the FILE keeps, not
|
|
41
|
+
* how many the UI shows: older ones fall off when new terminations
|
|
42
|
+
* land. The bell popup renders `HISTORY_INITIAL_VISIBLE` of them and
|
|
43
|
+
* reveals the rest behind "Show more" (`NotificationBell.vue`). */
|
|
39
44
|
var HISTORY_CAP = 50;
|
|
40
45
|
//#endregion
|
|
41
46
|
//#region src/notifier/store.ts
|
|
@@ -484,4 +489,4 @@ async function listHistory() {
|
|
|
484
489
|
//#endregion
|
|
485
490
|
export { HISTORY_CAP as _, get as a, listFor as c, publish as d, resetNotifier as f, validatePublishInput as g, NOTIFIER_LIMITS as h, configureNotifier as i, listHistory as l, updateForPlugin as m, clear as n, getForPlugin as o, setNotifierFilePaths as p, clearForPlugin as r, listAll as s, cancel as t, onEvent as u, NOTIFIER_LIFECYCLES as v, NOTIFIER_SEVERITIES as y };
|
|
486
491
|
|
|
487
|
-
//# sourceMappingURL=notifier-
|
|
492
|
+
//# sourceMappingURL=notifier-vAp5t6wL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifier-vAp5t6wL.js","names":[],"sources":["../src/notifier/types.ts","../src/notifier/store.ts","../src/notifier/validate.ts","../src/notifier/engine.ts"],"sourcesContent":["// Notifier value types. Kept dependency-free (no node, no fs, no\n// pubsub) so the host's API-route layer and any future browser\n// consumer can validate inbound payloads against the same enum\n// constants the engine accepts without pulling in the engine's I/O.\n\n/** Two notification shapes, distinguished by who fires the close call:\n *\n * `fyi` — informational. The host (bell panel) clears it when the\n * user dismisses the row. MAY carry a `navigateTarget`\n * (the legacy wrapper publishes fyi entries with one);\n * the row then navigates and clears.\n * `action` — pending obligation. The plugin clears it when the\n * underlying domain state changes (the user paid the tax,\n * viewed the digest, etc.). The bell row navigates to\n * `navigateTarget` on click.\n *\n * The engine reads `lifecycle` only to enforce two publish-time rules\n * (everything downstream — pubsub fan-out, persistence, history — is\n * lifecycle-blind):\n *\n * 1. `action` requires a non-empty `navigateTarget`. Without one,\n * clicking the row does nothing and the entry is a degraded fyi.\n * 2. `action` cannot use `info` severity. A low-priority obligation\n * is incoherent — fyi if it's a ping, `nudge`/`urgent` if it's a\n * real obligation worth a landing page.\n *\n * Both rules live in `validate.ts`, which `publish` and\n * `updateForPlugin` share — there is no HTTP publish path for them to\n * be mirrored into (see `server/api/routes/notifier.ts`). */\nexport const NOTIFIER_LIFECYCLES = [\"fyi\", \"action\"] as const;\nexport type NotifierLifecycle = (typeof NOTIFIER_LIFECYCLES)[number];\n\n/** Severity drives badge color (gray / amber / red, worst-wins) and\n * in a future iteration channel routing. Mostly stored verbatim by\n * the engine; the one engine-visible interaction is the rule that\n * `action` lifecycle cannot pair with `info` severity (see\n * `NotifierLifecycle` above). */\nexport const NOTIFIER_SEVERITIES = [\"info\", \"nudge\", \"urgent\"] as const;\nexport type NotifierSeverity = (typeof NOTIFIER_SEVERITIES)[number];\n\nexport interface NotifierEntry<TPluginData = unknown> {\n /** Engine-assigned UUID. Generated synchronously inside `publish()`\n * so the caller can use it before persistence completes. */\n id: string;\n /** Plugin namespace (e.g. `\"encore\"`, `\"debug__system\"`), and the\n * key the per-plugin isolation is built on: `updateForPlugin`,\n * `getForPlugin` and `clearForPlugin` each compare it and no-op on\n * a mismatch, so one plugin cannot touch another's entries.\n * `listFor` filters on it, and the UI groups by it. */\n pluginPkg: string;\n severity: NotifierSeverity;\n lifecycle?: NotifierLifecycle;\n title: string;\n body?: string;\n /** Optional in-app deep-link target (relative URL). The bell popup\n * routes here on row click, splicing in `notificationId=<id>` so the\n * landing page can identify which entry to clear — with `?` or `&`\n * as the target requires, and before any `#fragment`\n * (`appendNotificationId` in `NotificationBell.vue`). The engine\n * doesn't read this — it's a UI hint stored on the entry. */\n navigateTarget?: string;\n /** Opaque to the engine. Round-trips through JSON unchanged; only\n * the originating plugin's UI knows the shape. */\n pluginData?: TPluginData;\n /** ISO-8601 timestamp set at `publish()` time. */\n createdAt: string;\n}\n\n/** A history entry — a `NotifierEntry` after it has been cleared or\n * cancelled, with the terminal type and timestamp recorded. The\n * bell popup's \"History\" section renders these read-only. */\nexport interface NotifierHistoryEntry<TPluginData = unknown> extends NotifierEntry<TPluginData> {\n terminalType: \"cleared\" | \"cancelled\";\n terminalAt: string;\n}\n\n/** Caller-supplied input for `publish()`. The engine fills in `id`\n * and `createdAt`; everything else flows through verbatim.\n *\n * Two publish-time rules apply to `action` lifecycle (see\n * `NotifierLifecycle`):\n *\n * - `navigateTarget` MUST be a non-empty string.\n * - `severity` MUST NOT be `\"info\"`.\n *\n * Violations cause `publish()` to throw. Currently expressed as\n * runtime validation rather than a discriminated-union type, so the\n * fields below are all individually optional / loose at the\n * type-level. */\nexport interface PublishInput<TPluginData = unknown> {\n pluginPkg: string;\n severity: NotifierSeverity;\n title: string;\n body?: string | undefined;\n lifecycle?: NotifierLifecycle | undefined;\n navigateTarget?: string | undefined;\n pluginData?: TPluginData | undefined;\n}\n\n/** On-disk shape of `~/mulmoclaude/data/notifier/active.json`. Holds\n * only entries that haven't been cleared or cancelled — the file is\n * a snapshot, not an event log. */\nexport interface NotifierFile {\n entries: Record<string, NotifierEntry>;\n}\n\n/** On-disk shape of `~/mulmoclaude/data/notifier/history.json`. Array\n * of terminated entries newest-first, capped at `HISTORY_CAP` with\n * FIFO eviction (push at index 0, slice from the tail). */\nexport interface NotifierHistoryFile {\n entries: NotifierHistoryEntry[];\n}\n\n/** History size cap — how many terminated entries the FILE keeps, not\n * how many the UI shows: older ones fall off when new terminations\n * land. The bell popup renders `HISTORY_INITIAL_VISIBLE` of them and\n * reveals the rest behind \"Show more\" (`NotificationBell.vue`). */\nexport const HISTORY_CAP = 50;\n\n/** Pub-sub event published on the host's notifier channel after every\n * successful state change. Discriminated union — subscribers switch\n * on `type` to keep TypeScript narrowing the rest of the payload.\n *\n * `updated` carries the post-mutation entry — the receiver swaps\n * the matching `id` in their local active set. Reserved for in-\n * place edits via `updateForPlugin`; no history record is written\n * because the entry is still active, just with refreshed content. */\nexport type NotifierEvent =\n { type: \"published\"; entry: NotifierEntry } | { type: \"cleared\"; id: string } | { type: \"cancelled\"; id: string } | { type: \"updated\"; entry: NotifierEntry };\n","// Low-level file I/O for the notifier. Reads use node:fs directly;\n// writes go through an injected atomic-JSON writer (the host owns the\n// rename-based atomic write so it stays single-sourced with its other\n// writers). Kept separate from `engine.ts` so the path can be\n// overridden in tests without monkey-patching.\n\nimport { hasStringProp, isErrorWithCode, isRecord, isUnknownArray } from \"@mulmoclaude/common\";\nimport { promises as fsPromises } from \"node:fs\";\nimport {\n NOTIFIER_LIFECYCLES,\n NOTIFIER_SEVERITIES,\n type NotifierEntry,\n type NotifierFile,\n type NotifierHistoryEntry,\n type NotifierHistoryFile,\n} from \"./types.js\";\n\n/** Injected atomic JSON writer — the host's `writeJsonAtomic`. */\nexport type WriteJson = (filePath: string, data: unknown) => Promise<void>;\n\nconst TERMINAL_TYPES = [\"cleared\", \"cancelled\"] as const;\n\nfunction isOneOf<T extends string>(allowed: readonly T[], value: unknown): value is T {\n return allowed.some((candidate) => candidate === value);\n}\n\nfunction isOptionalString(value: unknown): boolean {\n return value === undefined || typeof value === \"string\";\n}\n\n// Checks every field `NotifierEntry` declares — `pluginData` is `unknown`,\n// so nothing is left unverified. A partial check would be an assertion\n// wearing a predicate's clothes.\nfunction isNotifierEntry(value: unknown): value is NotifierEntry {\n return (\n hasStringProp(value, \"id\") &&\n hasStringProp(value, \"pluginPkg\") &&\n hasStringProp(value, \"title\") &&\n hasStringProp(value, \"createdAt\") &&\n isOneOf(NOTIFIER_SEVERITIES, value.severity) &&\n (value.lifecycle === undefined || isOneOf(NOTIFIER_LIFECYCLES, value.lifecycle)) &&\n isOptionalString(value.body) &&\n isOptionalString(value.navigateTarget)\n );\n}\n\nfunction isNotifierHistoryEntry(value: unknown): value is NotifierHistoryEntry {\n return isNotifierEntry(value) && hasStringProp(value, \"terminalAt\") && isOneOf(TERMINAL_TYPES, value.terminalType);\n}\n\n// `isRecord` rejects `null` and arrays, so `{ entries: [] }` and\n// `{ entries: null }` stay malformed instead of reading as \"no entries\" —\n// downstream `engine.get` / `list*` mutations assume a plain object.\nfunction isNotifierFile(value: unknown): value is NotifierFile {\n return isRecord(value) && isRecord(value.entries) && Object.values(value.entries).every(isNotifierEntry);\n}\n\nfunction isNotifierHistoryFile(value: unknown): value is NotifierHistoryFile {\n return isRecord(value) && isUnknownArray(value.entries) && value.entries.every(isNotifierHistoryEntry);\n}\n\n/** Parsed JSON, or `undefined` when the file doesn't exist yet (first\n * ever call on a fresh workspace) — unambiguous as a sentinel because\n * no JSON document parses to `undefined`. */\nasync function readJsonIfPresent(filePath: string): Promise<unknown> {\n try {\n return JSON.parse(await fsPromises.readFile(filePath, \"utf-8\"));\n } catch (err) {\n if (isErrorWithCode(err) && err.code === \"ENOENT\") return undefined;\n throw err;\n }\n}\n\n/** Read the active-entries file. Returns an empty store when the file\n * doesn't exist yet. Any other read or parse failure throws — the\n * caller has to decide whether to surface or recover, since silently\n * treating \"malformed file\" as \"no entries\" would lose data. A rejected\n * file is never rewritten, so it stays on disk intact for repair. */\nexport async function loadActive(filePath: string): Promise<NotifierFile> {\n const parsed = await readJsonIfPresent(filePath);\n if (parsed === undefined) return { entries: {} };\n if (!isNotifierFile(parsed)) throw new Error(`notifier: malformed active.json at ${filePath}`);\n return parsed;\n}\n\n/** Write the active-entries file via the injected atomic writer so a\n * half-written file is never visible to readers. The caller serialises\n * writes (engine.ts queues mutations) — this function makes no\n * concurrency guarantees of its own. */\nexport async function saveActive(writeJson: WriteJson, filePath: string, state: NotifierFile): Promise<void> {\n await writeJson(filePath, state);\n}\n\n/** Read the history file. Empty array on first run. Same parse-error\n * policy as `loadActive`. */\nexport async function loadHistory(filePath: string): Promise<NotifierHistoryFile> {\n const parsed = await readJsonIfPresent(filePath);\n if (parsed === undefined) return { entries: [] };\n if (!isNotifierHistoryFile(parsed)) throw new Error(`notifier: malformed history.json at ${filePath}`);\n return parsed;\n}\n\nexport async function saveHistory(writeJson: WriteJson, filePath: string, state: NotifierHistoryFile): Promise<void> {\n await writeJson(filePath, state);\n}\n","// Publish-input validation — pure, dependency-free, so every caller\n// can share one rule set. Today that is `engine.publish`, which throws,\n// and `updateForPlugin`, which re-validates the MERGED entry and skips\n// the update with a warning rather than throwing — an update must not\n// be able to degrade an entry below publish-time invariants, but it is\n// also not the caller's error to handle. Publish is deliberately absent\n// from the HTTP surface (see `server/api/routes/notifier.ts`), so the\n// route layer this comment once also named no longer exists.\n\nimport type { PublishInput } from \"./types.js\";\n\n/** Hard caps on publish-input fields. The engine reads each entry on\n * every list/get call (no in-memory cache), so unbounded fields hurt\n * every reader. Caps chosen to be generous for legitimate UX copy\n * while bounding active.json growth: a notification fundamentally is\n * a short blurb, not a document. */\nexport const NOTIFIER_LIMITS = {\n titleMax: 200,\n bodyMax: 4000,\n navigateTargetMax: 1000,\n pluginDataMaxBytes: 16 * 1024,\n} as const;\n\nfunction validateTitle(title: string): string | null {\n if (typeof title !== \"string\" || title.length === 0) return \"title must be a non-empty string\";\n if (title.length > NOTIFIER_LIMITS.titleMax) return `title exceeds max length of ${NOTIFIER_LIMITS.titleMax} chars`;\n return null;\n}\n\nfunction validateBody(body: string | undefined): string | null {\n if (body === undefined) return null;\n if (body.length > NOTIFIER_LIMITS.bodyMax) return `body exceeds max length of ${NOTIFIER_LIMITS.bodyMax} chars`;\n return null;\n}\n\nfunction validateNavigateTarget(target: string | undefined): string | null {\n if (target === undefined) return null;\n if (target.length === 0) return \"navigateTarget must be a non-empty relative path when set\";\n if (target.length > NOTIFIER_LIMITS.navigateTargetMax) {\n return `navigateTarget exceeds max length of ${NOTIFIER_LIMITS.navigateTargetMax} chars`;\n }\n // Must be a same-origin relative path. Reject schemes\n // (`javascript:`, `https://...`) and scheme-relative URLs\n // (`//evil.com/...`, which an `<a href>` would resolve to the\n // attacker's origin). One leading \"/\" only.\n if (!target.startsWith(\"/\") || target.startsWith(\"//\")) {\n return \"navigateTarget must be a relative path beginning with a single '/' (no scheme, no '//')\";\n }\n return null;\n}\n\nfunction validatePluginData(pluginData: unknown): string | null {\n if (pluginData === undefined) return null;\n let serialized: string | undefined;\n try {\n serialized = JSON.stringify(pluginData);\n } catch (err) {\n return `pluginData is not JSON-serialisable: ${String(err)}`;\n }\n // `JSON.stringify` returns `undefined` for non-serialisable roots\n // (e.g. a bare function or symbol). Treat that as a serialisation\n // failure so it doesn't slip through as an empty-string size.\n if (typeof serialized !== \"string\") return \"pluginData is not JSON-serialisable\";\n if (serialized.length > NOTIFIER_LIMITS.pluginDataMaxBytes) {\n return `pluginData JSON exceeds ${NOTIFIER_LIMITS.pluginDataMaxBytes} bytes`;\n }\n return null;\n}\n\nfunction validateActionCoherence(input: PublishInput): string | null {\n if (input.lifecycle !== \"action\") return null;\n if (input.severity === \"info\") {\n return \"action lifecycle is incompatible with info severity (use fyi for low-priority pings)\";\n }\n if (typeof input.navigateTarget !== \"string\" || input.navigateTarget.length === 0) {\n return \"action lifecycle requires a non-empty navigateTarget\";\n }\n return null;\n}\n\n/** Validate a `PublishInput`. Returns `null` if OK, or a\n * human-readable error string. Order matters — shape/size errors are\n * reported before lifecycle/severity coherence errors so the message\n * the caller sees points at the most fundamental problem first. */\nexport function validatePublishInput(input: PublishInput): string | null {\n return (\n validateTitle(input.title) ??\n validateBody(input.body) ??\n validateNavigateTarget(input.navigateTarget) ??\n validatePluginData(input.pluginData) ??\n validateActionCoherence(input)\n );\n}\n","// Notifier engine — single-process, two-file (active + history),\n// single-channel. Host-agnostic: file paths, the atomic JSON writer,\n// the pub-sub event sink, and the logger are all injected via\n// `configureNotifier` + `setNotifierFilePaths` so MulmoClaude and\n// MulmoTerminal share one notification engine over their own\n// workspaces and pub-sub fabrics.\n//\n// API surface: publish / clear / cancel / get / listFor / listAll /\n// listHistory (+ plugin-scoped variants). Mutations queue through a\n// writing-flag + waiter-queue coordinator so concurrent callers can't\n// race on the atomic write's rename. Reads bypass the queue (rename\n// atomicity makes half-reads impossible) and trade strict\n// linearisability for simpler code: the contract is \"after\n// `await publish(x)` resolves, subsequent reads see x\" — which holds\n// because `publish` awaits the persist before returning.\n//\n// `clear` / `cancel` push to history *before* removing from active.\n// History persistence is best-effort: if it fails, the active write\n// still wins and the failure is logged. Active is the source of\n// truth; history is an audit aid.\n\nimport type { MinimalLogger } from \"@mulmoclaude/common\";\nimport { randomUUID } from \"node:crypto\";\nimport { loadActive, loadHistory, saveActive, saveHistory, type WriteJson } from \"./store.js\";\nimport { validatePublishInput } from \"./validate.js\";\nimport {\n HISTORY_CAP,\n type NotifierEntry,\n type NotifierEvent,\n type NotifierFile,\n type NotifierHistoryEntry,\n type NotifierSeverity,\n type PublishInput,\n} from \"./types.js\";\n\nexport { NOTIFIER_LIMITS, validatePublishInput } from \"./validate.js\";\n\n// ── Dependency injection ──────────────────────────────────────────\n\n/** Minimal logger the engine needs. The host passes its structured\n * logger; absent one, failures are swallowed (the engine never throws\n * on a fan-out/persist-best-effort path). */\nexport type NotifierLogger = Pick<MinimalLogger, \"warn\" | \"error\">;\n\nexport interface NotifierConfig {\n /** Atomic JSON writer (the host's `writeJsonAtomic`). */\n writeJson: WriteJson;\n /** Fan-out sink — the host binds this to `pubsub.publish(channel, event)`. */\n publishEvent: (event: NotifierEvent) => void;\n /** Optional logger. */\n log?: NotifierLogger;\n}\n\nconst NOOP_LOG: NotifierLogger = { warn: () => {}, error: () => {} };\n\nlet config: NotifierConfig | null = null;\nlet activeFilePath = \"\";\nlet historyFilePath = \"\";\n\nfunction logger(): NotifierLogger {\n return config?.log ?? NOOP_LOG;\n}\n\n/** Wire the engine's I/O deps. Call once at startup, before the first\n * mutation. Does NOT set file paths — those are set independently via\n * `setNotifierFilePaths` so a host can bind production paths at module\n * load and a test can override them without re-supplying the deps. */\nexport function configureNotifier(injected: NotifierConfig): void {\n config = injected;\n}\n\n// ── In-process event listeners ────────────────────────────────────\n//\n// Separate from the socket.io pubsub so server-side adapters (macOS\n// push, future Encore) can react to state changes without going\n// through a websocket round-trip. The host's pubsub is fan-out-only\n// with no server-side subscribe, so this listener registry is the\n// in-process equivalent. Listeners run synchronously inside `emit`,\n// before the pubsub fan-out.\n\ntype NotifierEventListener = (event: NotifierEvent) => void;\nconst listeners: NotifierEventListener[] = [];\n\n/** Register an in-process listener for engine events. Returns an\n * unsubscribe function the caller can use during teardown. */\nexport function onEvent(listener: NotifierEventListener): () => void {\n listeners.push(listener);\n return () => {\n const idx = listeners.indexOf(listener);\n if (idx >= 0) listeners.splice(idx, 1);\n };\n}\n\nfunction emit(event: NotifierEvent): void {\n // In-process fan-out first. Each listener is wrapped: a throwing\n // listener must not poison the rest, and must not propagate out of\n // `processBatch` and strand the still-unsettled waiters (their\n // resolve/reject is called *after* this emit loop). Fan-out is\n // best-effort by contract — losing one subscriber must not lose\n // the write that already committed.\n for (const listener of listeners) {\n try {\n listener(event);\n } catch (err) {\n logger().error(\"in-process listener failed\", { type: event.type, error: String(err) });\n }\n }\n if (!config) {\n logger().warn(\"emit before init\", { type: event.type });\n return;\n }\n try {\n config.publishEvent(event);\n } catch (err) {\n logger().error(\"emit failed\", { type: event.type, error: String(err) });\n }\n}\n\n// ── Write coordinator ─────────────────────────────────────────────\n\n/** A mutation function applied to the in-memory state object during\n * drain. Returns either:\n *\n * - `null` — no state change (e.g., `clear` on an unknown id).\n * The drainer skips the disk write and the emit if every\n * mutation in a batch returned `null`.\n * - `{ event, historyEntry? }` — state changed. The drainer emits\n * the event after the active write succeeds, and prepends\n * `historyEntry` to history (best-effort) when present.\n *\n * Mutations MUST NOT modify state when returning `null`. Violating\n * this invariant produces a write skip with stale on-disk state. */\ntype MutationOutcome = { event: NotifierEvent; historyEntry?: NotifierHistoryEntry } | null;\ntype Mutation = (state: NotifierFile) => MutationOutcome;\n\ninterface Waiter {\n mutate: Mutation;\n resolve: () => void;\n reject: (err: unknown) => void;\n}\n\ntype MutationResult = { ok: true; outcome: MutationOutcome } | { ok: false; error: unknown };\n\nlet writing = false;\nlet waiters: Waiter[] = [];\n\n/** Point the engine at its active/history files. Resets the write\n * queue, so callers must not have in-flight mutations. The host calls\n * this once with the workspace paths; tests call it per-case with temp\n * files. */\nexport function setNotifierFilePaths(paths: { active: string; history: string }): void {\n activeFilePath = paths.active;\n historyFilePath = paths.history;\n writing = false;\n waiters = [];\n}\n\n/** Test-only: clear config + queue so each suite starts clean. */\nexport function resetNotifier(): void {\n config = null;\n activeFilePath = \"\";\n historyFilePath = \"\";\n writing = false;\n waiters = [];\n listeners.length = 0;\n}\n\nfunction requireWriteJson(): WriteJson {\n if (!config) throw new Error(\"notifier: configureNotifier() not called\");\n return config.writeJson;\n}\n\n/** A waiter carried alongside the outcome of its own mutation, so settling can\n * never pair a result with the wrong caller's promise. */\ninterface SettledMutation {\n waiter: Waiter;\n result: MutationResult;\n}\n\nfunction applyBatchMutations(batch: Waiter[], state: NotifierFile): SettledMutation[] {\n return batch.map((waiter) => {\n try {\n return { waiter, result: { ok: true, outcome: waiter.mutate(state) } };\n } catch (err) {\n return { waiter, result: { ok: false, error: err } };\n }\n });\n}\n\nfunction collectEvents(settled: SettledMutation[]): NotifierEvent[] {\n const events: NotifierEvent[] = [];\n for (const { result } of settled) {\n if (result.ok && result.outcome !== null) events.push(result.outcome.event);\n }\n return events;\n}\n\nfunction collectHistoryEntries(settled: SettledMutation[]): NotifierHistoryEntry[] {\n const entries: NotifierHistoryEntry[] = [];\n for (const { result } of settled) {\n if (result.ok && result.outcome !== null && result.outcome.historyEntry) {\n entries.push(result.outcome.historyEntry);\n }\n }\n return entries;\n}\n\nfunction settleBatch(settled: SettledMutation[]): void {\n // Resolves come AFTER any emits so subscribers see the event\n // before the caller's `await` returns.\n for (const { waiter, result } of settled) {\n if (result.ok) waiter.resolve();\n else waiter.reject(result.error);\n }\n}\n\nfunction rejectBatch(batch: Waiter[], err: unknown): void {\n for (const waiter of batch) waiter.reject(err);\n}\n\nasync function persistHistory(newEntries: NotifierHistoryEntry[]): Promise<void> {\n const existing = await loadHistory(historyFilePath);\n // Newest-first ordering: a batch contains terminations in arrival\n // order; we want the last one to land at index 0 of history.\n const merged = [...newEntries.slice().reverse(), ...existing.entries].slice(0, HISTORY_CAP);\n await saveHistory(requireWriteJson(), historyFilePath, { entries: merged });\n}\n\nasync function processBatch(batch: Waiter[]): Promise<void> {\n let state: NotifierFile;\n try {\n state = await loadActive(activeFilePath);\n } catch (err) {\n logger().error(\"load failed\", { error: String(err) });\n rejectBatch(batch, err);\n return;\n }\n const settled = applyBatchMutations(batch, state);\n const events = collectEvents(settled);\n const historyEntries = collectHistoryEntries(settled);\n\n if (events.length > 0) {\n try {\n await saveActive(requireWriteJson(), activeFilePath, state);\n } catch (err) {\n logger().error(\"active write failed\", { error: String(err) });\n rejectBatch(batch, err);\n return;\n }\n if (historyEntries.length > 0) {\n // Best-effort: active is the source of truth, history is an\n // audit aid. A failed history write is logged but doesn't\n // unwind the active commit.\n try {\n await persistHistory(historyEntries);\n } catch (err) {\n logger().error(\"history write failed\", { error: String(err) });\n }\n }\n for (const event of events) emit(event);\n }\n settleBatch(settled);\n}\n\nasync function drain(): Promise<void> {\n writing = true;\n try {\n while (waiters.length > 0) {\n const batch = waiters;\n waiters = [];\n await processBatch(batch);\n }\n } finally {\n writing = false;\n }\n}\n\nfunction enqueue(mutate: Mutation): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n waiters.push({ mutate, resolve, reject });\n if (!writing) void drain();\n });\n}\n\nfunction removeEntry(state: NotifierFile, entryId: string): NotifierFile[\"entries\"] {\n // Object-rest excludes the key without invoking `delete`.\n const { [entryId]: __removed, ...remaining } = state.entries;\n return remaining;\n}\n\nfunction buildHistoryEntry(entry: NotifierEntry, terminalType: \"cleared\" | \"cancelled\"): NotifierHistoryEntry {\n return { ...entry, terminalType, terminalAt: new Date().toISOString() };\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\n/** The optional half of a publish input, with unset keys left OUT rather than\n * written as `undefined` — an entry is persisted and re-read through JSON,\n * where an unset field carries no key at all. */\nfunction optionalPublishFields<TPluginData>(input: PublishInput<TPluginData>): Partial<NotifierEntry<TPluginData>> {\n return {\n ...(input.lifecycle !== undefined ? { lifecycle: input.lifecycle } : {}),\n ...(input.body !== undefined ? { body: input.body } : {}),\n ...(input.navigateTarget !== undefined ? { navigateTarget: input.navigateTarget } : {}),\n ...(input.pluginData !== undefined ? { pluginData: input.pluginData } : {}),\n };\n}\n\nexport async function publish<TPluginData = unknown>(input: PublishInput<TPluginData>): Promise<{ id: string }> {\n // Validate at the engine boundary, so every caller shares one wall —\n // plugin runtime, legacy wrapper, host module. Publish has no HTTP\n // surface to validate separately (`server/api/routes/notifier.ts`).\n const validationError = validatePublishInput(input);\n if (validationError) {\n throw new Error(`notifier.publish: ${validationError}`);\n }\n const entryId = randomUUID();\n const entry: NotifierEntry<TPluginData> = {\n id: entryId,\n pluginPkg: input.pluginPkg,\n severity: input.severity,\n title: input.title,\n createdAt: new Date().toISOString(),\n ...optionalPublishFields(input),\n };\n await enqueue((state) => {\n state.entries[entryId] = entry;\n return { event: { type: \"published\", entry } };\n });\n return { id: entryId };\n}\n\n/** Remove an active entry and record its terminal history. `clear`\n * (user dismissed) and `cancel` (producer withdrew) differ only in the\n * emitted event / history reason. No-op when the id is unknown. */\nasync function terminateEntry(entryId: string, reason: \"cleared\" | \"cancelled\"): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: reason === \"cleared\" ? { type: \"cleared\", id: entryId } : { type: \"cancelled\", id: entryId },\n historyEntry: buildHistoryEntry(entry, reason),\n };\n });\n}\n\nexport async function clear(entryId: string): Promise<void> {\n await terminateEntry(entryId, \"cleared\");\n}\n\nexport async function cancel(entryId: string): Promise<void> {\n await terminateEntry(entryId, \"cancelled\");\n}\n\n/** In-place update for an active entry. Only the fields present on\n * `patch` are rewritten; `id`, `pluginPkg`, `lifecycle`, and\n * `createdAt` stay fixed. Emits a single `\"updated\"` event with the\n * post-mutation entry — no history record is written because the\n * entry is still active, just with refreshed content.\n *\n * No-ops (no throw) when the id is unknown, the entry belongs to a\n * different plugin, or the merged shape would violate\n * `validatePublishInput`. The silent skip matches `clearForPlugin`'s\n * isolation semantics; validation failures are logged for diagnosis. */\nexport async function updateForPlugin<TPluginData = unknown>(\n pluginPkg: string,\n entryId: string,\n patch: {\n severity?: NotifierSeverity;\n title?: string;\n body?: string;\n navigateTarget?: string;\n pluginData?: TPluginData;\n },\n): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n if (entry.pluginPkg !== pluginPkg) return null;\n const next: NotifierEntry = {\n ...entry,\n ...(patch.severity !== undefined ? { severity: patch.severity } : {}),\n ...(patch.title !== undefined ? { title: patch.title } : {}),\n ...(patch.body !== undefined ? { body: patch.body } : {}),\n ...(patch.navigateTarget !== undefined ? { navigateTarget: patch.navigateTarget } : {}),\n ...(patch.pluginData !== undefined ? { pluginData: patch.pluginData } : {}),\n };\n // Re-validate the merged shape so an update can't degrade the\n // entry below publish-time invariants.\n const validationError = validatePublishInput({\n pluginPkg: next.pluginPkg,\n severity: next.severity,\n title: next.title,\n body: next.body,\n lifecycle: next.lifecycle,\n navigateTarget: next.navigateTarget,\n pluginData: next.pluginData,\n });\n if (validationError) {\n logger().warn(\"update rejected by validation\", { entryId, pluginPkg, error: validationError });\n return null;\n }\n state.entries[entryId] = next;\n return { event: { type: \"updated\", entry: next } };\n });\n}\n\n/** Plugin-scoped point lookup. Returns the entry by id, but only if it\n * belongs to the caller's plugin; otherwise undefined. Cross-plugin\n * reads return undefined for isolation — same property as\n * `clearForPlugin` / `updateForPlugin`. */\nexport async function getForPlugin(pluginPkg: string, entryId: string): Promise<NotifierEntry | undefined> {\n const state = await loadActive(activeFilePath);\n const entry = state.entries[entryId];\n if (!entry) return undefined;\n if (entry.pluginPkg !== pluginPkg) return undefined;\n return entry;\n}\n\n/** Plugin-scoped clear. Same as `clear` but no-ops if the entry's\n * `pluginPkg` doesn't match the caller's, so a plugin can't dismiss\n * another plugin's notification by guessing or scraping its id. */\nexport async function clearForPlugin(pluginPkg: string, entryId: string): Promise<void> {\n await enqueue((state) => {\n const entry = state.entries[entryId];\n if (!entry) return null;\n if (entry.pluginPkg !== pluginPkg) return null;\n state.entries = removeEntry(state, entryId);\n return {\n event: { type: \"cleared\", id: entryId },\n historyEntry: buildHistoryEntry(entry, \"cleared\"),\n };\n });\n}\n\nexport async function get(entryId: string): Promise<NotifierEntry | undefined> {\n const state = await loadActive(activeFilePath);\n return state.entries[entryId];\n}\n\nexport async function listFor(pluginPkg: string): Promise<NotifierEntry[]> {\n const state = await loadActive(activeFilePath);\n return Object.values(state.entries).filter((entry) => entry.pluginPkg === pluginPkg);\n}\n\nexport async function listAll(): Promise<NotifierEntry[]> {\n const state = await loadActive(activeFilePath);\n return Object.values(state.entries);\n}\n\nexport async function listHistory(): Promise<NotifierHistoryEntry[]> {\n const state = await loadHistory(historyFilePath);\n return state.entries;\n}\n"],"mappings":"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"}
|
|
@@ -138,7 +138,7 @@ IconGlyph.props = {
|
|
|
138
138
|
};
|
|
139
139
|
//#endregion
|
|
140
140
|
//#region ../../node_modules/dompurify/dist/purify.es.mjs
|
|
141
|
-
/*! @license DOMPurify 3.4.
|
|
141
|
+
/*! @license DOMPurify 3.4.15 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.15/LICENSE */
|
|
142
142
|
function _arrayLikeToArray(r, a) {
|
|
143
143
|
(null == a || a > r.length) && (a = r.length);
|
|
144
144
|
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
|
|
@@ -1133,7 +1133,7 @@ var _resolveObjectOption = function _resolveObjectOption(cfg, key, makeFallback)
|
|
|
1133
1133
|
function createDOMPurify() {
|
|
1134
1134
|
let window = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal();
|
|
1135
1135
|
const DOMPurify = (root) => createDOMPurify(root);
|
|
1136
|
-
DOMPurify.version = "3.4.
|
|
1136
|
+
DOMPurify.version = "3.4.15";
|
|
1137
1137
|
DOMPurify.removed = [];
|
|
1138
1138
|
if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
|
|
1139
1139
|
DOMPurify.isSupported = false;
|
|
@@ -1150,6 +1150,7 @@ function createDOMPurify() {
|
|
|
1150
1150
|
const ElementPrototype = Element.prototype;
|
|
1151
1151
|
const cloneNode = lookupGetter(ElementPrototype, "cloneNode");
|
|
1152
1152
|
const remove = lookupGetter(ElementPrototype, "remove");
|
|
1153
|
+
const removeAttributeNode = lookupGetter(ElementPrototype, "removeAttributeNode");
|
|
1153
1154
|
const getNextSibling = lookupGetter(ElementPrototype, "nextSibling");
|
|
1154
1155
|
const getChildNodes = lookupGetter(ElementPrototype, "childNodes");
|
|
1155
1156
|
const getParentNode = lookupGetter(ElementPrototype, "parentNode");
|
|
@@ -1604,7 +1605,7 @@ function createDOMPurify() {
|
|
|
1604
1605
|
*/
|
|
1605
1606
|
const _stripAttributeNode = function _stripAttributeNode(element, attribute, name) {
|
|
1606
1607
|
try {
|
|
1607
|
-
|
|
1608
|
+
removeAttributeNode(element, attribute);
|
|
1608
1609
|
} catch (_) {
|
|
1609
1610
|
try {
|
|
1610
1611
|
element.removeAttribute(name);
|
|
@@ -1677,7 +1678,7 @@ function createDOMPurify() {
|
|
|
1677
1678
|
from: element
|
|
1678
1679
|
});
|
|
1679
1680
|
try {
|
|
1680
|
-
if (attr)
|
|
1681
|
+
if (attr) removeAttributeNode(element, attr);
|
|
1681
1682
|
else element.removeAttribute(name);
|
|
1682
1683
|
} catch (_) {
|
|
1683
1684
|
try {
|
|
@@ -1923,7 +1924,7 @@ function createDOMPurify() {
|
|
|
1923
1924
|
const realTagName = getNodeName ? getNodeName(element) : null;
|
|
1924
1925
|
if (typeof realTagName !== "string") return false;
|
|
1925
1926
|
if (transformCaseFunc(realTagName) !== "form") return false;
|
|
1926
|
-
return typeof element.nodeName !== "string" || typeof element.textContent !== "string" || typeof element.removeChild !== "function" || element.attributes !== getAttributes(element) || typeof element.removeAttribute !== "function" || typeof element.setAttribute !== "function" || typeof element.namespaceURI !== "string" || typeof element.insertBefore !== "function" || typeof element.hasChildNodes !== "function" || element.nodeType !== getNodeType(element) || element.childNodes !== getChildNodes(element);
|
|
1927
|
+
return typeof element.nodeName !== "string" || typeof element.textContent !== "string" || typeof element.removeChild !== "function" || element.attributes !== getAttributes(element) || typeof element.removeAttribute !== "function" || typeof element.removeAttributeNode !== "function" || typeof element.getAttributeNode !== "function" || typeof element.setAttribute !== "function" || typeof element.namespaceURI !== "string" || typeof element.insertBefore !== "function" || typeof element.hasChildNodes !== "function" || element.nodeType !== getNodeType(element) || element.childNodes !== getChildNodes(element);
|
|
1927
1928
|
};
|
|
1928
1929
|
/**
|
|
1929
1930
|
* Checks whether the given value is a DocumentFragment from any realm.
|
|
@@ -2208,24 +2209,38 @@ function createDOMPurify() {
|
|
|
2208
2209
|
/**
|
|
2209
2210
|
* Write a modified attribute value back onto the element. On
|
|
2210
2211
|
* success, re-probe for clobbering introduced by the new value and
|
|
2211
|
-
* remove the element when found; otherwise
|
|
2212
|
-
*
|
|
2213
|
-
*
|
|
2212
|
+
* remove the element when found; otherwise, when this writeback is the
|
|
2213
|
+
* recreate half of the SANITIZE_NAMED_PROPS remove-and-recreate, pop the
|
|
2214
|
+
* removal entry that path recorded so it does not show as removed. On
|
|
2214
2215
|
* failure, remove the attribute instead.
|
|
2215
2216
|
*
|
|
2217
|
+
* Returns true only on a clean write (the value was set and the new value
|
|
2218
|
+
* introduced no clobbering). The caller uses that, together with its own
|
|
2219
|
+
* knowledge of whether this attribute pushed a DOMPurify.removed record, to
|
|
2220
|
+
* decide whether to pop that record. The pop must happen ONLY for the
|
|
2221
|
+
* named-prop remove-and-recreate; popping on any other value change (trim,
|
|
2222
|
+
* template scrubbing, Trusted Types) would consume an unrelated _forceRemove
|
|
2223
|
+
* subtree-cleanup record and let that detached subtree keep a live event
|
|
2224
|
+
* handler through the IN_PLACE neutralization pass (SO-001).
|
|
2225
|
+
*
|
|
2216
2226
|
* @param currentNode the element carrying the attribute
|
|
2217
2227
|
* @param name the attribute name as present on the element
|
|
2218
2228
|
* @param namespaceURI the attribute's namespace, if any
|
|
2219
2229
|
* @param value the new attribute value
|
|
2230
|
+
* @return true if the value was written without introducing clobbering
|
|
2220
2231
|
*/
|
|
2221
2232
|
const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) {
|
|
2222
2233
|
try {
|
|
2223
2234
|
if (namespaceURI) currentNode.setAttributeNS(namespaceURI, name, value);
|
|
2224
2235
|
else currentNode.setAttribute(name, value);
|
|
2225
|
-
if (_isClobbered(currentNode))
|
|
2226
|
-
|
|
2236
|
+
if (_isClobbered(currentNode)) {
|
|
2237
|
+
_forceRemove(currentNode);
|
|
2238
|
+
return false;
|
|
2239
|
+
}
|
|
2240
|
+
return true;
|
|
2227
2241
|
} catch (_) {
|
|
2228
2242
|
_removeAttribute(name, currentNode);
|
|
2243
|
+
return false;
|
|
2229
2244
|
}
|
|
2230
2245
|
};
|
|
2231
2246
|
/**
|
|
@@ -2258,6 +2273,7 @@ function createDOMPurify() {
|
|
|
2258
2273
|
const lcName = transformCaseFunc(name);
|
|
2259
2274
|
const initValue = attrValue;
|
|
2260
2275
|
let value = name === "value" ? initValue : stringTrim(initValue);
|
|
2276
|
+
let recreatedNamedProp = false;
|
|
2261
2277
|
hookEvent.attrName = lcName;
|
|
2262
2278
|
hookEvent.attrValue = value;
|
|
2263
2279
|
hookEvent.keepAttr = true;
|
|
@@ -2267,6 +2283,7 @@ function createDOMPurify() {
|
|
|
2267
2283
|
if (SANITIZE_NAMED_PROPS && (lcName === "id" || lcName === "name") && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
|
|
2268
2284
|
_removeAttribute(name, currentNode, attr);
|
|
2269
2285
|
value = SANITIZE_NAMED_PROPS_PREFIX + value;
|
|
2286
|
+
recreatedNamedProp = true;
|
|
2270
2287
|
}
|
|
2271
2288
|
if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
|
|
2272
2289
|
_removeAttribute(name, currentNode, attr);
|
|
@@ -2291,7 +2308,9 @@ function createDOMPurify() {
|
|
|
2291
2308
|
continue;
|
|
2292
2309
|
}
|
|
2293
2310
|
value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
|
|
2294
|
-
if (value !== initValue)
|
|
2311
|
+
if (value !== initValue) {
|
|
2312
|
+
if (_setAttributeValue(currentNode, name, namespaceURI, value) && recreatedNamedProp) arrayPop(DOMPurify.removed);
|
|
2313
|
+
}
|
|
2295
2314
|
}
|
|
2296
2315
|
_executeHooks(hooks.afterSanitizeAttributes, currentNode, null);
|
|
2297
2316
|
};
|
|
@@ -2425,7 +2444,7 @@ function createDOMPurify() {
|
|
|
2425
2444
|
if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === "BODY") body = importedNode;
|
|
2426
2445
|
else if (importedNode.nodeName === "HTML") body = importedNode;
|
|
2427
2446
|
else body.appendChild(importedNode);
|
|
2428
|
-
_sanitizeAttachedShadowRoots(
|
|
2447
|
+
_sanitizeAttachedShadowRoots(body);
|
|
2429
2448
|
} else {
|
|
2430
2449
|
if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && dirty.indexOf("<") === -1) return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(dirty) : dirty;
|
|
2431
2450
|
body = _initDocument(dirty);
|