@mulmoclaude/core 5.7.1 → 5.8.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/dist/artifacts/paths.cjs +38 -75
- package/dist/artifacts/paths.cjs.map +1 -1
- package/dist/artifacts/paths.d.ts +2 -59
- package/dist/artifacts/paths.js +2 -69
- package/dist/artifacts/paths.js.map +1 -1
- package/dist/collection/firestore.cjs.map +1 -1
- package/dist/collection/firestore.js.map +1 -1
- package/dist/collection/index.cjs +3 -3
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.js +3 -3
- package/dist/collection/registry/server/index.cjs +3 -3
- package/dist/collection/registry/server/index.cjs.map +1 -1
- package/dist/collection/registry/server/index.js +3 -3
- package/dist/collection/registry/server/index.js.map +1 -1
- 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.cjs.map +1 -1
- package/dist/collection-watchers/index.js +4 -4
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/{discovery-XoYJI2Ot.js → discovery-CbwHtcwk.js} +3 -3
- package/dist/{discovery-XoYJI2Ot.js.map → discovery-CbwHtcwk.js.map} +1 -1
- package/dist/{discovery-CwBXvtyn.cjs → discovery-DgZRmO7F.cjs} +3 -3
- package/dist/discovery-DgZRmO7F.cjs.map +1 -0
- package/dist/{dist-Gj7ygaW2.cjs → dist-BZ62pKuU.cjs} +11 -1
- package/dist/dist-BZ62pKuU.cjs.map +1 -0
- package/dist/{dist-BzoA9pDR.js → dist-MFUV9FYo.js} +6 -2
- package/dist/dist-MFUV9FYo.js.map +1 -0
- package/dist/errorMessage-OEQzRwsi.cjs +1 -0
- package/dist/errorMessage-VEiXHRnX.js +1 -0
- package/dist/{errors-CdXsfFCF.cjs → errors-B0xloJ3E.cjs} +2 -2
- package/dist/errors-B0xloJ3E.cjs.map +1 -0
- package/dist/{errors-C7TI_t_4.js → errors-zTAsW7dS.js} +2 -2
- package/dist/{errors-C7TI_t_4.js.map → errors-zTAsW7dS.js.map} +1 -1
- package/dist/feeds/paths.cjs.map +1 -1
- package/dist/feeds/server/index.cjs +4 -4
- package/dist/feeds/server/index.cjs.map +1 -1
- package/dist/feeds/server/index.js +4 -4
- package/dist/feeds/server/index.js.map +1 -1
- package/dist/file-change/index.cjs.map +1 -1
- package/dist/files/index.cjs.map +1 -1
- package/dist/files/index.js.map +1 -1
- package/dist/global-config/index.cjs.map +1 -1
- package/dist/google/index.cjs +3 -3
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.js +3 -3
- package/dist/google/index.js.map +1 -1
- package/dist/graph-CURotEkH.cjs.map +1 -1
- package/dist/ingestTypes-DS9HiR2I.cjs.map +1 -1
- package/dist/{listen-Dca-NoBi.js → listen-DUlzSOzX.js} +2 -2
- package/dist/{listen-Dca-NoBi.js.map → listen-DUlzSOzX.js.map} +1 -1
- package/dist/{listen-BXthb-9C.cjs → listen-N_8XdKz_.cjs} +2 -2
- package/dist/listen-N_8XdKz_.cjs.map +1 -0
- package/dist/notifier/index.cjs +1 -1
- package/dist/notifier/index.js +1 -1
- package/dist/{notifier-B65Mvngb.cjs → notifier-CiemUClt.cjs} +3 -6
- package/dist/notifier-CiemUClt.cjs.map +1 -0
- package/dist/{notifier-vAp5t6wL.js → notifier-dgkC-h80.js} +2 -5
- package/dist/notifier-dgkC-h80.js.map +1 -0
- package/dist/paths-C8-jxR0s.cjs.map +1 -1
- package/dist/plugin-vue/i18n.cjs.map +1 -1
- package/dist/plugin-vue/index.cjs +82 -16
- package/dist/plugin-vue/index.cjs.map +1 -1
- package/dist/plugin-vue/index.js +82 -16
- package/dist/plugin-vue/index.js.map +1 -1
- package/dist/{promptSafety-BGV1wrE6.cjs → promptSafety-auQEatax.cjs} +2 -2
- package/dist/promptSafety-auQEatax.cjs.map +1 -0
- package/dist/{promptSafety-B5uGroik.js → promptSafety-yG60Vof8.js} +2 -2
- package/dist/promptSafety-yG60Vof8.js.map +1 -0
- package/dist/relPath-CTAjGdCL.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 +4 -4
- package/dist/remote-host/server/index.cjs.map +1 -1
- package/dist/remote-host/server/index.js +4 -4
- package/dist/remote-host/server/index.js.map +1 -1
- package/dist/{remote-host-DhAHBYzN.cjs → remote-host-BMjh_F-6.cjs} +2 -2
- package/dist/remote-host-BMjh_F-6.cjs.map +1 -0
- package/dist/{remote-host-Cjox2F2J.js → remote-host-CoNX9JEC.js} +2 -2
- package/dist/remote-host-CoNX9JEC.js.map +1 -0
- package/dist/remote-view/index.cjs +1 -1
- package/dist/remote-view/index.cjs.map +1 -1
- package/dist/remote-view/index.js +1 -1
- package/dist/root-BvwBUcol.cjs.map +1 -1
- package/dist/scheduler/index.cjs +2 -2
- package/dist/scheduler/index.cjs.map +1 -1
- package/dist/scheduler/index.js +2 -2
- package/dist/scheduler/index.js.map +1 -1
- package/dist/{server-CLhww8NL.cjs → server-RsnFqPt7.cjs} +5 -5
- package/dist/server-RsnFqPt7.cjs.map +1 -0
- package/dist/{server-CTkTHqEL.js → server-iYXYObKb.js} +5 -5
- package/dist/server-iYXYObKb.js.map +1 -0
- package/dist/skill-bridge/index.cjs.map +1 -1
- package/dist/utils/index.cjs +2 -2
- package/dist/utils/index.js +2 -2
- package/dist/whisper/index.cjs +2 -2
- package/dist/whisper/index.cjs.map +1 -1
- package/dist/whisper/index.js +2 -2
- package/dist/whisper/index.js.map +1 -1
- package/dist/wiki/index.cjs +1 -1
- package/dist/wiki/index.cjs.map +1 -1
- package/dist/wiki/index.js +1 -1
- package/dist/wiki/index.js.map +1 -1
- package/dist/wiki/paths.cjs.map +1 -1
- package/dist/wiki/server/index.cjs.map +1 -1
- package/dist/workspace-setup/index.js.map +1 -1
- package/package.json +9 -8
- package/dist/discovery-CwBXvtyn.cjs.map +0 -1
- package/dist/dist-BzoA9pDR.js.map +0 -1
- package/dist/dist-Gj7ygaW2.cjs.map +0 -1
- package/dist/errorMessage-BQNpSYdT.js +0 -1
- package/dist/errorMessage-CT-va_Pv.cjs +0 -1
- package/dist/errors-CdXsfFCF.cjs.map +0 -1
- package/dist/listen-BXthb-9C.cjs.map +0 -1
- package/dist/notifier-B65Mvngb.cjs.map +0 -1
- package/dist/notifier-vAp5t6wL.js.map +0 -1
- package/dist/promptSafety-B5uGroik.js.map +0 -1
- package/dist/promptSafety-BGV1wrE6.cjs.map +0 -1
- package/dist/remote-host-Cjox2F2J.js.map +0 -1
- package/dist/remote-host-DhAHBYzN.cjs.map +0 -1
- package/dist/server-CLhww8NL.cjs.map +0 -1
- package/dist/server-CTkTHqEL.js.map +0 -1
package/dist/artifacts/paths.cjs
CHANGED
|
@@ -1,74 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let gui_chat_protocol = require("gui-chat-protocol");
|
|
3
|
+
let _gui_chat_plugin_common = require("@gui-chat-plugin/common");
|
|
2
4
|
//#region src/artifacts/paths.ts
|
|
3
|
-
var MAX_SLUG_LEN = 120;
|
|
4
|
-
/** The workspace directory every artifact lives under (`<workspace>/artifacts`). */
|
|
5
|
-
var ARTIFACTS_ROOT = "artifacts";
|
|
6
|
-
/**
|
|
7
|
-
* Lowercase-ASCII slug for a throwaway, timestamped artifact filename. Empty,
|
|
8
|
-
* whitespace-only, and non-ASCII-only titles fall back to `fallback`. Capped at
|
|
9
|
-
* 120 chars so a long LLM title can't blow the filesystem's NAME_MAX.
|
|
10
|
-
*
|
|
11
|
-
* Leading/trailing hyphens are stripped with a linear scan rather than a regex
|
|
12
|
-
* like `/^-+|-+$/` — CodeQL flags the trailing-anchor form as polynomial
|
|
13
|
-
* backtracking on the attacker-influenced (LLM-provided) title. Strip → cap →
|
|
14
|
-
* strip so a cut at the 120-char boundary can't re-expose a trailing hyphen.
|
|
15
|
-
*/
|
|
16
|
-
function slugifyArtifact(title, fallback) {
|
|
17
|
-
if (!title) return fallback;
|
|
18
|
-
const collapsed = title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
19
|
-
let start = 0;
|
|
20
|
-
let end = collapsed.length;
|
|
21
|
-
while (start < end && collapsed[start] === "-") start += 1;
|
|
22
|
-
while (end > start && collapsed[end - 1] === "-") end -= 1;
|
|
23
|
-
if (end - start > MAX_SLUG_LEN) end = start + MAX_SLUG_LEN;
|
|
24
|
-
while (end > start && collapsed[end - 1] === "-") end -= 1;
|
|
25
|
-
return collapsed.slice(start, end) || fallback;
|
|
26
|
-
}
|
|
27
|
-
/** UTC `YYYY/MM` partition (matches the host's #764 artifact sharding). UTC —
|
|
28
|
-
* not local — so a workspace synced across timezones stays in one bucket. */
|
|
29
|
-
function yearMonthUtc(now = /* @__PURE__ */ new Date()) {
|
|
30
|
-
return `${now.getUTCFullYear()}/${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* FileOps-relative artifact path: `<dir>[/YYYY/MM]/<slug>-<epochMs>[-<suffix>]<ext>`.
|
|
34
|
-
* This is what `files.artifacts.write` takes; prefix it with
|
|
35
|
-
* `toWorkspaceArtifactPath` for the workspace-relative form shown to the LLM.
|
|
36
|
-
*
|
|
37
|
-
* `<epochMs>` alone separates paths built at different times, but NOT two calls
|
|
38
|
-
* with the same title inside one millisecond — those produce the same name, and
|
|
39
|
-
* the second write silently replaces the first artifact. Pass `suffix` (a short
|
|
40
|
-
* random token, as `shapeArtifactPath` does) wherever concurrent callers can
|
|
41
|
-
* share a title.
|
|
42
|
-
*/
|
|
43
|
-
function buildArtifactRelPath(params) {
|
|
44
|
-
const { dir, title, ext, fallback, now = /* @__PURE__ */ new Date(), partitioned = true, suffix } = params;
|
|
45
|
-
const stamp = suffix ? `${now.getTime()}-${suffix}` : `${now.getTime()}`;
|
|
46
|
-
const fileName = `${slugifyArtifact(title, fallback)}-${stamp}${ext}`;
|
|
47
|
-
return (partitioned ? [
|
|
48
|
-
dir,
|
|
49
|
-
yearMonthUtc(now),
|
|
50
|
-
fileName
|
|
51
|
-
] : [dir, fileName]).join("/");
|
|
52
|
-
}
|
|
53
|
-
/** Prefix a FileOps-relative artifact path with the workspace `artifacts/` root. */
|
|
54
|
-
function toWorkspaceArtifactPath(relPath) {
|
|
55
|
-
return `${ARTIFACTS_ROOT}/${relPath}`;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* True when any `/` or `\`-separated segment of `value` is empty (`//`,
|
|
59
|
-
* leading/trailing slash), `.`, or `..`. The lexical traversal / non-canonical
|
|
60
|
-
* guard every artifact path check shares — so a workspace-escape judgement
|
|
61
|
-
* can't drift between plugins.
|
|
62
|
-
*
|
|
63
|
-
* Both separators, matching `classifyFilePath`: an artifact path is minted from
|
|
64
|
-
* a slug and so never contains a backslash, while `node:path` on Windows treats
|
|
65
|
-
* one as a separator. Splitting on `/` alone accepted
|
|
66
|
-
* `artifacts/shapes/..\..\secrets.shape` — canonical to this check, traversal
|
|
67
|
-
* to `path.join` (codex on #3056).
|
|
68
|
-
*/
|
|
69
|
-
function hasUnsafePathSegment(value) {
|
|
70
|
-
return value.split(/[/\\]/).some((seg) => seg === "" || seg === "." || seg === "..");
|
|
71
|
-
}
|
|
72
5
|
var WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\/]/;
|
|
73
6
|
/** True when `value` names a location that does not depend on a base directory.
|
|
74
7
|
* Exported so a URL builder and a path resolver cannot disagree about which
|
|
@@ -110,14 +43,44 @@ function hasDotfileSegment(value) {
|
|
|
110
43
|
return value.split(/[/\\]/).some((segment) => segment.startsWith("."));
|
|
111
44
|
}
|
|
112
45
|
//#endregion
|
|
113
|
-
exports
|
|
114
|
-
|
|
46
|
+
Object.defineProperty(exports, "ARTIFACTS_ROOT", {
|
|
47
|
+
enumerable: true,
|
|
48
|
+
get: function() {
|
|
49
|
+
return gui_chat_protocol.ARTIFACTS_ROOT;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
Object.defineProperty(exports, "buildArtifactRelPath", {
|
|
53
|
+
enumerable: true,
|
|
54
|
+
get: function() {
|
|
55
|
+
return _gui_chat_plugin_common.buildArtifactRelPath;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
115
58
|
exports.classifyFilePath = classifyFilePath;
|
|
116
59
|
exports.hasDotfileSegment = hasDotfileSegment;
|
|
117
|
-
exports
|
|
60
|
+
Object.defineProperty(exports, "hasUnsafePathSegment", {
|
|
61
|
+
enumerable: true,
|
|
62
|
+
get: function() {
|
|
63
|
+
return _gui_chat_plugin_common.hasUnsafePathSegment;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
118
66
|
exports.isAbsoluteFilePathValue = isAbsoluteFilePathValue;
|
|
119
|
-
exports
|
|
120
|
-
|
|
121
|
-
|
|
67
|
+
Object.defineProperty(exports, "slugifyArtifact", {
|
|
68
|
+
enumerable: true,
|
|
69
|
+
get: function() {
|
|
70
|
+
return _gui_chat_plugin_common.slugifyArtifact;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
Object.defineProperty(exports, "toWorkspaceArtifactPath", {
|
|
74
|
+
enumerable: true,
|
|
75
|
+
get: function() {
|
|
76
|
+
return _gui_chat_plugin_common.toWorkspaceArtifactPath;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
Object.defineProperty(exports, "yearMonthUtc", {
|
|
80
|
+
enumerable: true,
|
|
81
|
+
get: function() {
|
|
82
|
+
return _gui_chat_plugin_common.yearMonthUtc;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
122
85
|
|
|
123
86
|
//# sourceMappingURL=paths.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.cjs","names":[],"sources":["../../src/artifacts/paths.ts"],"sourcesContent":["// Shared artifact-path builders for the presentation plugins (chart / html /\n// mulmoscript). Browser-safe by design: no node:path / no node:crypto, so it\n// bundles into both the server core and the browser (`./vue`) plugin entries.\n//\n// These are POSIX artifact *wire paths* — stored in JSON and used as the\n// generic `files.artifacts` FileOps keys — so they must ALWAYS join with `/`\n// regardless of host OS. `path.join` would emit `\\` on Windows and corrupt\n// them; `path.posix.join` would drag in node:path and break the browser\n// bundle. So already-sanitised segments are joined with `/` directly (same\n// choice the plugins made before this module existed).\n\nconst MAX_SLUG_LEN = 120;\n\n/** The workspace directory every artifact lives under (`<workspace>/artifacts`). */\nexport const ARTIFACTS_ROOT = \"artifacts\";\n\n/**\n * Lowercase-ASCII slug for a throwaway, timestamped artifact filename. Empty,\n * whitespace-only, and non-ASCII-only titles fall back to `fallback`. Capped at\n * 120 chars so a long LLM title can't blow the filesystem's NAME_MAX.\n *\n * Leading/trailing hyphens are stripped with a linear scan rather than a regex\n * like `/^-+|-+$/` — CodeQL flags the trailing-anchor form as polynomial\n * backtracking on the attacker-influenced (LLM-provided) title. Strip → cap →\n * strip so a cut at the 120-char boundary can't re-expose a trailing hyphen.\n */\nexport function slugifyArtifact(title: string | undefined, fallback: string): string {\n if (!title) return fallback;\n const collapsed = title.toLowerCase().replace(/[^a-z0-9]+/g, \"-\");\n let start = 0;\n let end = collapsed.length;\n while (start < end && collapsed[start] === \"-\") start += 1;\n while (end > start && collapsed[end - 1] === \"-\") end -= 1;\n if (end - start > MAX_SLUG_LEN) end = start + MAX_SLUG_LEN;\n while (end > start && collapsed[end - 1] === \"-\") end -= 1;\n return collapsed.slice(start, end) || fallback;\n}\n\n/** UTC `YYYY/MM` partition (matches the host's #764 artifact sharding). UTC —\n * not local — so a workspace synced across timezones stays in one bucket. */\nexport function yearMonthUtc(now: Date = new Date()): string {\n const year = now.getUTCFullYear();\n const month = String(now.getUTCMonth() + 1).padStart(2, \"0\");\n return `${year}/${month}`;\n}\n\nexport interface ArtifactRelPathParams {\n /** Artifact-kind directory under the artifacts root (e.g. `charts`, `html`, `stories`). */\n dir: string;\n /** Human title the slug is derived from; empty/non-ASCII falls back to `fallback`. */\n title: string | undefined;\n /** File extension INCLUDING the leading dot (e.g. `.html`, `.chart.json`). */\n ext: string;\n /** Slug used when `title` yields nothing (e.g. `chart`, `page`, `story`). */\n fallback: string;\n now?: Date;\n /** Include the `YYYY/MM` partition segment. Default true; stories opt out. */\n partitioned?: boolean;\n /** Extra token appended after the timestamp, for callers that cannot accept a\n * same-millisecond collision (see the note on `buildArtifactRelPath`). */\n suffix?: string;\n}\n\n/**\n * FileOps-relative artifact path: `<dir>[/YYYY/MM]/<slug>-<epochMs>[-<suffix>]<ext>`.\n * This is what `files.artifacts.write` takes; prefix it with\n * `toWorkspaceArtifactPath` for the workspace-relative form shown to the LLM.\n *\n * `<epochMs>` alone separates paths built at different times, but NOT two calls\n * with the same title inside one millisecond — those produce the same name, and\n * the second write silently replaces the first artifact. Pass `suffix` (a short\n * random token, as `shapeArtifactPath` does) wherever concurrent callers can\n * share a title.\n */\nexport function buildArtifactRelPath(params: ArtifactRelPathParams): string {\n const { dir, title, ext, fallback, now = new Date(), partitioned = true, suffix } = params;\n const stamp = suffix ? `${now.getTime()}-${suffix}` : `${now.getTime()}`;\n const fileName = `${slugifyArtifact(title, fallback)}-${stamp}${ext}`;\n const segments = partitioned ? [dir, yearMonthUtc(now), fileName] : [dir, fileName];\n return segments.join(\"/\");\n}\n\n/** Prefix a FileOps-relative artifact path with the workspace `artifacts/` root. */\nexport function toWorkspaceArtifactPath(relPath: string): string {\n return `${ARTIFACTS_ROOT}/${relPath}`;\n}\n\n/**\n * True when any `/` or `\\`-separated segment of `value` is empty (`//`,\n * leading/trailing slash), `.`, or `..`. The lexical traversal / non-canonical\n * guard every artifact path check shares — so a workspace-escape judgement\n * can't drift between plugins.\n *\n * Both separators, matching `classifyFilePath`: an artifact path is minted from\n * a slug and so never contains a backslash, while `node:path` on Windows treats\n * one as a separator. Splitting on `/` alone accepted\n * `artifacts/shapes/..\\..\\secrets.shape` — canonical to this check, traversal\n * to `path.join` (codex on #3056).\n */\nexport function hasUnsafePathSegment(value: string): boolean {\n return value.split(/[/\\\\]/).some((seg) => seg === \"\" || seg === \".\" || seg === \"..\");\n}\n\n// ── Presentable document paths (presentDocument / presentHtml `path`) ──\n//\n// The two present* tools accept a path to an EXISTING file to display and\n// edit in place. That file is no longer necessarily an artifact the agent\n// wrote: it can be any document in the workspace (MulmoTerminal's workspace\n// IS the git project the user is working in) or, when the host allows it, an\n// absolute path elsewhere on disk.\n//\n// Which of those a value is decides how the host resolves it, so the\n// judgement lives here rather than in each plugin — the plugins may not\n// import one another, and a predicate two of them spell differently is how\n// \"the write site accepts what the refresh site rejects\" bugs start.\n\n/** How a caller-supplied file path must be resolved. `null` = not a usable path. */\nexport type FilePathKind = \"absolute\" | \"relative\";\n\n// `/x`, `C:\\x` / `C:/x`, `\\\\server\\share` (UNC), and the Windows root-relative\n// `\\dir\\x` — which node's `path.resolve` on Windows sends to the drive root, so\n// treating it as relative would mean the classification and the resolution\n// disagreed about where the file is. Windows spellings are recognised on every\n// platform: the value is produced by an LLM or a remote host, not by the local\n// `path` module.\nconst WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\\\/]/;\n\n/** True when `value` names a location that does not depend on a base directory.\n * Exported so a URL builder and a path resolver cannot disagree about which\n * values are rooted. */\nexport function isAbsoluteFilePathValue(value: string): boolean {\n // One leading backslash covers both the UNC `\\\\server\\share` and the Windows\n // root-relative `\\dir\\x`.\n return value.startsWith(\"/\") || value.startsWith(\"\\\\\") || WINDOWS_DRIVE_RE.test(value);\n}\n\n/**\n * Classify a caller-supplied path to a file the host may read and overwrite.\n *\n * Accepts one of `extensions` (compared case-insensitively) and rejects NUL\n * bytes and any `.` / `..` / empty segment — a relative path must be canonical\n * so it can be joined onto a root, and an absolute one must not climb, so\n * neither form can be re-pointed by traversal after the host has vetted it.\n * Returns `\"absolute\"` / `\"relative\"` so the host knows whether to resolve\n * against its workspace root, or `null` when the value is unusable.\n *\n * This is a LEXICAL check only. Existence, file-vs-directory, symlink\n * containment and any host policy about which roots are reachable stay with\n * the host, which is the only layer that can consult the filesystem.\n */\nexport function classifyFilePath(value: string, extensions: readonly string[]): FilePathKind | null {\n if (!value || value.includes(\"\\0\")) return null;\n const lower = value.toLowerCase();\n if (!extensions.some((ext) => lower.endsWith(ext))) return null;\n const absolute = isAbsoluteFilePathValue(value);\n // Split on both separators: `..` must be refused however the value spells it.\n const segments = value.split(/[/\\\\]/);\n // A leading `/` (or drive / UNC prefix) makes the first segment empty by\n // construction — skip those, then require every remaining segment to be a\n // real name.\n const body = absolute ? segments.slice(segments.findIndex((segment) => segment.length > 0)) : segments;\n if (body.length === 0) return null;\n if (body.some((segment) => segment === \"\" || segment === \".\" || segment === \"..\")) return null;\n return absolute ? \"absolute\" : \"relative\";\n}\n\n/** True when any `/` or `\\`-separated segment starts with a dot. The file\n * servers that hand these pages to a browser refuse dotfile segments (the\n * artifact mounts' `dotfiles: \"deny\"` policy), so a `path` argument bearing\n * one can be accepted by a tool and then never render — the gate and the\n * server have to agree on this, hence one definition. */\nexport function hasDotfileSegment(value: string): boolean {\n return value.split(/[/\\\\]/).some((segment) => segment.startsWith(\".\"));\n}\n"],"mappings":";;AAWA,IAAM,eAAe;;AAGrB,IAAa,iBAAiB;;;;;;;;;;;AAY9B,SAAgB,gBAAgB,OAA2B,UAA0B;CACnF,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,YAAY,MAAM,YAAY,CAAC,CAAC,QAAQ,eAAe,GAAG;CAChE,IAAI,QAAQ;CACZ,IAAI,MAAM,UAAU;CACpB,OAAO,QAAQ,OAAO,UAAU,WAAW,KAAK,SAAS;CACzD,OAAO,MAAM,SAAS,UAAU,MAAM,OAAO,KAAK,OAAO;CACzD,IAAI,MAAM,QAAQ,cAAc,MAAM,QAAQ;CAC9C,OAAO,MAAM,SAAS,UAAU,MAAM,OAAO,KAAK,OAAO;CACzD,OAAO,UAAU,MAAM,OAAO,GAAG,KAAK;AACxC;;;AAIA,SAAgB,aAAa,sBAAY,IAAI,KAAK,GAAW;CAG3D,OAAO,GAFM,IAAI,eAEP,EAAK,GADD,OAAO,IAAI,YAAY,IAAI,CAAC,CAAC,CAAC,SAAS,GAAG,GACtC;AACpB;;;;;;;;;;;;AA8BA,SAAgB,qBAAqB,QAAuC;CAC1E,MAAM,EAAE,KAAK,OAAO,KAAK,UAAU,sBAAM,IAAI,KAAK,GAAG,cAAc,MAAM,WAAW;CACpF,MAAM,QAAQ,SAAS,GAAG,IAAI,QAAQ,EAAE,GAAG,WAAW,GAAG,IAAI,QAAQ;CACrE,MAAM,WAAW,GAAG,gBAAgB,OAAO,QAAQ,EAAE,GAAG,QAAQ;CAEhE,QADiB,cAAc;EAAC;EAAK,aAAa,GAAG;EAAG;CAAQ,IAAI,CAAC,KAAK,QAAQ,EAAA,CAClE,KAAK,GAAG;AAC1B;;AAGA,SAAgB,wBAAwB,SAAyB;CAC/D,OAAO,GAAG,eAAe,GAAG;AAC9B;;;;;;;;;;;;;AAcA,SAAgB,qBAAqB,OAAwB;CAC3D,OAAO,MAAM,MAAM,OAAO,CAAC,CAAC,MAAM,QAAQ,QAAQ,MAAM,QAAQ,OAAO,QAAQ,IAAI;AACrF;AAwBA,IAAM,mBAAmB;;;;AAKzB,SAAgB,wBAAwB,OAAwB;CAG9D,OAAO,MAAM,WAAW,GAAG,KAAK,MAAM,WAAW,IAAI,KAAK,iBAAiB,KAAK,KAAK;AACvF;;;;;;;;;;;;;;;AAgBA,SAAgB,iBAAiB,OAAe,YAAoD;CAClG,IAAI,CAAC,SAAS,MAAM,SAAS,IAAI,GAAG,OAAO;CAC3C,MAAM,QAAQ,MAAM,YAAY;CAChC,IAAI,CAAC,WAAW,MAAM,QAAQ,MAAM,SAAS,GAAG,CAAC,GAAG,OAAO;CAC3D,MAAM,WAAW,wBAAwB,KAAK;CAE9C,MAAM,WAAW,MAAM,MAAM,OAAO;CAIpC,MAAM,OAAO,WAAW,SAAS,MAAM,SAAS,WAAW,YAAY,QAAQ,SAAS,CAAC,CAAC,IAAI;CAC9F,IAAI,KAAK,WAAW,GAAG,OAAO;CAC9B,IAAI,KAAK,MAAM,YAAY,YAAY,MAAM,YAAY,OAAO,YAAY,IAAI,GAAG,OAAO;CAC1F,OAAO,WAAW,aAAa;AACjC;;;;;;AAOA,SAAgB,kBAAkB,OAAwB;CACxD,OAAO,MAAM,MAAM,OAAO,CAAC,CAAC,MAAM,YAAY,QAAQ,WAAW,GAAG,CAAC;AACvE"}
|
|
1
|
+
{"version":3,"file":"paths.cjs","names":[],"sources":["../../src/artifacts/paths.ts"],"sourcesContent":["// Shared artifact-path builders for the presentation plugins (chart / html /\n// mulmoscript / markdown) and the host's file stores. Browser-safe by design:\n// no node:path / no node:crypto, so it bundles into both the server core and\n// the browser (`./vue`) plugin entries.\n//\n// The artifact-path half lives upstream so plugins outside this repo share it\n// without importing MulmoClaude: the directory's name is gui-chat-protocol's\n// (`ARTIFACTS_ROOT`, 2.3), and the slug / partition / path builder / traversal\n// guard are `@gui-chat-plugin/common`'s. They are re-exported here so every\n// existing `@mulmoclaude/core/artifacts` import keeps working unchanged.\n//\n// These are POSIX artifact *wire paths* — stored in JSON and used as the\n// generic `files.artifacts` FileOps keys — so they must ALWAYS join with `/`\n// regardless of host OS.\n\nexport { ARTIFACTS_ROOT } from \"gui-chat-protocol\";\nexport {\n buildArtifactRelPath,\n hasUnsafePathSegment,\n slugifyArtifact,\n toWorkspaceArtifactPath,\n yearMonthUtc,\n type ArtifactRelPathParams,\n} from \"@gui-chat-plugin/common\";\n\n// ── Presentable document paths (presentDocument / presentHtml `path`) ──\n//\n// The two present* tools accept a path to an EXISTING file to display and\n// edit in place. That file is no longer necessarily an artifact the agent\n// wrote: it can be any document in the workspace (MulmoTerminal's workspace\n// IS the git project the user is working in) or, when the host allows it, an\n// absolute path elsewhere on disk.\n//\n// Which of those a value is decides how the host resolves it, so the\n// judgement lives here rather than in each plugin — the plugins may not\n// import one another, and a predicate two of them spell differently is how\n// \"the write site accepts what the refresh site rejects\" bugs start.\n\n/** How a caller-supplied file path must be resolved. `null` = not a usable path. */\nexport type FilePathKind = \"absolute\" | \"relative\";\n\n// `/x`, `C:\\x` / `C:/x`, `\\\\server\\share` (UNC), and the Windows root-relative\n// `\\dir\\x` — which node's `path.resolve` on Windows sends to the drive root, so\n// treating it as relative would mean the classification and the resolution\n// disagreed about where the file is. Windows spellings are recognised on every\n// platform: the value is produced by an LLM or a remote host, not by the local\n// `path` module.\nconst WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\\\/]/;\n\n/** True when `value` names a location that does not depend on a base directory.\n * Exported so a URL builder and a path resolver cannot disagree about which\n * values are rooted. */\nexport function isAbsoluteFilePathValue(value: string): boolean {\n // One leading backslash covers both the UNC `\\\\server\\share` and the Windows\n // root-relative `\\dir\\x`.\n return value.startsWith(\"/\") || value.startsWith(\"\\\\\") || WINDOWS_DRIVE_RE.test(value);\n}\n\n/**\n * Classify a caller-supplied path to a file the host may read and overwrite.\n *\n * Accepts one of `extensions` (compared case-insensitively) and rejects NUL\n * bytes and any `.` / `..` / empty segment — a relative path must be canonical\n * so it can be joined onto a root, and an absolute one must not climb, so\n * neither form can be re-pointed by traversal after the host has vetted it.\n * Returns `\"absolute\"` / `\"relative\"` so the host knows whether to resolve\n * against its workspace root, or `null` when the value is unusable.\n *\n * This is a LEXICAL check only. Existence, file-vs-directory, symlink\n * containment and any host policy about which roots are reachable stay with\n * the host, which is the only layer that can consult the filesystem.\n */\nexport function classifyFilePath(value: string, extensions: readonly string[]): FilePathKind | null {\n if (!value || value.includes(\"\\0\")) return null;\n const lower = value.toLowerCase();\n if (!extensions.some((ext) => lower.endsWith(ext))) return null;\n const absolute = isAbsoluteFilePathValue(value);\n // Split on both separators: `..` must be refused however the value spells it.\n const segments = value.split(/[/\\\\]/);\n // A leading `/` (or drive / UNC prefix) makes the first segment empty by\n // construction — skip those, then require every remaining segment to be a\n // real name.\n const body = absolute ? segments.slice(segments.findIndex((segment) => segment.length > 0)) : segments;\n if (body.length === 0) return null;\n if (body.some((segment) => segment === \"\" || segment === \".\" || segment === \"..\")) return null;\n return absolute ? \"absolute\" : \"relative\";\n}\n\n/** True when any `/` or `\\`-separated segment starts with a dot. The file\n * servers that hand these pages to a browser refuse dotfile segments (the\n * artifact mounts' `dotfiles: \"deny\"` policy), so a `path` argument bearing\n * one can be accepted by a tool and then never render — the gate and the\n * server have to agree on this, hence one definition. */\nexport function hasDotfileSegment(value: string): boolean {\n return value.split(/[/\\\\]/).some((segment) => segment.startsWith(\".\"));\n}\n"],"mappings":";;;;AA+CA,IAAM,mBAAmB;;;;AAKzB,SAAgB,wBAAwB,OAAwB;CAG9D,OAAO,MAAM,WAAW,GAAG,KAAK,MAAM,WAAW,IAAI,KAAK,iBAAiB,KAAK,KAAK;AACvF;;;;;;;;;;;;;;;AAgBA,SAAgB,iBAAiB,OAAe,YAAoD;CAClG,IAAI,CAAC,SAAS,MAAM,SAAS,IAAI,GAAG,OAAO;CAC3C,MAAM,QAAQ,MAAM,YAAY;CAChC,IAAI,CAAC,WAAW,MAAM,QAAQ,MAAM,SAAS,GAAG,CAAC,GAAG,OAAO;CAC3D,MAAM,WAAW,wBAAwB,KAAK;CAE9C,MAAM,WAAW,MAAM,MAAM,OAAO;CAIpC,MAAM,OAAO,WAAW,SAAS,MAAM,SAAS,WAAW,YAAY,QAAQ,SAAS,CAAC,CAAC,IAAI;CAC9F,IAAI,KAAK,WAAW,GAAG,OAAO;CAC9B,IAAI,KAAK,MAAM,YAAY,YAAY,MAAM,YAAY,OAAO,YAAY,IAAI,GAAG,OAAO;CAC1F,OAAO,WAAW,aAAa;AACjC;;;;;;AAOA,SAAgB,kBAAkB,OAAwB;CACxD,OAAO,MAAM,MAAM,OAAO,CAAC,CAAC,MAAM,YAAY,QAAQ,WAAW,GAAG,CAAC;AACvE"}
|
|
@@ -1,62 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
/**
|
|
4
|
-
* Lowercase-ASCII slug for a throwaway, timestamped artifact filename. Empty,
|
|
5
|
-
* whitespace-only, and non-ASCII-only titles fall back to `fallback`. Capped at
|
|
6
|
-
* 120 chars so a long LLM title can't blow the filesystem's NAME_MAX.
|
|
7
|
-
*
|
|
8
|
-
* Leading/trailing hyphens are stripped with a linear scan rather than a regex
|
|
9
|
-
* like `/^-+|-+$/` — CodeQL flags the trailing-anchor form as polynomial
|
|
10
|
-
* backtracking on the attacker-influenced (LLM-provided) title. Strip → cap →
|
|
11
|
-
* strip so a cut at the 120-char boundary can't re-expose a trailing hyphen.
|
|
12
|
-
*/
|
|
13
|
-
export declare function slugifyArtifact(title: string | undefined, fallback: string): string;
|
|
14
|
-
/** UTC `YYYY/MM` partition (matches the host's #764 artifact sharding). UTC —
|
|
15
|
-
* not local — so a workspace synced across timezones stays in one bucket. */
|
|
16
|
-
export declare function yearMonthUtc(now?: Date): string;
|
|
17
|
-
export interface ArtifactRelPathParams {
|
|
18
|
-
/** Artifact-kind directory under the artifacts root (e.g. `charts`, `html`, `stories`). */
|
|
19
|
-
dir: string;
|
|
20
|
-
/** Human title the slug is derived from; empty/non-ASCII falls back to `fallback`. */
|
|
21
|
-
title: string | undefined;
|
|
22
|
-
/** File extension INCLUDING the leading dot (e.g. `.html`, `.chart.json`). */
|
|
23
|
-
ext: string;
|
|
24
|
-
/** Slug used when `title` yields nothing (e.g. `chart`, `page`, `story`). */
|
|
25
|
-
fallback: string;
|
|
26
|
-
now?: Date;
|
|
27
|
-
/** Include the `YYYY/MM` partition segment. Default true; stories opt out. */
|
|
28
|
-
partitioned?: boolean;
|
|
29
|
-
/** Extra token appended after the timestamp, for callers that cannot accept a
|
|
30
|
-
* same-millisecond collision (see the note on `buildArtifactRelPath`). */
|
|
31
|
-
suffix?: string;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* FileOps-relative artifact path: `<dir>[/YYYY/MM]/<slug>-<epochMs>[-<suffix>]<ext>`.
|
|
35
|
-
* This is what `files.artifacts.write` takes; prefix it with
|
|
36
|
-
* `toWorkspaceArtifactPath` for the workspace-relative form shown to the LLM.
|
|
37
|
-
*
|
|
38
|
-
* `<epochMs>` alone separates paths built at different times, but NOT two calls
|
|
39
|
-
* with the same title inside one millisecond — those produce the same name, and
|
|
40
|
-
* the second write silently replaces the first artifact. Pass `suffix` (a short
|
|
41
|
-
* random token, as `shapeArtifactPath` does) wherever concurrent callers can
|
|
42
|
-
* share a title.
|
|
43
|
-
*/
|
|
44
|
-
export declare function buildArtifactRelPath(params: ArtifactRelPathParams): string;
|
|
45
|
-
/** Prefix a FileOps-relative artifact path with the workspace `artifacts/` root. */
|
|
46
|
-
export declare function toWorkspaceArtifactPath(relPath: string): string;
|
|
47
|
-
/**
|
|
48
|
-
* True when any `/` or `\`-separated segment of `value` is empty (`//`,
|
|
49
|
-
* leading/trailing slash), `.`, or `..`. The lexical traversal / non-canonical
|
|
50
|
-
* guard every artifact path check shares — so a workspace-escape judgement
|
|
51
|
-
* can't drift between plugins.
|
|
52
|
-
*
|
|
53
|
-
* Both separators, matching `classifyFilePath`: an artifact path is minted from
|
|
54
|
-
* a slug and so never contains a backslash, while `node:path` on Windows treats
|
|
55
|
-
* one as a separator. Splitting on `/` alone accepted
|
|
56
|
-
* `artifacts/shapes/..\..\secrets.shape` — canonical to this check, traversal
|
|
57
|
-
* to `path.join` (codex on #3056).
|
|
58
|
-
*/
|
|
59
|
-
export declare function hasUnsafePathSegment(value: string): boolean;
|
|
1
|
+
export { ARTIFACTS_ROOT } from 'gui-chat-protocol';
|
|
2
|
+
export { buildArtifactRelPath, hasUnsafePathSegment, slugifyArtifact, toWorkspaceArtifactPath, yearMonthUtc, type ArtifactRelPathParams, } from '@gui-chat-plugin/common';
|
|
60
3
|
/** How a caller-supplied file path must be resolved. `null` = not a usable path. */
|
|
61
4
|
export type FilePathKind = "absolute" | "relative";
|
|
62
5
|
/** True when `value` names a location that does not depend on a base directory.
|
package/dist/artifacts/paths.js
CHANGED
|
@@ -1,73 +1,6 @@
|
|
|
1
|
+
import { ARTIFACTS_ROOT } from "gui-chat-protocol";
|
|
2
|
+
import { buildArtifactRelPath, hasUnsafePathSegment, slugifyArtifact, toWorkspaceArtifactPath, yearMonthUtc } from "@gui-chat-plugin/common";
|
|
1
3
|
//#region src/artifacts/paths.ts
|
|
2
|
-
var MAX_SLUG_LEN = 120;
|
|
3
|
-
/** The workspace directory every artifact lives under (`<workspace>/artifacts`). */
|
|
4
|
-
var ARTIFACTS_ROOT = "artifacts";
|
|
5
|
-
/**
|
|
6
|
-
* Lowercase-ASCII slug for a throwaway, timestamped artifact filename. Empty,
|
|
7
|
-
* whitespace-only, and non-ASCII-only titles fall back to `fallback`. Capped at
|
|
8
|
-
* 120 chars so a long LLM title can't blow the filesystem's NAME_MAX.
|
|
9
|
-
*
|
|
10
|
-
* Leading/trailing hyphens are stripped with a linear scan rather than a regex
|
|
11
|
-
* like `/^-+|-+$/` — CodeQL flags the trailing-anchor form as polynomial
|
|
12
|
-
* backtracking on the attacker-influenced (LLM-provided) title. Strip → cap →
|
|
13
|
-
* strip so a cut at the 120-char boundary can't re-expose a trailing hyphen.
|
|
14
|
-
*/
|
|
15
|
-
function slugifyArtifact(title, fallback) {
|
|
16
|
-
if (!title) return fallback;
|
|
17
|
-
const collapsed = title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
18
|
-
let start = 0;
|
|
19
|
-
let end = collapsed.length;
|
|
20
|
-
while (start < end && collapsed[start] === "-") start += 1;
|
|
21
|
-
while (end > start && collapsed[end - 1] === "-") end -= 1;
|
|
22
|
-
if (end - start > MAX_SLUG_LEN) end = start + MAX_SLUG_LEN;
|
|
23
|
-
while (end > start && collapsed[end - 1] === "-") end -= 1;
|
|
24
|
-
return collapsed.slice(start, end) || fallback;
|
|
25
|
-
}
|
|
26
|
-
/** UTC `YYYY/MM` partition (matches the host's #764 artifact sharding). UTC —
|
|
27
|
-
* not local — so a workspace synced across timezones stays in one bucket. */
|
|
28
|
-
function yearMonthUtc(now = /* @__PURE__ */ new Date()) {
|
|
29
|
-
return `${now.getUTCFullYear()}/${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* FileOps-relative artifact path: `<dir>[/YYYY/MM]/<slug>-<epochMs>[-<suffix>]<ext>`.
|
|
33
|
-
* This is what `files.artifacts.write` takes; prefix it with
|
|
34
|
-
* `toWorkspaceArtifactPath` for the workspace-relative form shown to the LLM.
|
|
35
|
-
*
|
|
36
|
-
* `<epochMs>` alone separates paths built at different times, but NOT two calls
|
|
37
|
-
* with the same title inside one millisecond — those produce the same name, and
|
|
38
|
-
* the second write silently replaces the first artifact. Pass `suffix` (a short
|
|
39
|
-
* random token, as `shapeArtifactPath` does) wherever concurrent callers can
|
|
40
|
-
* share a title.
|
|
41
|
-
*/
|
|
42
|
-
function buildArtifactRelPath(params) {
|
|
43
|
-
const { dir, title, ext, fallback, now = /* @__PURE__ */ new Date(), partitioned = true, suffix } = params;
|
|
44
|
-
const stamp = suffix ? `${now.getTime()}-${suffix}` : `${now.getTime()}`;
|
|
45
|
-
const fileName = `${slugifyArtifact(title, fallback)}-${stamp}${ext}`;
|
|
46
|
-
return (partitioned ? [
|
|
47
|
-
dir,
|
|
48
|
-
yearMonthUtc(now),
|
|
49
|
-
fileName
|
|
50
|
-
] : [dir, fileName]).join("/");
|
|
51
|
-
}
|
|
52
|
-
/** Prefix a FileOps-relative artifact path with the workspace `artifacts/` root. */
|
|
53
|
-
function toWorkspaceArtifactPath(relPath) {
|
|
54
|
-
return `${ARTIFACTS_ROOT}/${relPath}`;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* True when any `/` or `\`-separated segment of `value` is empty (`//`,
|
|
58
|
-
* leading/trailing slash), `.`, or `..`. The lexical traversal / non-canonical
|
|
59
|
-
* guard every artifact path check shares — so a workspace-escape judgement
|
|
60
|
-
* can't drift between plugins.
|
|
61
|
-
*
|
|
62
|
-
* Both separators, matching `classifyFilePath`: an artifact path is minted from
|
|
63
|
-
* a slug and so never contains a backslash, while `node:path` on Windows treats
|
|
64
|
-
* one as a separator. Splitting on `/` alone accepted
|
|
65
|
-
* `artifacts/shapes/..\..\secrets.shape` — canonical to this check, traversal
|
|
66
|
-
* to `path.join` (codex on #3056).
|
|
67
|
-
*/
|
|
68
|
-
function hasUnsafePathSegment(value) {
|
|
69
|
-
return value.split(/[/\\]/).some((seg) => seg === "" || seg === "." || seg === "..");
|
|
70
|
-
}
|
|
71
4
|
var WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\/]/;
|
|
72
5
|
/** True when `value` names a location that does not depend on a base directory.
|
|
73
6
|
* Exported so a URL builder and a path resolver cannot disagree about which
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.js","names":[],"sources":["../../src/artifacts/paths.ts"],"sourcesContent":["// Shared artifact-path builders for the presentation plugins (chart / html /\n// mulmoscript). Browser-safe by design: no node:path / no node:crypto, so it\n// bundles into both the server core and the browser (`./vue`) plugin entries.\n//\n// These are POSIX artifact *wire paths* — stored in JSON and used as the\n// generic `files.artifacts` FileOps keys — so they must ALWAYS join with `/`\n// regardless of host OS. `path.join` would emit `\\` on Windows and corrupt\n// them; `path.posix.join` would drag in node:path and break the browser\n// bundle. So already-sanitised segments are joined with `/` directly (same\n// choice the plugins made before this module existed).\n\nconst MAX_SLUG_LEN = 120;\n\n/** The workspace directory every artifact lives under (`<workspace>/artifacts`). */\nexport const ARTIFACTS_ROOT = \"artifacts\";\n\n/**\n * Lowercase-ASCII slug for a throwaway, timestamped artifact filename. Empty,\n * whitespace-only, and non-ASCII-only titles fall back to `fallback`. Capped at\n * 120 chars so a long LLM title can't blow the filesystem's NAME_MAX.\n *\n * Leading/trailing hyphens are stripped with a linear scan rather than a regex\n * like `/^-+|-+$/` — CodeQL flags the trailing-anchor form as polynomial\n * backtracking on the attacker-influenced (LLM-provided) title. Strip → cap →\n * strip so a cut at the 120-char boundary can't re-expose a trailing hyphen.\n */\nexport function slugifyArtifact(title: string | undefined, fallback: string): string {\n if (!title) return fallback;\n const collapsed = title.toLowerCase().replace(/[^a-z0-9]+/g, \"-\");\n let start = 0;\n let end = collapsed.length;\n while (start < end && collapsed[start] === \"-\") start += 1;\n while (end > start && collapsed[end - 1] === \"-\") end -= 1;\n if (end - start > MAX_SLUG_LEN) end = start + MAX_SLUG_LEN;\n while (end > start && collapsed[end - 1] === \"-\") end -= 1;\n return collapsed.slice(start, end) || fallback;\n}\n\n/** UTC `YYYY/MM` partition (matches the host's #764 artifact sharding). UTC —\n * not local — so a workspace synced across timezones stays in one bucket. */\nexport function yearMonthUtc(now: Date = new Date()): string {\n const year = now.getUTCFullYear();\n const month = String(now.getUTCMonth() + 1).padStart(2, \"0\");\n return `${year}/${month}`;\n}\n\nexport interface ArtifactRelPathParams {\n /** Artifact-kind directory under the artifacts root (e.g. `charts`, `html`, `stories`). */\n dir: string;\n /** Human title the slug is derived from; empty/non-ASCII falls back to `fallback`. */\n title: string | undefined;\n /** File extension INCLUDING the leading dot (e.g. `.html`, `.chart.json`). */\n ext: string;\n /** Slug used when `title` yields nothing (e.g. `chart`, `page`, `story`). */\n fallback: string;\n now?: Date;\n /** Include the `YYYY/MM` partition segment. Default true; stories opt out. */\n partitioned?: boolean;\n /** Extra token appended after the timestamp, for callers that cannot accept a\n * same-millisecond collision (see the note on `buildArtifactRelPath`). */\n suffix?: string;\n}\n\n/**\n * FileOps-relative artifact path: `<dir>[/YYYY/MM]/<slug>-<epochMs>[-<suffix>]<ext>`.\n * This is what `files.artifacts.write` takes; prefix it with\n * `toWorkspaceArtifactPath` for the workspace-relative form shown to the LLM.\n *\n * `<epochMs>` alone separates paths built at different times, but NOT two calls\n * with the same title inside one millisecond — those produce the same name, and\n * the second write silently replaces the first artifact. Pass `suffix` (a short\n * random token, as `shapeArtifactPath` does) wherever concurrent callers can\n * share a title.\n */\nexport function buildArtifactRelPath(params: ArtifactRelPathParams): string {\n const { dir, title, ext, fallback, now = new Date(), partitioned = true, suffix } = params;\n const stamp = suffix ? `${now.getTime()}-${suffix}` : `${now.getTime()}`;\n const fileName = `${slugifyArtifact(title, fallback)}-${stamp}${ext}`;\n const segments = partitioned ? [dir, yearMonthUtc(now), fileName] : [dir, fileName];\n return segments.join(\"/\");\n}\n\n/** Prefix a FileOps-relative artifact path with the workspace `artifacts/` root. */\nexport function toWorkspaceArtifactPath(relPath: string): string {\n return `${ARTIFACTS_ROOT}/${relPath}`;\n}\n\n/**\n * True when any `/` or `\\`-separated segment of `value` is empty (`//`,\n * leading/trailing slash), `.`, or `..`. The lexical traversal / non-canonical\n * guard every artifact path check shares — so a workspace-escape judgement\n * can't drift between plugins.\n *\n * Both separators, matching `classifyFilePath`: an artifact path is minted from\n * a slug and so never contains a backslash, while `node:path` on Windows treats\n * one as a separator. Splitting on `/` alone accepted\n * `artifacts/shapes/..\\..\\secrets.shape` — canonical to this check, traversal\n * to `path.join` (codex on #3056).\n */\nexport function hasUnsafePathSegment(value: string): boolean {\n return value.split(/[/\\\\]/).some((seg) => seg === \"\" || seg === \".\" || seg === \"..\");\n}\n\n// ── Presentable document paths (presentDocument / presentHtml `path`) ──\n//\n// The two present* tools accept a path to an EXISTING file to display and\n// edit in place. That file is no longer necessarily an artifact the agent\n// wrote: it can be any document in the workspace (MulmoTerminal's workspace\n// IS the git project the user is working in) or, when the host allows it, an\n// absolute path elsewhere on disk.\n//\n// Which of those a value is decides how the host resolves it, so the\n// judgement lives here rather than in each plugin — the plugins may not\n// import one another, and a predicate two of them spell differently is how\n// \"the write site accepts what the refresh site rejects\" bugs start.\n\n/** How a caller-supplied file path must be resolved. `null` = not a usable path. */\nexport type FilePathKind = \"absolute\" | \"relative\";\n\n// `/x`, `C:\\x` / `C:/x`, `\\\\server\\share` (UNC), and the Windows root-relative\n// `\\dir\\x` — which node's `path.resolve` on Windows sends to the drive root, so\n// treating it as relative would mean the classification and the resolution\n// disagreed about where the file is. Windows spellings are recognised on every\n// platform: the value is produced by an LLM or a remote host, not by the local\n// `path` module.\nconst WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\\\/]/;\n\n/** True when `value` names a location that does not depend on a base directory.\n * Exported so a URL builder and a path resolver cannot disagree about which\n * values are rooted. */\nexport function isAbsoluteFilePathValue(value: string): boolean {\n // One leading backslash covers both the UNC `\\\\server\\share` and the Windows\n // root-relative `\\dir\\x`.\n return value.startsWith(\"/\") || value.startsWith(\"\\\\\") || WINDOWS_DRIVE_RE.test(value);\n}\n\n/**\n * Classify a caller-supplied path to a file the host may read and overwrite.\n *\n * Accepts one of `extensions` (compared case-insensitively) and rejects NUL\n * bytes and any `.` / `..` / empty segment — a relative path must be canonical\n * so it can be joined onto a root, and an absolute one must not climb, so\n * neither form can be re-pointed by traversal after the host has vetted it.\n * Returns `\"absolute\"` / `\"relative\"` so the host knows whether to resolve\n * against its workspace root, or `null` when the value is unusable.\n *\n * This is a LEXICAL check only. Existence, file-vs-directory, symlink\n * containment and any host policy about which roots are reachable stay with\n * the host, which is the only layer that can consult the filesystem.\n */\nexport function classifyFilePath(value: string, extensions: readonly string[]): FilePathKind | null {\n if (!value || value.includes(\"\\0\")) return null;\n const lower = value.toLowerCase();\n if (!extensions.some((ext) => lower.endsWith(ext))) return null;\n const absolute = isAbsoluteFilePathValue(value);\n // Split on both separators: `..` must be refused however the value spells it.\n const segments = value.split(/[/\\\\]/);\n // A leading `/` (or drive / UNC prefix) makes the first segment empty by\n // construction — skip those, then require every remaining segment to be a\n // real name.\n const body = absolute ? segments.slice(segments.findIndex((segment) => segment.length > 0)) : segments;\n if (body.length === 0) return null;\n if (body.some((segment) => segment === \"\" || segment === \".\" || segment === \"..\")) return null;\n return absolute ? \"absolute\" : \"relative\";\n}\n\n/** True when any `/` or `\\`-separated segment starts with a dot. The file\n * servers that hand these pages to a browser refuse dotfile segments (the\n * artifact mounts' `dotfiles: \"deny\"` policy), so a `path` argument bearing\n * one can be accepted by a tool and then never render — the gate and the\n * server have to agree on this, hence one definition. */\nexport function hasDotfileSegment(value: string): boolean {\n return value.split(/[/\\\\]/).some((segment) => segment.startsWith(\".\"));\n}\n"],"mappings":";AAWA,IAAM,eAAe;;AAGrB,IAAa,iBAAiB;;;;;;;;;;;AAY9B,SAAgB,gBAAgB,OAA2B,UAA0B;CACnF,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,YAAY,MAAM,YAAY,CAAC,CAAC,QAAQ,eAAe,GAAG;CAChE,IAAI,QAAQ;CACZ,IAAI,MAAM,UAAU;CACpB,OAAO,QAAQ,OAAO,UAAU,WAAW,KAAK,SAAS;CACzD,OAAO,MAAM,SAAS,UAAU,MAAM,OAAO,KAAK,OAAO;CACzD,IAAI,MAAM,QAAQ,cAAc,MAAM,QAAQ;CAC9C,OAAO,MAAM,SAAS,UAAU,MAAM,OAAO,KAAK,OAAO;CACzD,OAAO,UAAU,MAAM,OAAO,GAAG,KAAK;AACxC;;;AAIA,SAAgB,aAAa,sBAAY,IAAI,KAAK,GAAW;CAG3D,OAAO,GAFM,IAAI,eAEP,EAAK,GADD,OAAO,IAAI,YAAY,IAAI,CAAC,CAAC,CAAC,SAAS,GAAG,GACtC;AACpB;;;;;;;;;;;;AA8BA,SAAgB,qBAAqB,QAAuC;CAC1E,MAAM,EAAE,KAAK,OAAO,KAAK,UAAU,sBAAM,IAAI,KAAK,GAAG,cAAc,MAAM,WAAW;CACpF,MAAM,QAAQ,SAAS,GAAG,IAAI,QAAQ,EAAE,GAAG,WAAW,GAAG,IAAI,QAAQ;CACrE,MAAM,WAAW,GAAG,gBAAgB,OAAO,QAAQ,EAAE,GAAG,QAAQ;CAEhE,QADiB,cAAc;EAAC;EAAK,aAAa,GAAG;EAAG;CAAQ,IAAI,CAAC,KAAK,QAAQ,EAAA,CAClE,KAAK,GAAG;AAC1B;;AAGA,SAAgB,wBAAwB,SAAyB;CAC/D,OAAO,GAAG,eAAe,GAAG;AAC9B;;;;;;;;;;;;;AAcA,SAAgB,qBAAqB,OAAwB;CAC3D,OAAO,MAAM,MAAM,OAAO,CAAC,CAAC,MAAM,QAAQ,QAAQ,MAAM,QAAQ,OAAO,QAAQ,IAAI;AACrF;AAwBA,IAAM,mBAAmB;;;;AAKzB,SAAgB,wBAAwB,OAAwB;CAG9D,OAAO,MAAM,WAAW,GAAG,KAAK,MAAM,WAAW,IAAI,KAAK,iBAAiB,KAAK,KAAK;AACvF;;;;;;;;;;;;;;;AAgBA,SAAgB,iBAAiB,OAAe,YAAoD;CAClG,IAAI,CAAC,SAAS,MAAM,SAAS,IAAI,GAAG,OAAO;CAC3C,MAAM,QAAQ,MAAM,YAAY;CAChC,IAAI,CAAC,WAAW,MAAM,QAAQ,MAAM,SAAS,GAAG,CAAC,GAAG,OAAO;CAC3D,MAAM,WAAW,wBAAwB,KAAK;CAE9C,MAAM,WAAW,MAAM,MAAM,OAAO;CAIpC,MAAM,OAAO,WAAW,SAAS,MAAM,SAAS,WAAW,YAAY,QAAQ,SAAS,CAAC,CAAC,IAAI;CAC9F,IAAI,KAAK,WAAW,GAAG,OAAO;CAC9B,IAAI,KAAK,MAAM,YAAY,YAAY,MAAM,YAAY,OAAO,YAAY,IAAI,GAAG,OAAO;CAC1F,OAAO,WAAW,aAAa;AACjC;;;;;;AAOA,SAAgB,kBAAkB,OAAwB;CACxD,OAAO,MAAM,MAAM,OAAO,CAAC,CAAC,MAAM,YAAY,QAAQ,WAAW,GAAG,CAAC;AACvE"}
|
|
1
|
+
{"version":3,"file":"paths.js","names":[],"sources":["../../src/artifacts/paths.ts"],"sourcesContent":["// Shared artifact-path builders for the presentation plugins (chart / html /\n// mulmoscript / markdown) and the host's file stores. Browser-safe by design:\n// no node:path / no node:crypto, so it bundles into both the server core and\n// the browser (`./vue`) plugin entries.\n//\n// The artifact-path half lives upstream so plugins outside this repo share it\n// without importing MulmoClaude: the directory's name is gui-chat-protocol's\n// (`ARTIFACTS_ROOT`, 2.3), and the slug / partition / path builder / traversal\n// guard are `@gui-chat-plugin/common`'s. They are re-exported here so every\n// existing `@mulmoclaude/core/artifacts` import keeps working unchanged.\n//\n// These are POSIX artifact *wire paths* — stored in JSON and used as the\n// generic `files.artifacts` FileOps keys — so they must ALWAYS join with `/`\n// regardless of host OS.\n\nexport { ARTIFACTS_ROOT } from \"gui-chat-protocol\";\nexport {\n buildArtifactRelPath,\n hasUnsafePathSegment,\n slugifyArtifact,\n toWorkspaceArtifactPath,\n yearMonthUtc,\n type ArtifactRelPathParams,\n} from \"@gui-chat-plugin/common\";\n\n// ── Presentable document paths (presentDocument / presentHtml `path`) ──\n//\n// The two present* tools accept a path to an EXISTING file to display and\n// edit in place. That file is no longer necessarily an artifact the agent\n// wrote: it can be any document in the workspace (MulmoTerminal's workspace\n// IS the git project the user is working in) or, when the host allows it, an\n// absolute path elsewhere on disk.\n//\n// Which of those a value is decides how the host resolves it, so the\n// judgement lives here rather than in each plugin — the plugins may not\n// import one another, and a predicate two of them spell differently is how\n// \"the write site accepts what the refresh site rejects\" bugs start.\n\n/** How a caller-supplied file path must be resolved. `null` = not a usable path. */\nexport type FilePathKind = \"absolute\" | \"relative\";\n\n// `/x`, `C:\\x` / `C:/x`, `\\\\server\\share` (UNC), and the Windows root-relative\n// `\\dir\\x` — which node's `path.resolve` on Windows sends to the drive root, so\n// treating it as relative would mean the classification and the resolution\n// disagreed about where the file is. Windows spellings are recognised on every\n// platform: the value is produced by an LLM or a remote host, not by the local\n// `path` module.\nconst WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\\\/]/;\n\n/** True when `value` names a location that does not depend on a base directory.\n * Exported so a URL builder and a path resolver cannot disagree about which\n * values are rooted. */\nexport function isAbsoluteFilePathValue(value: string): boolean {\n // One leading backslash covers both the UNC `\\\\server\\share` and the Windows\n // root-relative `\\dir\\x`.\n return value.startsWith(\"/\") || value.startsWith(\"\\\\\") || WINDOWS_DRIVE_RE.test(value);\n}\n\n/**\n * Classify a caller-supplied path to a file the host may read and overwrite.\n *\n * Accepts one of `extensions` (compared case-insensitively) and rejects NUL\n * bytes and any `.` / `..` / empty segment — a relative path must be canonical\n * so it can be joined onto a root, and an absolute one must not climb, so\n * neither form can be re-pointed by traversal after the host has vetted it.\n * Returns `\"absolute\"` / `\"relative\"` so the host knows whether to resolve\n * against its workspace root, or `null` when the value is unusable.\n *\n * This is a LEXICAL check only. Existence, file-vs-directory, symlink\n * containment and any host policy about which roots are reachable stay with\n * the host, which is the only layer that can consult the filesystem.\n */\nexport function classifyFilePath(value: string, extensions: readonly string[]): FilePathKind | null {\n if (!value || value.includes(\"\\0\")) return null;\n const lower = value.toLowerCase();\n if (!extensions.some((ext) => lower.endsWith(ext))) return null;\n const absolute = isAbsoluteFilePathValue(value);\n // Split on both separators: `..` must be refused however the value spells it.\n const segments = value.split(/[/\\\\]/);\n // A leading `/` (or drive / UNC prefix) makes the first segment empty by\n // construction — skip those, then require every remaining segment to be a\n // real name.\n const body = absolute ? segments.slice(segments.findIndex((segment) => segment.length > 0)) : segments;\n if (body.length === 0) return null;\n if (body.some((segment) => segment === \"\" || segment === \".\" || segment === \"..\")) return null;\n return absolute ? \"absolute\" : \"relative\";\n}\n\n/** True when any `/` or `\\`-separated segment starts with a dot. The file\n * servers that hand these pages to a browser refuse dotfile segments (the\n * artifact mounts' `dotfiles: \"deny\"` policy), so a `path` argument bearing\n * one can be accepted by a tool and then never render — the gate and the\n * server have to agree on this, hence one definition. */\nexport function hasDotfileSegment(value: string): boolean {\n return value.split(/[/\\\\]/).some((segment) => segment.startsWith(\".\"));\n}\n"],"mappings":";;;AA+CA,IAAM,mBAAmB;;;;AAKzB,SAAgB,wBAAwB,OAAwB;CAG9D,OAAO,MAAM,WAAW,GAAG,KAAK,MAAM,WAAW,IAAI,KAAK,iBAAiB,KAAK,KAAK;AACvF;;;;;;;;;;;;;;;AAgBA,SAAgB,iBAAiB,OAAe,YAAoD;CAClG,IAAI,CAAC,SAAS,MAAM,SAAS,IAAI,GAAG,OAAO;CAC3C,MAAM,QAAQ,MAAM,YAAY;CAChC,IAAI,CAAC,WAAW,MAAM,QAAQ,MAAM,SAAS,GAAG,CAAC,GAAG,OAAO;CAC3D,MAAM,WAAW,wBAAwB,KAAK;CAE9C,MAAM,WAAW,MAAM,MAAM,OAAO;CAIpC,MAAM,OAAO,WAAW,SAAS,MAAM,SAAS,WAAW,YAAY,QAAQ,SAAS,CAAC,CAAC,IAAI;CAC9F,IAAI,KAAK,WAAW,GAAG,OAAO;CAC9B,IAAI,KAAK,MAAM,YAAY,YAAY,MAAM,YAAY,OAAO,YAAY,IAAI,GAAG,OAAO;CAC1F,OAAO,WAAW,aAAa;AACjC;;;;;;AAOA,SAAgB,kBAAkB,OAAwB;CACxD,OAAO,MAAM,MAAM,OAAO,CAAC,CAAC,MAAM,YAAY,QAAQ,WAAW,GAAG,CAAC;AACvE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"firestore.cjs","names":[],"sources":["../../src/collection/server/firestoreDocs.ts"],"sourcesContent":["// The seam between the firestore store and the Firestore SDK.\n//\n// The modular SDK is function-based (`getDocs(query(collection(db, …)))`), so a\n// store that imported those functions directly could not be tested without a\n// real backend — a fake `db` would still be handed to the real functions.\n// Narrowing to this interface makes the backend swappable: core ships\n// `createFirestoreDocs` over the real SDK, and the tests inject an in-memory\n// fake that satisfies the same shape. That is what lets this repository's tests\n// run with no API key and no network.\n//\n// Deliberately minimal and id-keyed: no query builder, no field ordering, no\n// cursors. Everything the store needs is \"the documents of one collection path,\n// ordered by document id\" — see the store header for why field ordering is\n// avoided entirely.\n//\n// The collection path is an ARGUMENT, not something this module composes. It\n// owns the SDK calls; `firestoreStore` owns where the documents live.\n\nimport {\n collection as firestoreCollection,\n doc,\n getDoc,\n getDocs,\n orderBy,\n query as firestoreQuery,\n onSnapshot,\n runTransaction,\n setDoc,\n Timestamp,\n type Firestore,\n} from \"firebase/firestore\";\n\n/** One stored record document. `data` is the document's own fields — the\n * record itself, not a wrapper around it; see `set` below. The store\n * validates its shape, because a document written by hand could be\n * anything. */\nexport interface FirestoreDoc {\n id: string;\n data: unknown;\n}\n\nexport interface FirestoreDocs {\n /** Every document under `collectionPath`, ordered by document id. */\n list: (collectionPath: string) => Promise<FirestoreDoc[]>;\n /** One document's fields, or null when it doesn't exist. */\n get: (collectionPath: string, docId: string) => Promise<unknown | null>;\n /** Create or replace. */\n set: (collectionPath: string, docId: string, data: Record<string, unknown>) => Promise<void>;\n /** Create only. Returns false when the id already exists — atomic, so two\n * concurrent creates can't both observe \"missing\". */\n create: (collectionPath: string, docId: string, data: Record<string, unknown>) => Promise<boolean>;\n /** Delete. Returns false when the id didn't exist, so a caller can tell a\n * real delete from a typo'd id. */\n delete: (collectionPath: string, docId: string) => Promise<boolean>;\n /** Listen to `collectionPath`. Every snapshot reports the ids that changed\n * in it, and whether it is the FIRST one.\n *\n * `initial` is not a nicety. `onSnapshot` delivers the current contents\n * immediately, as one snapshot in which every existing document reads as\n * `added` — so a listener that treats snapshots uniformly announces the\n * whole collection as changed the moment it arms, on every mount. The flag\n * is passed up rather than swallowed here so the decision (and its test)\n * lives with the store's policy, next to the rest of it.\n *\n * `onError` fires at most once per subscription: a Firestore listen error\n * TERMINATES the listener and never recovers on its own. Re-subscribing is\n * the caller's job (`firestore/listen.ts` holds the policy).\n *\n * Returns the detach function synchronously — `onSnapshot` is not async. */\n watch: (collectionPath: string, onChanged: (ids: string[], meta: { initial: boolean }) => void, onError: (error: unknown) => void) => () => void;\n /** Firestore's own instant, for a field the store must hand back as the type\n * it was stored as.\n *\n * Here rather than in the store because this is the seam that owns the SDK:\n * `firebase` is an OPTIONAL peer, and the store is reachable from\n * `collection/server` (the factory registry in `store.ts` names it), so a\n * top-level `import { Timestamp }` there makes the peer required for every\n * consumer of that entry — including hosts with no Firestore at all. Typed\n * `unknown` so no SDK type crosses back out. */\n timestamp: (seconds: number, nanoseconds: number) => unknown;\n}\n\n/** The real implementation over the modular SDK.\n *\n * THE RECORD IS THE DOCUMENT. Its fields are written at the top level, not\n * nested under a `data` key. This is not a matter of taste: the deployed\n * security rules read `request.resource.data.<field>` and\n * `resource.data[submit.emailField]` — a wrapper would put every field one\n * level down, so the required-field checks, the status state machine and the\n * \"your own row\" predicate would all read absent values and fail closed. A\n * shared record's shape is part of the authorization contract.\n *\n * `orderBy(\"__name__\")` orders by DOCUMENT ID. Ordering by a record field\n * would silently EXCLUDE documents missing that field (the trap\n * `remote-host/server/hostRunner.ts:158-169` documents), turning a read into a\n * partial one; the document id is always present and gives the stable order\n * the store contract requires. */\nexport function createFirestoreDocs(database: Firestore): FirestoreDocs {\n return {\n list: async (collectionPath) => {\n const snapshot = await getDocs(firestoreQuery(firestoreCollection(database, collectionPath), orderBy(\"__name__\")));\n return snapshot.docs.map((entry) => ({ id: entry.id, data: entry.data() }));\n },\n get: async (collectionPath, docId) => {\n const snapshot = await getDoc(doc(database, collectionPath, docId));\n return snapshot.exists() ? snapshot.data() : null;\n },\n set: async (collectionPath, docId, data) => {\n await setDoc(doc(database, collectionPath, docId), data);\n },\n create: (collectionPath, docId, data) =>\n runTransaction(database, async (transaction) => {\n const ref = doc(database, collectionPath, docId);\n const existing = await transaction.get(ref);\n if (existing.exists()) return false;\n transaction.set(ref, data);\n return true;\n }),\n // Firestore's deleteDoc succeeds on a missing document, so an existence\n // check is what makes \"deleted\" distinguishable from \"there was nothing\".\n // It runs INSIDE the transaction (like `create`): a plain get-then-delete\n // would report `true` for a document a concurrent client had already\n // removed, i.e. claim a delete this call never performed.\n delete: (collectionPath, docId) =>\n runTransaction(database, async (transaction) => {\n const ref = doc(database, collectionPath, docId);\n const existing = await transaction.get(ref);\n if (!existing.exists()) return false;\n transaction.delete(ref);\n return true;\n }),\n // `docChanges()` rather than the whole snapshot: it names the documents\n // that moved, which is what lets the store report per-record changes\n // instead of \"something in this collection changed\". Added, modified and\n // removed are all reported the same way — the store's listener takes an\n // id, and re-reads; what KIND of change it was is not something a\n // reconcile pass needs to be told.\n //\n // `includeMetadataChanges` is deliberately left off: local writes and\n // has-pending-writes flips would otherwise wake every listener for\n // changes this process just made.\n watch: (collectionPath, onChanged, onError) => {\n let seen = false;\n return onSnapshot(\n firestoreCollection(database, collectionPath),\n (snapshot) => {\n const initial = !seen;\n seen = true;\n onChanged(\n snapshot.docChanges().map((change) => change.doc.id),\n { initial },\n );\n },\n onError,\n );\n },\n timestamp: (seconds, nanoseconds) => new Timestamp(seconds, nanoseconds),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiGA,SAAgB,oBAAoB,UAAoC;CACtE,OAAO;EACL,MAAM,OAAO,mBAAmB;GAE9B,QAAO,OAAA,
|
|
1
|
+
{"version":3,"file":"firestore.cjs","names":["getDocs","firestoreQuery","firestoreCollection","orderBy","getDoc","doc","setDoc","runTransaction","onSnapshot","Timestamp"],"sources":["../../src/collection/server/firestoreDocs.ts"],"sourcesContent":["// The seam between the firestore store and the Firestore SDK.\n//\n// The modular SDK is function-based (`getDocs(query(collection(db, …)))`), so a\n// store that imported those functions directly could not be tested without a\n// real backend — a fake `db` would still be handed to the real functions.\n// Narrowing to this interface makes the backend swappable: core ships\n// `createFirestoreDocs` over the real SDK, and the tests inject an in-memory\n// fake that satisfies the same shape. That is what lets this repository's tests\n// run with no API key and no network.\n//\n// Deliberately minimal and id-keyed: no query builder, no field ordering, no\n// cursors. Everything the store needs is \"the documents of one collection path,\n// ordered by document id\" — see the store header for why field ordering is\n// avoided entirely.\n//\n// The collection path is an ARGUMENT, not something this module composes. It\n// owns the SDK calls; `firestoreStore` owns where the documents live.\n\nimport {\n collection as firestoreCollection,\n doc,\n getDoc,\n getDocs,\n orderBy,\n query as firestoreQuery,\n onSnapshot,\n runTransaction,\n setDoc,\n Timestamp,\n type Firestore,\n} from \"firebase/firestore\";\n\n/** One stored record document. `data` is the document's own fields — the\n * record itself, not a wrapper around it; see `set` below. The store\n * validates its shape, because a document written by hand could be\n * anything. */\nexport interface FirestoreDoc {\n id: string;\n data: unknown;\n}\n\nexport interface FirestoreDocs {\n /** Every document under `collectionPath`, ordered by document id. */\n list: (collectionPath: string) => Promise<FirestoreDoc[]>;\n /** One document's fields, or null when it doesn't exist. */\n get: (collectionPath: string, docId: string) => Promise<unknown | null>;\n /** Create or replace. */\n set: (collectionPath: string, docId: string, data: Record<string, unknown>) => Promise<void>;\n /** Create only. Returns false when the id already exists — atomic, so two\n * concurrent creates can't both observe \"missing\". */\n create: (collectionPath: string, docId: string, data: Record<string, unknown>) => Promise<boolean>;\n /** Delete. Returns false when the id didn't exist, so a caller can tell a\n * real delete from a typo'd id. */\n delete: (collectionPath: string, docId: string) => Promise<boolean>;\n /** Listen to `collectionPath`. Every snapshot reports the ids that changed\n * in it, and whether it is the FIRST one.\n *\n * `initial` is not a nicety. `onSnapshot` delivers the current contents\n * immediately, as one snapshot in which every existing document reads as\n * `added` — so a listener that treats snapshots uniformly announces the\n * whole collection as changed the moment it arms, on every mount. The flag\n * is passed up rather than swallowed here so the decision (and its test)\n * lives with the store's policy, next to the rest of it.\n *\n * `onError` fires at most once per subscription: a Firestore listen error\n * TERMINATES the listener and never recovers on its own. Re-subscribing is\n * the caller's job (`firestore/listen.ts` holds the policy).\n *\n * Returns the detach function synchronously — `onSnapshot` is not async. */\n watch: (collectionPath: string, onChanged: (ids: string[], meta: { initial: boolean }) => void, onError: (error: unknown) => void) => () => void;\n /** Firestore's own instant, for a field the store must hand back as the type\n * it was stored as.\n *\n * Here rather than in the store because this is the seam that owns the SDK:\n * `firebase` is an OPTIONAL peer, and the store is reachable from\n * `collection/server` (the factory registry in `store.ts` names it), so a\n * top-level `import { Timestamp }` there makes the peer required for every\n * consumer of that entry — including hosts with no Firestore at all. Typed\n * `unknown` so no SDK type crosses back out. */\n timestamp: (seconds: number, nanoseconds: number) => unknown;\n}\n\n/** The real implementation over the modular SDK.\n *\n * THE RECORD IS THE DOCUMENT. Its fields are written at the top level, not\n * nested under a `data` key. This is not a matter of taste: the deployed\n * security rules read `request.resource.data.<field>` and\n * `resource.data[submit.emailField]` — a wrapper would put every field one\n * level down, so the required-field checks, the status state machine and the\n * \"your own row\" predicate would all read absent values and fail closed. A\n * shared record's shape is part of the authorization contract.\n *\n * `orderBy(\"__name__\")` orders by DOCUMENT ID. Ordering by a record field\n * would silently EXCLUDE documents missing that field (the trap\n * `remote-host/server/hostRunner.ts:158-169` documents), turning a read into a\n * partial one; the document id is always present and gives the stable order\n * the store contract requires. */\nexport function createFirestoreDocs(database: Firestore): FirestoreDocs {\n return {\n list: async (collectionPath) => {\n const snapshot = await getDocs(firestoreQuery(firestoreCollection(database, collectionPath), orderBy(\"__name__\")));\n return snapshot.docs.map((entry) => ({ id: entry.id, data: entry.data() }));\n },\n get: async (collectionPath, docId) => {\n const snapshot = await getDoc(doc(database, collectionPath, docId));\n return snapshot.exists() ? snapshot.data() : null;\n },\n set: async (collectionPath, docId, data) => {\n await setDoc(doc(database, collectionPath, docId), data);\n },\n create: (collectionPath, docId, data) =>\n runTransaction(database, async (transaction) => {\n const ref = doc(database, collectionPath, docId);\n const existing = await transaction.get(ref);\n if (existing.exists()) return false;\n transaction.set(ref, data);\n return true;\n }),\n // Firestore's deleteDoc succeeds on a missing document, so an existence\n // check is what makes \"deleted\" distinguishable from \"there was nothing\".\n // It runs INSIDE the transaction (like `create`): a plain get-then-delete\n // would report `true` for a document a concurrent client had already\n // removed, i.e. claim a delete this call never performed.\n delete: (collectionPath, docId) =>\n runTransaction(database, async (transaction) => {\n const ref = doc(database, collectionPath, docId);\n const existing = await transaction.get(ref);\n if (!existing.exists()) return false;\n transaction.delete(ref);\n return true;\n }),\n // `docChanges()` rather than the whole snapshot: it names the documents\n // that moved, which is what lets the store report per-record changes\n // instead of \"something in this collection changed\". Added, modified and\n // removed are all reported the same way — the store's listener takes an\n // id, and re-reads; what KIND of change it was is not something a\n // reconcile pass needs to be told.\n //\n // `includeMetadataChanges` is deliberately left off: local writes and\n // has-pending-writes flips would otherwise wake every listener for\n // changes this process just made.\n watch: (collectionPath, onChanged, onError) => {\n let seen = false;\n return onSnapshot(\n firestoreCollection(database, collectionPath),\n (snapshot) => {\n const initial = !seen;\n seen = true;\n onChanged(\n snapshot.docChanges().map((change) => change.doc.id),\n { initial },\n );\n },\n onError,\n );\n },\n timestamp: (seconds, nanoseconds) => new Timestamp(seconds, nanoseconds),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiGA,SAAgB,oBAAoB,UAAoC;CACtE,OAAO;EACL,MAAM,OAAO,mBAAmB;GAE9B,QAAO,OAAA,GADgBA,mBAAAA,QAAAA,EAAAA,GAAQC,mBAAAA,MAAAA,EAAAA,GAAeC,mBAAAA,WAAAA,CAAoB,UAAU,cAAc,IAAA,GAAGC,mBAAAA,QAAAA,CAAQ,UAAU,CAAC,CAAC,EAAA,CACjG,KAAK,KAAK,WAAW;IAAE,IAAI,MAAM;IAAI,MAAM,MAAM,KAAK;GAAE,EAAE;EAC5E;EACA,KAAK,OAAO,gBAAgB,UAAU;GACpC,MAAM,WAAW,OAAA,GAAMC,mBAAAA,OAAAA,EAAAA,GAAOC,mBAAAA,IAAAA,CAAI,UAAU,gBAAgB,KAAK,CAAC;GAClE,OAAO,SAAS,OAAO,IAAI,SAAS,KAAK,IAAI;EAC/C;EACA,KAAK,OAAO,gBAAgB,OAAO,SAAS;GAC1C,OAAA,GAAMC,mBAAAA,OAAAA,EAAAA,GAAOD,mBAAAA,IAAAA,CAAI,UAAU,gBAAgB,KAAK,GAAG,IAAI;EACzD;EACA,SAAS,gBAAgB,OAAO,UAAA,GAC9BE,mBAAAA,eAAAA,CAAe,UAAU,OAAO,gBAAgB;GAC9C,MAAM,OAAA,GAAMF,mBAAAA,IAAAA,CAAI,UAAU,gBAAgB,KAAK;GAE/C,KAAI,MADmB,YAAY,IAAI,GAAG,EAAA,CAC7B,OAAO,GAAG,OAAO;GAC9B,YAAY,IAAI,KAAK,IAAI;GACzB,OAAO;EACT,CAAC;EAMH,SAAS,gBAAgB,WAAA,GACvBE,mBAAAA,eAAAA,CAAe,UAAU,OAAO,gBAAgB;GAC9C,MAAM,OAAA,GAAMF,mBAAAA,IAAAA,CAAI,UAAU,gBAAgB,KAAK;GAE/C,IAAI,EAAC,MADkB,YAAY,IAAI,GAAG,EAAA,CAC5B,OAAO,GAAG,OAAO;GAC/B,YAAY,OAAO,GAAG;GACtB,OAAO;EACT,CAAC;EAWH,QAAQ,gBAAgB,WAAW,YAAY;GAC7C,IAAI,OAAO;GACX,QAAA,GAAOG,mBAAAA,WAAAA,EAAAA,GACLN,mBAAAA,WAAAA,CAAoB,UAAU,cAAc,IAC3C,aAAa;IACZ,MAAM,UAAU,CAAC;IACjB,OAAO;IACP,UACE,SAAS,WAAW,CAAC,CAAC,KAAK,WAAW,OAAO,IAAI,EAAE,GACnD,EAAE,QAAQ,CACZ;GACF,GACA,OACF;EACF;EACA,YAAY,SAAS,gBAAgB,IAAIO,mBAAAA,UAAU,SAAS,WAAW;CACzE;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"firestore.js","names":[],"sources":["../../src/collection/server/firestoreDocs.ts"],"sourcesContent":["// The seam between the firestore store and the Firestore SDK.\n//\n// The modular SDK is function-based (`getDocs(query(collection(db, …)))`), so a\n// store that imported those functions directly could not be tested without a\n// real backend — a fake `db` would still be handed to the real functions.\n// Narrowing to this interface makes the backend swappable: core ships\n// `createFirestoreDocs` over the real SDK, and the tests inject an in-memory\n// fake that satisfies the same shape. That is what lets this repository's tests\n// run with no API key and no network.\n//\n// Deliberately minimal and id-keyed: no query builder, no field ordering, no\n// cursors. Everything the store needs is \"the documents of one collection path,\n// ordered by document id\" — see the store header for why field ordering is\n// avoided entirely.\n//\n// The collection path is an ARGUMENT, not something this module composes. It\n// owns the SDK calls; `firestoreStore` owns where the documents live.\n\nimport {\n collection as firestoreCollection,\n doc,\n getDoc,\n getDocs,\n orderBy,\n query as firestoreQuery,\n onSnapshot,\n runTransaction,\n setDoc,\n Timestamp,\n type Firestore,\n} from \"firebase/firestore\";\n\n/** One stored record document. `data` is the document's own fields — the\n * record itself, not a wrapper around it; see `set` below. The store\n * validates its shape, because a document written by hand could be\n * anything. */\nexport interface FirestoreDoc {\n id: string;\n data: unknown;\n}\n\nexport interface FirestoreDocs {\n /** Every document under `collectionPath`, ordered by document id. */\n list: (collectionPath: string) => Promise<FirestoreDoc[]>;\n /** One document's fields, or null when it doesn't exist. */\n get: (collectionPath: string, docId: string) => Promise<unknown | null>;\n /** Create or replace. */\n set: (collectionPath: string, docId: string, data: Record<string, unknown>) => Promise<void>;\n /** Create only. Returns false when the id already exists — atomic, so two\n * concurrent creates can't both observe \"missing\". */\n create: (collectionPath: string, docId: string, data: Record<string, unknown>) => Promise<boolean>;\n /** Delete. Returns false when the id didn't exist, so a caller can tell a\n * real delete from a typo'd id. */\n delete: (collectionPath: string, docId: string) => Promise<boolean>;\n /** Listen to `collectionPath`. Every snapshot reports the ids that changed\n * in it, and whether it is the FIRST one.\n *\n * `initial` is not a nicety. `onSnapshot` delivers the current contents\n * immediately, as one snapshot in which every existing document reads as\n * `added` — so a listener that treats snapshots uniformly announces the\n * whole collection as changed the moment it arms, on every mount. The flag\n * is passed up rather than swallowed here so the decision (and its test)\n * lives with the store's policy, next to the rest of it.\n *\n * `onError` fires at most once per subscription: a Firestore listen error\n * TERMINATES the listener and never recovers on its own. Re-subscribing is\n * the caller's job (`firestore/listen.ts` holds the policy).\n *\n * Returns the detach function synchronously — `onSnapshot` is not async. */\n watch: (collectionPath: string, onChanged: (ids: string[], meta: { initial: boolean }) => void, onError: (error: unknown) => void) => () => void;\n /** Firestore's own instant, for a field the store must hand back as the type\n * it was stored as.\n *\n * Here rather than in the store because this is the seam that owns the SDK:\n * `firebase` is an OPTIONAL peer, and the store is reachable from\n * `collection/server` (the factory registry in `store.ts` names it), so a\n * top-level `import { Timestamp }` there makes the peer required for every\n * consumer of that entry — including hosts with no Firestore at all. Typed\n * `unknown` so no SDK type crosses back out. */\n timestamp: (seconds: number, nanoseconds: number) => unknown;\n}\n\n/** The real implementation over the modular SDK.\n *\n * THE RECORD IS THE DOCUMENT. Its fields are written at the top level, not\n * nested under a `data` key. This is not a matter of taste: the deployed\n * security rules read `request.resource.data.<field>` and\n * `resource.data[submit.emailField]` — a wrapper would put every field one\n * level down, so the required-field checks, the status state machine and the\n * \"your own row\" predicate would all read absent values and fail closed. A\n * shared record's shape is part of the authorization contract.\n *\n * `orderBy(\"__name__\")` orders by DOCUMENT ID. Ordering by a record field\n * would silently EXCLUDE documents missing that field (the trap\n * `remote-host/server/hostRunner.ts:158-169` documents), turning a read into a\n * partial one; the document id is always present and gives the stable order\n * the store contract requires. */\nexport function createFirestoreDocs(database: Firestore): FirestoreDocs {\n return {\n list: async (collectionPath) => {\n const snapshot = await getDocs(firestoreQuery(firestoreCollection(database, collectionPath), orderBy(\"__name__\")));\n return snapshot.docs.map((entry) => ({ id: entry.id, data: entry.data() }));\n },\n get: async (collectionPath, docId) => {\n const snapshot = await getDoc(doc(database, collectionPath, docId));\n return snapshot.exists() ? snapshot.data() : null;\n },\n set: async (collectionPath, docId, data) => {\n await setDoc(doc(database, collectionPath, docId), data);\n },\n create: (collectionPath, docId, data) =>\n runTransaction(database, async (transaction) => {\n const ref = doc(database, collectionPath, docId);\n const existing = await transaction.get(ref);\n if (existing.exists()) return false;\n transaction.set(ref, data);\n return true;\n }),\n // Firestore's deleteDoc succeeds on a missing document, so an existence\n // check is what makes \"deleted\" distinguishable from \"there was nothing\".\n // It runs INSIDE the transaction (like `create`): a plain get-then-delete\n // would report `true` for a document a concurrent client had already\n // removed, i.e. claim a delete this call never performed.\n delete: (collectionPath, docId) =>\n runTransaction(database, async (transaction) => {\n const ref = doc(database, collectionPath, docId);\n const existing = await transaction.get(ref);\n if (!existing.exists()) return false;\n transaction.delete(ref);\n return true;\n }),\n // `docChanges()` rather than the whole snapshot: it names the documents\n // that moved, which is what lets the store report per-record changes\n // instead of \"something in this collection changed\". Added, modified and\n // removed are all reported the same way — the store's listener takes an\n // id, and re-reads; what KIND of change it was is not something a\n // reconcile pass needs to be told.\n //\n // `includeMetadataChanges` is deliberately left off: local writes and\n // has-pending-writes flips would otherwise wake every listener for\n // changes this process just made.\n watch: (collectionPath, onChanged, onError) => {\n let seen = false;\n return onSnapshot(\n firestoreCollection(database, collectionPath),\n (snapshot) => {\n const initial = !seen;\n seen = true;\n onChanged(\n snapshot.docChanges().map((change) => change.doc.id),\n { initial },\n );\n },\n onError,\n );\n },\n timestamp: (seconds, nanoseconds) => new Timestamp(seconds, nanoseconds),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAiGA,SAAgB,oBAAoB,UAAoC;CACtE,OAAO;EACL,MAAM,OAAO,mBAAmB;GAE9B,QAAO,MADgB,
|
|
1
|
+
{"version":3,"file":"firestore.js","names":["firestoreQuery","firestoreCollection"],"sources":["../../src/collection/server/firestoreDocs.ts"],"sourcesContent":["// The seam between the firestore store and the Firestore SDK.\n//\n// The modular SDK is function-based (`getDocs(query(collection(db, …)))`), so a\n// store that imported those functions directly could not be tested without a\n// real backend — a fake `db` would still be handed to the real functions.\n// Narrowing to this interface makes the backend swappable: core ships\n// `createFirestoreDocs` over the real SDK, and the tests inject an in-memory\n// fake that satisfies the same shape. That is what lets this repository's tests\n// run with no API key and no network.\n//\n// Deliberately minimal and id-keyed: no query builder, no field ordering, no\n// cursors. Everything the store needs is \"the documents of one collection path,\n// ordered by document id\" — see the store header for why field ordering is\n// avoided entirely.\n//\n// The collection path is an ARGUMENT, not something this module composes. It\n// owns the SDK calls; `firestoreStore` owns where the documents live.\n\nimport {\n collection as firestoreCollection,\n doc,\n getDoc,\n getDocs,\n orderBy,\n query as firestoreQuery,\n onSnapshot,\n runTransaction,\n setDoc,\n Timestamp,\n type Firestore,\n} from \"firebase/firestore\";\n\n/** One stored record document. `data` is the document's own fields — the\n * record itself, not a wrapper around it; see `set` below. The store\n * validates its shape, because a document written by hand could be\n * anything. */\nexport interface FirestoreDoc {\n id: string;\n data: unknown;\n}\n\nexport interface FirestoreDocs {\n /** Every document under `collectionPath`, ordered by document id. */\n list: (collectionPath: string) => Promise<FirestoreDoc[]>;\n /** One document's fields, or null when it doesn't exist. */\n get: (collectionPath: string, docId: string) => Promise<unknown | null>;\n /** Create or replace. */\n set: (collectionPath: string, docId: string, data: Record<string, unknown>) => Promise<void>;\n /** Create only. Returns false when the id already exists — atomic, so two\n * concurrent creates can't both observe \"missing\". */\n create: (collectionPath: string, docId: string, data: Record<string, unknown>) => Promise<boolean>;\n /** Delete. Returns false when the id didn't exist, so a caller can tell a\n * real delete from a typo'd id. */\n delete: (collectionPath: string, docId: string) => Promise<boolean>;\n /** Listen to `collectionPath`. Every snapshot reports the ids that changed\n * in it, and whether it is the FIRST one.\n *\n * `initial` is not a nicety. `onSnapshot` delivers the current contents\n * immediately, as one snapshot in which every existing document reads as\n * `added` — so a listener that treats snapshots uniformly announces the\n * whole collection as changed the moment it arms, on every mount. The flag\n * is passed up rather than swallowed here so the decision (and its test)\n * lives with the store's policy, next to the rest of it.\n *\n * `onError` fires at most once per subscription: a Firestore listen error\n * TERMINATES the listener and never recovers on its own. Re-subscribing is\n * the caller's job (`firestore/listen.ts` holds the policy).\n *\n * Returns the detach function synchronously — `onSnapshot` is not async. */\n watch: (collectionPath: string, onChanged: (ids: string[], meta: { initial: boolean }) => void, onError: (error: unknown) => void) => () => void;\n /** Firestore's own instant, for a field the store must hand back as the type\n * it was stored as.\n *\n * Here rather than in the store because this is the seam that owns the SDK:\n * `firebase` is an OPTIONAL peer, and the store is reachable from\n * `collection/server` (the factory registry in `store.ts` names it), so a\n * top-level `import { Timestamp }` there makes the peer required for every\n * consumer of that entry — including hosts with no Firestore at all. Typed\n * `unknown` so no SDK type crosses back out. */\n timestamp: (seconds: number, nanoseconds: number) => unknown;\n}\n\n/** The real implementation over the modular SDK.\n *\n * THE RECORD IS THE DOCUMENT. Its fields are written at the top level, not\n * nested under a `data` key. This is not a matter of taste: the deployed\n * security rules read `request.resource.data.<field>` and\n * `resource.data[submit.emailField]` — a wrapper would put every field one\n * level down, so the required-field checks, the status state machine and the\n * \"your own row\" predicate would all read absent values and fail closed. A\n * shared record's shape is part of the authorization contract.\n *\n * `orderBy(\"__name__\")` orders by DOCUMENT ID. Ordering by a record field\n * would silently EXCLUDE documents missing that field (the trap\n * `remote-host/server/hostRunner.ts:158-169` documents), turning a read into a\n * partial one; the document id is always present and gives the stable order\n * the store contract requires. */\nexport function createFirestoreDocs(database: Firestore): FirestoreDocs {\n return {\n list: async (collectionPath) => {\n const snapshot = await getDocs(firestoreQuery(firestoreCollection(database, collectionPath), orderBy(\"__name__\")));\n return snapshot.docs.map((entry) => ({ id: entry.id, data: entry.data() }));\n },\n get: async (collectionPath, docId) => {\n const snapshot = await getDoc(doc(database, collectionPath, docId));\n return snapshot.exists() ? snapshot.data() : null;\n },\n set: async (collectionPath, docId, data) => {\n await setDoc(doc(database, collectionPath, docId), data);\n },\n create: (collectionPath, docId, data) =>\n runTransaction(database, async (transaction) => {\n const ref = doc(database, collectionPath, docId);\n const existing = await transaction.get(ref);\n if (existing.exists()) return false;\n transaction.set(ref, data);\n return true;\n }),\n // Firestore's deleteDoc succeeds on a missing document, so an existence\n // check is what makes \"deleted\" distinguishable from \"there was nothing\".\n // It runs INSIDE the transaction (like `create`): a plain get-then-delete\n // would report `true` for a document a concurrent client had already\n // removed, i.e. claim a delete this call never performed.\n delete: (collectionPath, docId) =>\n runTransaction(database, async (transaction) => {\n const ref = doc(database, collectionPath, docId);\n const existing = await transaction.get(ref);\n if (!existing.exists()) return false;\n transaction.delete(ref);\n return true;\n }),\n // `docChanges()` rather than the whole snapshot: it names the documents\n // that moved, which is what lets the store report per-record changes\n // instead of \"something in this collection changed\". Added, modified and\n // removed are all reported the same way — the store's listener takes an\n // id, and re-reads; what KIND of change it was is not something a\n // reconcile pass needs to be told.\n //\n // `includeMetadataChanges` is deliberately left off: local writes and\n // has-pending-writes flips would otherwise wake every listener for\n // changes this process just made.\n watch: (collectionPath, onChanged, onError) => {\n let seen = false;\n return onSnapshot(\n firestoreCollection(database, collectionPath),\n (snapshot) => {\n const initial = !seen;\n seen = true;\n onChanged(\n snapshot.docChanges().map((change) => change.doc.id),\n { initial },\n );\n },\n onError,\n );\n },\n timestamp: (seconds, nanoseconds) => new Timestamp(seconds, nanoseconds),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAiGA,SAAgB,oBAAoB,UAAoC;CACtE,OAAO;EACL,MAAM,OAAO,mBAAmB;GAE9B,QAAO,MADgB,QAAQA,MAAeC,WAAoB,UAAU,cAAc,GAAG,QAAQ,UAAU,CAAC,CAAC,EAAA,CACjG,KAAK,KAAK,WAAW;IAAE,IAAI,MAAM;IAAI,MAAM,MAAM,KAAK;GAAE,EAAE;EAC5E;EACA,KAAK,OAAO,gBAAgB,UAAU;GACpC,MAAM,WAAW,MAAM,OAAO,IAAI,UAAU,gBAAgB,KAAK,CAAC;GAClE,OAAO,SAAS,OAAO,IAAI,SAAS,KAAK,IAAI;EAC/C;EACA,KAAK,OAAO,gBAAgB,OAAO,SAAS;GAC1C,MAAM,OAAO,IAAI,UAAU,gBAAgB,KAAK,GAAG,IAAI;EACzD;EACA,SAAS,gBAAgB,OAAO,SAC9B,eAAe,UAAU,OAAO,gBAAgB;GAC9C,MAAM,MAAM,IAAI,UAAU,gBAAgB,KAAK;GAE/C,KAAI,MADmB,YAAY,IAAI,GAAG,EAAA,CAC7B,OAAO,GAAG,OAAO;GAC9B,YAAY,IAAI,KAAK,IAAI;GACzB,OAAO;EACT,CAAC;EAMH,SAAS,gBAAgB,UACvB,eAAe,UAAU,OAAO,gBAAgB;GAC9C,MAAM,MAAM,IAAI,UAAU,gBAAgB,KAAK;GAE/C,IAAI,EAAC,MADkB,YAAY,IAAI,GAAG,EAAA,CAC5B,OAAO,GAAG,OAAO;GAC/B,YAAY,OAAO,GAAG;GACtB,OAAO;EACT,CAAC;EAWH,QAAQ,gBAAgB,WAAW,YAAY;GAC7C,IAAI,OAAO;GACX,OAAO,WACLA,WAAoB,UAAU,cAAc,IAC3C,aAAa;IACZ,MAAM,UAAU,CAAC;IACjB,OAAO;IACP,UACE,SAAS,WAAW,CAAC,CAAC,KAAK,WAAW,OAAO,IAAI,EAAE,GACnD,EAAE,QAAQ,CACZ;GACF,GACA,OACF;EACF;EACA,YAAY,SAAS,gBAAgB,IAAI,UAAU,SAAS,WAAW;CACzE;AACF"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_dist = require("../dist-
|
|
2
|
+
const require_dist = require("../dist-BZ62pKuU.cjs");
|
|
3
3
|
const require_itemId = require("../itemId-CVrNibwT.cjs");
|
|
4
|
-
const require_promptSafety = require("../promptSafety-
|
|
4
|
+
const require_promptSafety = require("../promptSafety-auQEatax.cjs");
|
|
5
5
|
const require_project = require("../project-C1ep9pvo.cjs");
|
|
6
6
|
const require_iconGlyph = require("../iconGlyph-fSBp6k4J.cjs");
|
|
7
|
-
require("../errorMessage-
|
|
7
|
+
require("../errorMessage-OEQzRwsi.cjs");
|
|
8
8
|
//#region src/collection/core/presentCollection.ts
|
|
9
9
|
var TOOL_NAME = "presentCollection";
|
|
10
10
|
/** Stamp a host's opaque project scope onto a card payload. The ONLY way a
|