storymapper 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +334 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +239 -0
- package/frontend/css/storymap.css +4637 -0
- package/frontend/js/adapters.js +423 -0
- package/frontend/js/card-animate.js +384 -0
- package/frontend/js/core/graph.js +825 -0
- package/frontend/js/core.js +3908 -0
- package/frontend/js/dialog-ticket-import.js +506 -0
- package/frontend/js/dnd.js +322 -0
- package/frontend/js/filter.js +215 -0
- package/frontend/js/main.js +2499 -0
- package/frontend/js/project-io.js +109 -0
- package/frontend/js/query-autocomplete.js +196 -0
- package/frontend/js/query-engine.js +339 -0
- package/frontend/js/query.js +280 -0
- package/frontend/js/renderer-card.js +639 -0
- package/frontend/js/renderer-dependencies.js +974 -0
- package/frontend/js/renderer-kanban.js +505 -0
- package/frontend/js/renderer-storymap.js +2530 -0
- package/frontend/js/renderer-ticket-editor.js +455 -0
- package/frontend/js/renderer-ticket-modal.js +758 -0
- package/frontend/js/save-pipeline.js +170 -0
- package/frontend/js/smartbar-autocomplete.js +162 -0
- package/frontend/js/store.js +197 -0
- package/frontend/js/ticket-editor-boot.js +24 -0
- package/frontend/js/ticket-form.js +2095 -0
- package/frontend/js/ticket-import.js +477 -0
- package/frontend/js/ui-shell.js +441 -0
- package/frontend/js/view-process-steps.js +233 -0
- package/frontend/js/view-requirements.js +361 -0
- package/frontend/js/view-settings.js +1864 -0
- package/frontend/js/view-table.js +659 -0
- package/frontend/js/wheel-pan.js +65 -0
- package/frontend/storymap.html +87 -0
- package/frontend/ticket-editor.html +29 -0
- package/package.json +76 -0
- package/server/bus.js +16 -0
- package/server/core/graph.js +10 -0
- package/server/core.js +10 -0
- package/server/identity.js +134 -0
- package/server/index.js +283 -0
- package/server/ingest.js +212 -0
- package/server/mcp.js +2510 -0
- package/server/server.js +1599 -0
- package/server/slice.js +103 -0
- package/server/storage.js +571 -0
- package/server/validation.js +225 -0
- package/shared/core/graph.js +825 -0
- package/shared/core.js +3908 -0
- package/shared/project-io.js +109 -0
- package/shared/query-autocomplete.js +196 -0
- package/shared/query-engine.js +339 -0
- package/shared/query.js +280 -0
- package/shared/ticket-import.js +477 -0
- package/skill/SKILL.md +458 -0
- package/skill/reference/anatomy.md +196 -0
- package/skill/reference/spec-evolution.md +52 -0
- package/skill/reference/tools.md +156 -0
- package/skill/reference/workflows.md +203 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shift+wheel → horizontal pan (SM-281).
|
|
3
|
+
*
|
|
4
|
+
* A mouse with only a vertical scroll wheel produces no deltaX, ever. On
|
|
5
|
+
* macOS the overlay scrollbars only flash for the axis being scrolled, so
|
|
6
|
+
* such users never see (let alone grab) the horizontal bar — the map's
|
|
7
|
+
* off-screen process steps become unreachable. Chrome translates
|
|
8
|
+
* shift+wheel into horizontal scrolling natively; Safari does not.
|
|
9
|
+
*
|
|
10
|
+
* This module installs that translation on the view scroller: while shift
|
|
11
|
+
* is held, a vertical wheel delta pans horizontally instead. Events that
|
|
12
|
+
* already carry a horizontal delta (Chrome's native translation, trackpad
|
|
13
|
+
* swipes) pass through untouched — no double-scroll.
|
|
14
|
+
*
|
|
15
|
+
* UMD-wrapped — usable in browser and Node tests.
|
|
16
|
+
*/
|
|
17
|
+
(function (root, factory) {
|
|
18
|
+
if (typeof module === "object" && module.exports) {
|
|
19
|
+
module.exports = factory();
|
|
20
|
+
} else {
|
|
21
|
+
(root.STORYMAP = root.STORYMAP || {}).wheelPan = factory();
|
|
22
|
+
}
|
|
23
|
+
}(typeof self !== "undefined" ? self : this, function () {
|
|
24
|
+
"use strict";
|
|
25
|
+
|
|
26
|
+
const WHEEL_PAN = {
|
|
27
|
+
// Pixels per line for deltaMode DOM_DELTA_LINE (Firefox wheel events).
|
|
28
|
+
LINE_HEIGHT_PX: 16,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const DOM_DELTA_LINE = 1;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Horizontal delta (px) a wheel event should pan, or 0 when the event
|
|
35
|
+
* must not be translated (no shift, or the engine already produced a
|
|
36
|
+
* horizontal delta itself).
|
|
37
|
+
*/
|
|
38
|
+
function translateShiftWheel(ev) {
|
|
39
|
+
if (!ev.shiftKey) return 0;
|
|
40
|
+
if (ev.deltaX) return 0; // engine translated already (Chrome) or trackpad swipe
|
|
41
|
+
const dy = ev.deltaY || 0;
|
|
42
|
+
return ev.deltaMode === DOM_DELTA_LINE ? dy * WHEEL_PAN.LINE_HEIGHT_PX : dy;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Install the shift+wheel pan on a scroll container. Returns an
|
|
47
|
+
* uninstall function. The listener is non-passive on purpose — the
|
|
48
|
+
* translated event must be consumed or the page would ALSO scroll
|
|
49
|
+
* vertically.
|
|
50
|
+
*/
|
|
51
|
+
function installShiftWheelPan(el) {
|
|
52
|
+
function onWheel(ev) {
|
|
53
|
+
const dx = translateShiftWheel(ev);
|
|
54
|
+
if (!dx) return;
|
|
55
|
+
el.scrollLeft += dx;
|
|
56
|
+
ev.preventDefault();
|
|
57
|
+
}
|
|
58
|
+
el.addEventListener("wheel", onWheel, { passive: false });
|
|
59
|
+
return function uninstall() {
|
|
60
|
+
el.removeEventListener("wheel", onWheel);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { WHEEL_PAN, translateShiftWheel, installShiftWheelPan };
|
|
65
|
+
}));
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Story Mapper</title>
|
|
7
|
+
<link rel="stylesheet" href="./css/storymap.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header class="app-header">
|
|
11
|
+
<!-- Row 1: Brand + Menu-Bar + Current-Project-Indicator. Menus only. -->
|
|
12
|
+
<div class="toolbar toolbar-menu">
|
|
13
|
+
<span class="brand"><b>Story</b><em>Mapper</em></span>
|
|
14
|
+
<div class="sep" aria-hidden="true"></div>
|
|
15
|
+
<div id="menu-bar" class="menu-bar"></div>
|
|
16
|
+
<div class="toolbar-spacer"></div>
|
|
17
|
+
<span id="current-project" class="current-project" title="Open project"></span>
|
|
18
|
+
</div>
|
|
19
|
+
<!-- Row 2: Search + Filter. View switching lives in the View menu (SM-205);
|
|
20
|
+
every other action moved to the menus per E22. -->
|
|
21
|
+
<div class="toolbar toolbar-action">
|
|
22
|
+
<div class="toolbar-spacer"></div>
|
|
23
|
+
<input id="ticket-search" class="ticket-search" type="search"
|
|
24
|
+
placeholder="Search… or a query: status = ready AND type = bug"
|
|
25
|
+
aria-label="Search or query tickets" autocomplete="off" spellcheck="false">
|
|
26
|
+
<span id="smartbar-status" class="smartbar-status" aria-live="polite"></span>
|
|
27
|
+
<button id="btn-filter" class="filter-btn" title="Filter tickets" aria-haspopup="true" aria-expanded="false">
|
|
28
|
+
<span class="filter-btn-label">Filter</span>
|
|
29
|
+
<span id="filter-badge" class="filter-badge" hidden>0</span>
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
</header>
|
|
33
|
+
|
|
34
|
+
<main id="main">
|
|
35
|
+
<section id="view-host">
|
|
36
|
+
<div id="story-map-host"></div>
|
|
37
|
+
<div id="kanban-host" hidden></div>
|
|
38
|
+
<div id="deps-host" hidden></div>
|
|
39
|
+
<div id="requirements-view-host" hidden></div>
|
|
40
|
+
<div id="process-steps-host" hidden></div>
|
|
41
|
+
<div id="table-host" hidden></div>
|
|
42
|
+
</section>
|
|
43
|
+
<!-- E23.B — Settings as overlay over Map/Kanban (not a third view). -->
|
|
44
|
+
<div id="settings-host" class="vs-overlay" hidden role="dialog" aria-label="Project settings"></div>
|
|
45
|
+
</main>
|
|
46
|
+
|
|
47
|
+
<footer class="statusbar">
|
|
48
|
+
<span id="adapter-badge" class="badge" title="Active storage backend"></span>
|
|
49
|
+
<span id="build-tag" class="build-tag" title="Loaded build (Git-Commit, kurz)"></span>
|
|
50
|
+
<span class="statusbar-spacer"></span>
|
|
51
|
+
</footer>
|
|
52
|
+
|
|
53
|
+
<!-- Single modal host (cmapper-Pattern): at most one modal at a time. -->
|
|
54
|
+
<div id="modal-host"></div>
|
|
55
|
+
|
|
56
|
+
<!-- Module load order: core → store → adapters → ui-shell → renderer-storymap → renderer-ticket-modal → main.
|
|
57
|
+
Kanban-Renderer folgt in E11. -->
|
|
58
|
+
<script defer src="./js/core.js"></script>
|
|
59
|
+
<script defer src="./js/core/graph.js"></script>
|
|
60
|
+
<script defer src="./js/store.js"></script>
|
|
61
|
+
<script defer src="./js/save-pipeline.js"></script>
|
|
62
|
+
<script defer src="./js/adapters.js"></script>
|
|
63
|
+
<script defer src="./js/ui-shell.js"></script>
|
|
64
|
+
<script defer src="./js/dnd.js"></script>
|
|
65
|
+
<script defer src="./js/wheel-pan.js"></script>
|
|
66
|
+
<script defer src="./js/card-animate.js"></script>
|
|
67
|
+
<script defer src="./js/renderer-card.js"></script>
|
|
68
|
+
<script defer src="./js/filter.js"></script>
|
|
69
|
+
<script defer src="./js/query.js"></script>
|
|
70
|
+
<script defer src="./js/query-engine.js"></script>
|
|
71
|
+
<script defer src="./js/query-autocomplete.js"></script>
|
|
72
|
+
<script defer src="./js/smartbar-autocomplete.js"></script>
|
|
73
|
+
<script defer src="./js/renderer-storymap.js"></script>
|
|
74
|
+
<script defer src="./js/renderer-kanban.js"></script>
|
|
75
|
+
<script defer src="./js/renderer-dependencies.js"></script>
|
|
76
|
+
<script defer src="./js/ticket-form.js"></script>
|
|
77
|
+
<script defer src="./js/renderer-ticket-modal.js"></script>
|
|
78
|
+
<script defer src="./js/view-settings.js"></script>
|
|
79
|
+
<script defer src="./js/view-requirements.js"></script>
|
|
80
|
+
<script defer src="./js/view-process-steps.js"></script>
|
|
81
|
+
<script defer src="./js/view-table.js"></script>
|
|
82
|
+
<script defer src="./js/ticket-import.js"></script>
|
|
83
|
+
<script defer src="./js/project-io.js"></script>
|
|
84
|
+
<script defer src="./js/dialog-ticket-import.js"></script>
|
|
85
|
+
<script defer src="./js/main.js"></script>
|
|
86
|
+
</body>
|
|
87
|
+
</html>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Ticket Editor · Story Mapper</title>
|
|
7
|
+
<link rel="stylesheet" href="./css/storymap.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body class="te-page">
|
|
10
|
+
<!-- Full-page ticket editor (Epic B / B2, SM-117). Bookmarkable at
|
|
11
|
+
/editor?projectId=&ticketId= — loads the project into a ProjectStore and
|
|
12
|
+
renders the one ticket type-aware with the SHARED ticket-form builders.
|
|
13
|
+
Content-first layout lands in B3; in-place persistence in B5. -->
|
|
14
|
+
<main id="editor-host" class="te-host"></main>
|
|
15
|
+
<div id="modal-host"></div>
|
|
16
|
+
|
|
17
|
+
<!-- Module load order: core → store → adapters → ui-shell → dnd →
|
|
18
|
+
ticket-form (shared builders) → renderer-ticket-editor. -->
|
|
19
|
+
<script defer src="./js/core.js"></script>
|
|
20
|
+
<script defer src="./js/store.js"></script>
|
|
21
|
+
<script defer src="./js/save-pipeline.js"></script>
|
|
22
|
+
<script defer src="./js/adapters.js"></script>
|
|
23
|
+
<script defer src="./js/ui-shell.js"></script>
|
|
24
|
+
<script defer src="./js/dnd.js"></script>
|
|
25
|
+
<script defer src="./js/ticket-form.js"></script>
|
|
26
|
+
<script defer src="./js/renderer-ticket-editor.js"></script>
|
|
27
|
+
<script defer src="./js/ticket-editor-boot.js"></script>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "storymapper",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "User Story Map + Kanban board with a live HTTP/WebSocket UI and an MCP server, so AI agents and humans plan the same backlog together.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"story-map",
|
|
7
|
+
"user-story-map",
|
|
8
|
+
"kanban",
|
|
9
|
+
"agile",
|
|
10
|
+
"backlog",
|
|
11
|
+
"mcp",
|
|
12
|
+
"model-context-protocol",
|
|
13
|
+
"claude",
|
|
14
|
+
"ai-agent",
|
|
15
|
+
"sqlite",
|
|
16
|
+
"project-planning"
|
|
17
|
+
],
|
|
18
|
+
"author": "Thomas Schwenger",
|
|
19
|
+
"license": "Apache-2.0",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/TomAtAP/storymapper.git"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/TomAtAP/storymapper#readme",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/TomAtAP/storymapper/issues"
|
|
27
|
+
},
|
|
28
|
+
"main": "server/index.js",
|
|
29
|
+
"type": "commonjs",
|
|
30
|
+
"mcpName": "io.github.tomatap/storymapper",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"start": "node server/index.js server",
|
|
33
|
+
"mcp": "node server/index.js mcp",
|
|
34
|
+
"install-code": "node scripts/install.js claude-code",
|
|
35
|
+
"install-desktop": "node scripts/install.js claude-desktop",
|
|
36
|
+
"install-skill": "node scripts/install-skill.js",
|
|
37
|
+
"package-skill": "node scripts/install-skill.js --zip",
|
|
38
|
+
"prepack": "node scripts/pack-dereference.js apply",
|
|
39
|
+
"postpack": "node scripts/pack-dereference.js restore",
|
|
40
|
+
"test": "node tests/run.js",
|
|
41
|
+
"walkthrough": "node tests/mcp-walkthrough.js",
|
|
42
|
+
"anim-walkthrough": "node tests/mcp-animation-walkthrough.js",
|
|
43
|
+
"prd-demo": "node tests/mcp-prd-slicing-demo.js",
|
|
44
|
+
"query-demo": "node tests/mcp-query-demo.js",
|
|
45
|
+
"perf": "node tests/perf-baseline.js"
|
|
46
|
+
},
|
|
47
|
+
"bin": {
|
|
48
|
+
"storymapper": "./server/index.js"
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"server/",
|
|
52
|
+
"shared/",
|
|
53
|
+
"frontend/",
|
|
54
|
+
"skill/",
|
|
55
|
+
"README.md",
|
|
56
|
+
"ARCHITECTURE.md",
|
|
57
|
+
"LICENSE",
|
|
58
|
+
"NOTICE"
|
|
59
|
+
],
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"better-sqlite3": "^11.5.0",
|
|
62
|
+
"fflate": "0.8.3",
|
|
63
|
+
"pdf2json": "4.0.3",
|
|
64
|
+
"ws": "^8.21.0",
|
|
65
|
+
"zod": "^4.4.3"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"jsdom": "^29.1.1"
|
|
69
|
+
},
|
|
70
|
+
"overrides": {
|
|
71
|
+
"undici": "^7.28.0"
|
|
72
|
+
},
|
|
73
|
+
"engines": {
|
|
74
|
+
"node": ">=20"
|
|
75
|
+
}
|
|
76
|
+
}
|
package/server/bus.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Process-wide EventEmitter for storage-change notifications.
|
|
5
|
+
* Storage emits 'change' after every successful save / delete / restore.
|
|
6
|
+
* The WebSocket layer subscribes and forwards to browser clients.
|
|
7
|
+
*
|
|
8
|
+
* Event shape: { projectId, revision, savedAt, op, actor, originId? }
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { EventEmitter } = require("events");
|
|
12
|
+
|
|
13
|
+
const bus = new EventEmitter();
|
|
14
|
+
bus.setMaxListeners(0); // no warning when many WS clients subscribe
|
|
15
|
+
|
|
16
|
+
module.exports = bus;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* storymap — graph algorithms.
|
|
5
|
+
*
|
|
6
|
+
* SINGLE SOURCE OF TRUTH lives in shared/core/graph.js (UMD). Thin re-export
|
|
7
|
+
* so server-side requires keep working. Edit shared/core/graph.js, never this
|
|
8
|
+
* file. (SM-139)
|
|
9
|
+
*/
|
|
10
|
+
module.exports = require("../../shared/core/graph.js");
|
package/server/core.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* storymap — core data model.
|
|
5
|
+
*
|
|
6
|
+
* SINGLE SOURCE OF TRUTH lives in shared/core.js (UMD). This is a thin
|
|
7
|
+
* re-export so every server-side require("./core.js") / require("../core.js")
|
|
8
|
+
* keeps working unchanged. Edit shared/core.js, never this file. (SM-139)
|
|
9
|
+
*/
|
|
10
|
+
module.exports = require("../shared/core.js");
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SM-129 (A6) — Identity/Actor seam + authorization choke-point.
|
|
5
|
+
*
|
|
6
|
+
* One place that decides "who is acting" for every write path (REST + MCP),
|
|
7
|
+
* and ONE pluggable `authorize(actor, op, entity)` hook that every persist
|
|
8
|
+
* passes through. Today the hook is a no-op (allow everything) and identity
|
|
9
|
+
* is header/convention-based — but the seam exists so:
|
|
10
|
+
*
|
|
11
|
+
* - no write path ever invents an ad-hoc `{ id: "unknown" }` actor again
|
|
12
|
+
* (the only place "unknown" appears is core.normalizeActor's defensive
|
|
13
|
+
* fallback for legacy/stored data, never freshly minted on a write);
|
|
14
|
+
* - real token-verified identity (later) plugs into `resolveActor`;
|
|
15
|
+
* - real RBAC (the SM-130 outlook epic) plugs into `setAuthorizePolicy`
|
|
16
|
+
* without touching a single route or tool.
|
|
17
|
+
*
|
|
18
|
+
* Server-only module (REST + MCP). Not mirrored to the frontend.
|
|
19
|
+
*
|
|
20
|
+
* TRUST MODEL (SM-211): this is a LOCAL SINGLE-USER tool. The X-Actor-*
|
|
21
|
+
* headers are attribution (who shows up in the revision history), NOT
|
|
22
|
+
* authentication — nothing verifies them. That is acceptable because the
|
|
23
|
+
* server binds to loopback and CORS rejects foreign browser origins
|
|
24
|
+
* (server.js applyRequestSecurity). Never bind to a non-loopback interface
|
|
25
|
+
* as-is. Verified identity + token gate + RBAC are the deferred multi-user
|
|
26
|
+
* stage (epic SM-130) and plug into resolveActor / setAuthorizePolicy here.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const core = require("./core.js");
|
|
30
|
+
|
|
31
|
+
// Default actor for an HTTP client that does not identify itself via the
|
|
32
|
+
// X-Actor-* header convention. NOT "unknown" — an anonymous-but-present HTTP
|
|
33
|
+
// caller is still a known transport ("http"). Frozen so callers can't mutate
|
|
34
|
+
// the shared default.
|
|
35
|
+
const DEFAULT_HTTP_ACTOR = Object.freeze({ type: "human", id: "http", name: "HTTP User" });
|
|
36
|
+
|
|
37
|
+
// Default actor for MCP tool calls (the coding agent driving the stdio server).
|
|
38
|
+
const AI_ACTOR = Object.freeze({ type: "ai", id: "claude", name: "Claude" });
|
|
39
|
+
|
|
40
|
+
// Header convention for identifying a REST caller. Node lower-cases all
|
|
41
|
+
// incoming header names, so we read the lower-case keys.
|
|
42
|
+
const HDR = Object.freeze({
|
|
43
|
+
TYPE: "x-actor-type",
|
|
44
|
+
ID: "x-actor-id",
|
|
45
|
+
NAME: "x-actor-name",
|
|
46
|
+
SESSION: "x-actor-session"
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the actor for a REST request from the X-Actor-* header convention.
|
|
51
|
+
*
|
|
52
|
+
* - `X-Actor-Id` present → identified client; build the actor from the
|
|
53
|
+
* headers (id is mandatory, type defaults to "human", name defaults to id).
|
|
54
|
+
* Token verification is a later concern — for now the headers ARE the
|
|
55
|
+
* identity. An identified client never degrades to "unknown".
|
|
56
|
+
* - no `X-Actor-Id` → anonymous HTTP caller → DEFAULT_HTTP_ACTOR.
|
|
57
|
+
*
|
|
58
|
+
* Passing no request (or a request without headers) yields the default,
|
|
59
|
+
* which is what server-internal callers want.
|
|
60
|
+
*/
|
|
61
|
+
function resolveActor(req) {
|
|
62
|
+
const headers = (req && req.headers) || null;
|
|
63
|
+
if (headers) {
|
|
64
|
+
const get = (h) => {
|
|
65
|
+
const v = headers[h];
|
|
66
|
+
return (typeof v === "string" && v.trim()) ? v.trim() : null;
|
|
67
|
+
};
|
|
68
|
+
const id = get(HDR.ID);
|
|
69
|
+
if (id) {
|
|
70
|
+
const raw = { type: get(HDR.TYPE) || "human", id: id, name: get(HDR.NAME) || id };
|
|
71
|
+
const session = get(HDR.SESSION);
|
|
72
|
+
if (session) raw.sessionId = session;
|
|
73
|
+
return core.normalizeActor(raw);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return Object.assign({}, DEFAULT_HTTP_ACTOR);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolve the actor for an MCP tool call. Honours an explicit `args.actor`
|
|
81
|
+
* override (normalized) and otherwise attributes the write to the AI agent.
|
|
82
|
+
* Replaces the per-module AI_ACTOR literal + actorFromArgs in mcp.js so REST
|
|
83
|
+
* and MCP share one identity module.
|
|
84
|
+
*/
|
|
85
|
+
function resolveAiActor(args) {
|
|
86
|
+
if (args && args.actor && typeof args.actor === "object") {
|
|
87
|
+
return core.normalizeActor(args.actor);
|
|
88
|
+
}
|
|
89
|
+
return Object.assign({}, AI_ACTOR);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Authorization choke-point -------------------------------------------
|
|
93
|
+
|
|
94
|
+
// The default policy: allow every write. RBAC plugs in via setAuthorizePolicy.
|
|
95
|
+
function allowAll(/* actor, op, entity */) { return true; }
|
|
96
|
+
|
|
97
|
+
let _policy = allowAll;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The single write-authorization choke-point. Every persist (REST + MCP)
|
|
101
|
+
* passes through here via storage.saveProject / storage.deleteProject.
|
|
102
|
+
*
|
|
103
|
+
* The policy returns truthy to allow, falsy to deny. A denial throws a
|
|
104
|
+
* `{ statusCode: 403, kind: "AUTHZ" }` error so the REST router maps it to
|
|
105
|
+
* HTTP 403 and the MCP layer surfaces it as a structured tool error — the
|
|
106
|
+
* same translation path validation errors already use.
|
|
107
|
+
*/
|
|
108
|
+
function authorize(actor, op, entity) {
|
|
109
|
+
const allowed = _policy(actor, op, entity);
|
|
110
|
+
if (!allowed) {
|
|
111
|
+
throw Object.assign(new Error("not authorized: " + op), { statusCode: 403, kind: "AUTHZ" });
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Install an RBAC policy. Pass nothing / a non-function to reset to allow-all. */
|
|
117
|
+
function setAuthorizePolicy(fn) {
|
|
118
|
+
_policy = (typeof fn === "function") ? fn : allowAll;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Restore the default allow-all policy (mainly for tests). */
|
|
122
|
+
function resetAuthorizePolicy() {
|
|
123
|
+
_policy = allowAll;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
DEFAULT_HTTP_ACTOR,
|
|
128
|
+
AI_ACTOR,
|
|
129
|
+
resolveActor,
|
|
130
|
+
resolveAiActor,
|
|
131
|
+
authorize,
|
|
132
|
+
setAuthorizePolicy,
|
|
133
|
+
resetAuthorizePolicy
|
|
134
|
+
};
|