create-claude-cabinet 0.39.0 → 0.40.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/README.md +11 -0
- package/lib/cli.js +10 -3
- package/lib/mux-setup.js +4 -0
- package/lib/settings-merge.js +40 -1
- package/package.json +1 -1
- package/templates/cabinet/_cabinet-member-template.md +6 -20
- package/templates/cabinet/watchtower-contracts.md +75 -3
- package/templates/engagement-server/Dockerfile +2 -0
- package/templates/engagement-server/server.mjs +73 -21
- package/templates/mux/bin/mux +196 -10
- package/templates/mux/config/__pycache__/muxlib.cpython-314.pyc +0 -0
- package/templates/mux/config/help.txt +5 -0
- package/templates/mux/config/mux-server.py +5 -2
- package/templates/mux/config/muxlib.py +31 -1
- package/templates/mux/config/worktree-cleanup.sh +86 -0
- package/templates/mux/config/worktree-health-popup.sh +23 -0
- package/templates/mux/config/worktree-session-health.sh +105 -0
- package/templates/rules/maintainability.md +92 -0
- package/templates/rules/memory-capture.md +4 -2
- package/templates/scripts/watchtower-build-context.mjs +95 -7
- package/templates/scripts/watchtower-lib.mjs +41 -1
- package/templates/scripts/watchtower-queue.mjs +13 -9
- package/templates/scripts/watchtower-ring1.mjs +248 -14
- package/templates/scripts/watchtower-ring2.mjs +280 -20
- package/templates/scripts/watchtower-ring3-close.mjs +424 -105
- package/templates/scripts/watchtower-status.sh +260 -0
- package/templates/scripts/watchtower-validate.mjs +3 -3
- package/templates/skills/briefing/SKILL.md +46 -7
- package/templates/skills/cabinet-accessibility/SKILL.md +58 -223
- package/templates/skills/cabinet-anthropic-insider/SKILL.md +63 -296
- package/templates/skills/cabinet-anti-confirmation/SKILL.md +36 -152
- package/templates/skills/cabinet-architecture/SKILL.md +57 -265
- package/templates/skills/cabinet-automation/SKILL.md +75 -398
- package/templates/skills/cabinet-boundary-man/SKILL.md +56 -194
- package/templates/skills/cabinet-cc-health/SKILL.md +62 -462
- package/templates/skills/cabinet-cc-health/migration-reference.md +46 -0
- package/templates/skills/cabinet-data-integrity/SKILL.md +51 -142
- package/templates/skills/cabinet-debugger/SKILL.md +65 -209
- package/templates/skills/cabinet-elegance/SKILL.md +32 -229
- package/templates/skills/cabinet-framework-quality/SKILL.md +76 -387
- package/templates/skills/cabinet-goal-alignment/SKILL.md +62 -218
- package/templates/skills/cabinet-historian/SKILL.md +74 -320
- package/templates/skills/cabinet-information-design/SKILL.md +87 -432
- package/templates/skills/cabinet-interactive-storyteller/SKILL.md +75 -307
- package/templates/skills/cabinet-mantine-quality/SKILL.md +40 -293
- package/templates/skills/cabinet-narrative-architect/SKILL.md +65 -254
- package/templates/skills/cabinet-organized-mind/SKILL.md +88 -340
- package/templates/skills/cabinet-process-therapist/SKILL.md +68 -233
- package/templates/skills/cabinet-qa/SKILL.md +55 -195
- package/templates/skills/cabinet-record-keeper/SKILL.md +57 -170
- package/templates/skills/cabinet-roster-check/SKILL.md +72 -300
- package/templates/skills/cabinet-security/SKILL.md +56 -211
- package/templates/skills/cabinet-small-screen/SKILL.md +36 -138
- package/templates/skills/cabinet-speed-freak/SKILL.md +34 -198
- package/templates/skills/cabinet-system-advocate/SKILL.md +60 -170
- package/templates/skills/cabinet-technical-debt/SKILL.md +34 -176
- package/templates/skills/cabinet-ui-experimentalist/SKILL.md +65 -231
- package/templates/skills/cabinet-usability/SKILL.md +57 -175
- package/templates/skills/cabinet-user-advocate/SKILL.md +67 -290
- package/templates/skills/cabinet-vision/SKILL.md +60 -224
- package/templates/skills/cabinet-workflow-cop/SKILL.md +61 -226
- package/templates/skills/collab-client/SKILL.md +30 -0
- package/templates/skills/collab-consultant/SKILL.md +146 -2
- package/templates/skills/decisions/SKILL.md +7 -158
- package/templates/skills/dx-feedback/SKILL.md +125 -0
- package/templates/skills/inbox/SKILL.md +181 -0
- package/templates/skills/investigate/SKILL.md +2 -2
- package/templates/skills/orient/phases/dx-captures.md +16 -17
- package/templates/skills/plan/phases/verify-plan.md +3 -3
- package/templates/skills/watchtower/SKILL.md +3 -2
- package/templates/watchtower/queue/items/item.json.schema +3 -3
package/README.md
CHANGED
|
@@ -221,6 +221,17 @@ the listed modules to what's already there, it doesn't replace your
|
|
|
221
221
|
module set. Safe to run on a mature project without losing
|
|
222
222
|
customization. You can pass multiple modules: `--modules verify,audit`.
|
|
223
223
|
|
|
224
|
+
### Opt-in Modules
|
|
225
|
+
|
|
226
|
+
| Module | What it does |
|
|
227
|
+
|--------|-------------|
|
|
228
|
+
| **verify** | Cucumber + Playwright walkthrough verification harness |
|
|
229
|
+
| **site-audit** | 14-check deployed-site quality audit with HTML reports |
|
|
230
|
+
| **engagement** | Client engagement management — packets, billing, feedback loops |
|
|
231
|
+
| **engagement-server** | Central multi-engagement API server (Railway/Fly deploy) |
|
|
232
|
+
| **watchtower** | Continuous background state management replacing orient/debrief |
|
|
233
|
+
| **mux** | Multi-project terminal manager — desks, auto-worktrees with shared identity, trail logging, DX captures, portal color-switching |
|
|
234
|
+
|
|
224
235
|
## CLI Options
|
|
225
236
|
|
|
226
237
|
```
|
package/lib/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const { copyTemplates } = require('./copy');
|
|
7
|
-
const { mergeSettings, healUserSettings, mergeWatchtowerHooks } = require('./settings-merge');
|
|
7
|
+
const { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks } = require('./settings-merge');
|
|
8
8
|
const { create: createMetadata, read: readMetadata } = require('./metadata');
|
|
9
9
|
const { setupDb } = require('./db-setup');
|
|
10
10
|
const { setupVerifyRuntime } = require('./verify-setup');
|
|
@@ -496,7 +496,7 @@ const MODULES = {
|
|
|
496
496
|
mandatory: false,
|
|
497
497
|
default: true,
|
|
498
498
|
lean: false,
|
|
499
|
-
templates: ['rules/enforcement-pipeline.md', 'memory/patterns/_pattern-template.md', 'memory/patterns/pattern-intelligence-first.md'],
|
|
499
|
+
templates: ['rules/enforcement-pipeline.md', 'rules/maintainability.md', 'memory/patterns/_pattern-template.md', 'memory/patterns/pattern-intelligence-first.md'],
|
|
500
500
|
},
|
|
501
501
|
'memory': {
|
|
502
502
|
name: 'Built-In Memory (cc-remember + reader + validator)',
|
|
@@ -623,7 +623,7 @@ const MODULES = {
|
|
|
623
623
|
},
|
|
624
624
|
watchtower: {
|
|
625
625
|
name: 'Watchtower (continuous background state)',
|
|
626
|
-
description: 'Replaces orient/debrief with continuous background processing. Three rings (mechanical cron, Claude intelligence, session-aware), ambient state injection via SessionStart hook, and an
|
|
626
|
+
description: 'Replaces orient/debrief with continuous background processing. Three rings (mechanical cron, Claude intelligence, session-aware), ambient state injection via SessionStart hook, and an inbox for extracted knowledge and signals. Sessions start informed with minimal context cost.',
|
|
627
627
|
mandatory: false,
|
|
628
628
|
default: false,
|
|
629
629
|
lean: false,
|
|
@@ -632,6 +632,7 @@ const MODULES = {
|
|
|
632
632
|
'cabinet/watchtower-contracts.md',
|
|
633
633
|
'scripts/watchtower-lib.mjs',
|
|
634
634
|
'scripts/watchtower-queue.mjs',
|
|
635
|
+
'skills/inbox',
|
|
635
636
|
'skills/decisions',
|
|
636
637
|
'hooks/watchtower-session-start.sh',
|
|
637
638
|
'scripts/watchtower-build-context.mjs',
|
|
@@ -651,6 +652,7 @@ const MODULES = {
|
|
|
651
652
|
'watchtower/watchtower-ring2-slow.timer',
|
|
652
653
|
'hooks/watchtower-session-end.sh',
|
|
653
654
|
'scripts/watchtower-ring3-close.mjs',
|
|
655
|
+
'scripts/watchtower-status.sh',
|
|
654
656
|
'skills/briefing',
|
|
655
657
|
],
|
|
656
658
|
},
|
|
@@ -1285,6 +1287,11 @@ async function run() {
|
|
|
1285
1287
|
mergeWatchtowerHooks(settingsPath);
|
|
1286
1288
|
console.log(' ⚙️ Registered watchtower SessionStart/SessionEnd hooks');
|
|
1287
1289
|
}
|
|
1290
|
+
|
|
1291
|
+
if (selectedModules.includes('mux')) {
|
|
1292
|
+
mergeMuxHooks(settingsPath);
|
|
1293
|
+
console.log(' ⚙️ Registered mux worktree health SessionStart hook');
|
|
1294
|
+
}
|
|
1288
1295
|
}
|
|
1289
1296
|
|
|
1290
1297
|
// --- Heal user-level ~/.claude/settings.json ---
|
package/lib/mux-setup.js
CHANGED
|
@@ -42,6 +42,9 @@ const MANAGED_FILES = [
|
|
|
42
42
|
{ src: 'config/help.txt', dest: path.join(os.homedir(), '.config', 'mux', 'help.txt') },
|
|
43
43
|
{ src: 'config/_mux', dest: path.join(os.homedir(), '.config', 'mux', '_mux') },
|
|
44
44
|
{ src: 'config/mux.bash', dest: path.join(os.homedir(), '.config', 'mux', 'mux.bash') },
|
|
45
|
+
{ src: 'config/worktree-session-health.sh', dest: path.join(os.homedir(), '.config', 'mux', 'worktree-session-health.sh'), mode: 0o755 },
|
|
46
|
+
{ src: 'config/worktree-health-popup.sh', dest: path.join(os.homedir(), '.config', 'mux', 'worktree-health-popup.sh'), mode: 0o755 },
|
|
47
|
+
{ src: 'config/worktree-cleanup.sh', dest: path.join(os.homedir(), '.config', 'mux', 'worktree-cleanup.sh'), mode: 0o755 },
|
|
45
48
|
];
|
|
46
49
|
|
|
47
50
|
const DATA_DIRS = [
|
|
@@ -50,6 +53,7 @@ const DATA_DIRS = [
|
|
|
50
53
|
path.join(os.homedir(), '.config', 'mux', 'notes'),
|
|
51
54
|
path.join(os.homedir(), '.config', 'mux', 'dx'),
|
|
52
55
|
path.join(os.homedir(), '.config', 'mux', 'pending-prompts'),
|
|
56
|
+
path.join(os.homedir(), '.local', 'share', 'mux', 'wt-health'),
|
|
53
57
|
];
|
|
54
58
|
|
|
55
59
|
function sha256(content) {
|
package/lib/settings-merge.js
CHANGED
|
@@ -107,6 +107,20 @@ const WATCHTOWER_HOOKS = {
|
|
|
107
107
|
],
|
|
108
108
|
};
|
|
109
109
|
|
|
110
|
+
const MUX_HOOKS = {
|
|
111
|
+
SessionStart: [
|
|
112
|
+
{
|
|
113
|
+
matcher: '',
|
|
114
|
+
hooks: [
|
|
115
|
+
{
|
|
116
|
+
type: 'command',
|
|
117
|
+
command: '$HOME/.config/mux/worktree-session-health.sh',
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
|
|
110
124
|
// Legacy hook script names that should be stripped on any merge.
|
|
111
125
|
// Centralizes cleanup so a user who skips --migrate-memory but runs
|
|
112
126
|
// any other CC operation still gets omega-era hooks pruned.
|
|
@@ -264,4 +278,29 @@ function mergeWatchtowerHooks(settingsPath) {
|
|
|
264
278
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
265
279
|
}
|
|
266
280
|
|
|
267
|
-
|
|
281
|
+
function mergeMuxHooks(settingsPath) {
|
|
282
|
+
if (!fs.existsSync(settingsPath)) return;
|
|
283
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
284
|
+
if (!settings.hooks) settings.hooks = {};
|
|
285
|
+
|
|
286
|
+
for (const [event, newHooks] of Object.entries(MUX_HOOKS)) {
|
|
287
|
+
if (!settings.hooks[event]) {
|
|
288
|
+
settings.hooks[event] = newHooks;
|
|
289
|
+
} else {
|
|
290
|
+
for (const newHook of newHooks) {
|
|
291
|
+
const hookKey = h => h.command || h.prompt || '';
|
|
292
|
+
const existingKeys = settings.hooks[event].flatMap(h =>
|
|
293
|
+
h.hooks.map(hh => hookKey(hh))
|
|
294
|
+
);
|
|
295
|
+
const newKeys = newHook.hooks.map(h => hookKey(h));
|
|
296
|
+
if (!newKeys.every(k => existingKeys.includes(k))) {
|
|
297
|
+
settings.hooks[event].push(newHook);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, DEFAULT_HOOKS, WATCHTOWER_HOOKS, MUX_HOOKS, LEGACY_HOOK_COMMANDS };
|
package/package.json
CHANGED
|
@@ -125,33 +125,19 @@ to anchor the boundaries.
|
|
|
125
125
|
|
|
126
126
|
### 7. Historically Problematic Patterns
|
|
127
127
|
|
|
128
|
-
Two-file overlay:
|
|
129
|
-
|
|
130
128
|
```markdown
|
|
131
129
|
## Historically Problematic Patterns
|
|
132
130
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
1. **This section** (upstream, CC-owned) — universal patterns that apply to
|
|
136
|
-
any project. Grows when consuming projects promote recurring findings
|
|
137
|
-
via field-feedback.
|
|
138
|
-
2. **`patterns-project.md`** in this skill's directory — project-specific
|
|
139
|
-
patterns discovered during audits of this particular project. Project-
|
|
140
|
-
owned, never overwritten by CC upgrades.
|
|
141
|
-
|
|
142
|
-
If `patterns-project.md` exists, read it alongside this section. Both
|
|
143
|
-
inform your analysis equally.
|
|
144
|
-
|
|
145
|
-
**How patterns get here:** A consuming project's audit finds a real issue.
|
|
146
|
-
If the same pattern recurs across projects, it gets promoted upstream via
|
|
147
|
-
field-feedback. The CC maintainer adds it to this section. Project-specific
|
|
148
|
-
patterns that don't generalize stay in `patterns-project.md`.
|
|
131
|
+
Read `patterns-project.md` in this skill directory for project-specific
|
|
132
|
+
patterns from prior audits. Apply alongside universal patterns below.
|
|
149
133
|
|
|
150
134
|
<!-- Universal patterns below this line -->
|
|
151
135
|
```
|
|
152
136
|
|
|
153
|
-
This section starts empty for new members.
|
|
154
|
-
findings over time — never pre-fill with
|
|
137
|
+
This section starts empty for new members. Universal patterns accumulate
|
|
138
|
+
from real field-feedback findings over time — never pre-fill with
|
|
139
|
+
hypothetical patterns. Project-specific patterns live in
|
|
140
|
+
`patterns-project.md` (project-owned, never overwritten by CC upgrades).
|
|
155
141
|
|
|
156
142
|
## Optional Sections
|
|
157
143
|
|
|
@@ -17,12 +17,12 @@ fs.renameSync(tmp, filePath);
|
|
|
17
17
|
|
|
18
18
|
## No-Index Convention
|
|
19
19
|
|
|
20
|
-
The
|
|
20
|
+
The inbox queue uses directory listing, not an index file. To list
|
|
21
21
|
pending items, read `queue/items/`, open each `.json`, filter by
|
|
22
22
|
status. No manifest, no index.json.
|
|
23
23
|
|
|
24
24
|
Rationale: one fewer file to keep consistent. Directory listing is
|
|
25
|
-
O(n) but n is small (
|
|
25
|
+
O(n) but n is small (inbox items rarely exceed 50 items). If
|
|
26
26
|
listing exceeds 2 seconds or 500 items, revisit via `act:2b638b02`.
|
|
27
27
|
|
|
28
28
|
## Schema Versioning
|
|
@@ -70,7 +70,7 @@ standard files:
|
|
|
70
70
|
| `memory-refs.md` | Ring 2 fast | Relevant memory entries |
|
|
71
71
|
| `options-analysis.md` | Ring 2 fast | Pros/cons with evidence |
|
|
72
72
|
|
|
73
|
-
`/
|
|
73
|
+
`/inbox` reads these when `enrichment_status` is `"complete"`.
|
|
74
74
|
Missing files degrade gracefully (null in the read result).
|
|
75
75
|
|
|
76
76
|
## Deferred Schemas
|
|
@@ -85,3 +85,75 @@ their owning plan ships:
|
|
|
85
85
|
| `hooks/<ring>-<phase>.d/` | Plan 9 | Lifecycle hooks |
|
|
86
86
|
| `logs/<ring>.log` | Plan 4 | Ring 1 runner |
|
|
87
87
|
| `lock/<ring>.pid` | Plan 4 | Ring 1 runner |
|
|
88
|
+
|
|
89
|
+
## Ring 4 — Periodic Truth Reconciliation (design phase)
|
|
90
|
+
|
|
91
|
+
**Status: concept. Needs design session before implementation.**
|
|
92
|
+
|
|
93
|
+
Rings 1–3 operate on individual signals: individual files (R1),
|
|
94
|
+
individual patterns (R2), individual sessions (R3). Nothing catches
|
|
95
|
+
**cumulative drift** — the slow rot where twenty sessions each move
|
|
96
|
+
reality a little further from what documents claim, but no single
|
|
97
|
+
session is the tipping point.
|
|
98
|
+
|
|
99
|
+
Ring 4 is the answer to: "Is what we wrote still true?"
|
|
100
|
+
|
|
101
|
+
### The problem it solves
|
|
102
|
+
|
|
103
|
+
The maginnis project's architecture briefing said "Layer 4 Not built"
|
|
104
|
+
and "tech stack TBD" for three weeks after the platform shipped with
|
|
105
|
+
a Rails 8 app, Mantine frontend, 421 RSpec specs, and staging on
|
|
106
|
+
Railway. No individual session caused the staleness. No individual
|
|
107
|
+
session's debrief would flag it. The drift was invisible until
|
|
108
|
+
someone tried to use the briefing for a real audit and found it
|
|
109
|
+
described a project that no longer existed.
|
|
110
|
+
|
|
111
|
+
This class of problem includes:
|
|
112
|
+
- **Briefing-to-codebase drift** — claims about architecture, tech
|
|
113
|
+
stack, file layout, or project state that no longer match reality
|
|
114
|
+
- **Memory-to-codebase drift** — memory files that reference
|
|
115
|
+
functions, files, or flags that have been renamed or removed
|
|
116
|
+
- **CLAUDE.md drift** — sections describing architecture or
|
|
117
|
+
conventions that have been refactored past
|
|
118
|
+
- **Plan-to-reality drift** — plan files describing work that's been
|
|
119
|
+
done but never marked complete, or describing approaches that were
|
|
120
|
+
abandoned
|
|
121
|
+
|
|
122
|
+
### Cadence and weight
|
|
123
|
+
|
|
124
|
+
Less frequent than R1–R3. Daily or weekly, not per-session or
|
|
125
|
+
per-minute. Heavier per run — reads real code, cross-references
|
|
126
|
+
document claims against codebase state, may invoke Claude for
|
|
127
|
+
semantic comparison. Acceptable cost because it runs rarely and
|
|
128
|
+
catches problems that compound silently.
|
|
129
|
+
|
|
130
|
+
### Relationship to other rings
|
|
131
|
+
|
|
132
|
+
Following the nervous system principle (not a stack): R4 consumes
|
|
133
|
+
signals from R1–R3 (what changed recently, what sessions touched,
|
|
134
|
+
what patterns emerged) to prioritize what to reconcile. R4 produces
|
|
135
|
+
inbox items when drift is detected. R4 does not mutate documents —
|
|
136
|
+
it flags, the user triages and routes.
|
|
137
|
+
|
|
138
|
+
R3 catches "this session broke the briefing." R4 catches "the
|
|
139
|
+
briefing has been slowly wrong for three weeks and nobody noticed."
|
|
140
|
+
They're complementary, not redundant.
|
|
141
|
+
|
|
142
|
+
### Open design questions
|
|
143
|
+
|
|
144
|
+
- **Scope per run:** Reconcile everything every time, or rotate
|
|
145
|
+
through documents on a schedule? Full sweep is thorough but
|
|
146
|
+
expensive; rotation risks missing urgent drift on off-cycle docs.
|
|
147
|
+
- **Drift threshold:** How wrong does a document need to be before
|
|
148
|
+
it's worth flagging? A missing model in the architecture briefing
|
|
149
|
+
is clear; a slightly outdated line count is noise.
|
|
150
|
+
- **Trigger vs schedule:** Pure schedule (weekly), or also triggered
|
|
151
|
+
when R1/R2 detect significant codebase changes (new migration,
|
|
152
|
+
major refactor, new dependency)?
|
|
153
|
+
- **Cross-project scope:** Per-project reconciliation, or also
|
|
154
|
+
cross-project (e.g., a CC template claiming something about
|
|
155
|
+
consumer behavior that's no longer true)?
|
|
156
|
+
- **Relationship to Ring 2 slow tier:** R2 slow already does some
|
|
157
|
+
staleness detection (stale work, memory hygiene). Where's the line
|
|
158
|
+
between R2's "is this work item stale?" and R4's "is this document
|
|
159
|
+
still true?" Is R4 an extension of R2 slow, or genuinely distinct?
|
|
@@ -73,7 +73,7 @@ function hashToken(raw) {
|
|
|
73
73
|
return createHash('sha256').update(raw).digest('hex');
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function
|
|
76
|
+
function resolveLocalToken(tokenHash) {
|
|
77
77
|
return db.prepare(`
|
|
78
78
|
SELECT u.id AS user_id, u.engagement_id, u.role, u.name AS user_name,
|
|
79
79
|
e.auth_mode, e.auth_config
|
|
@@ -84,31 +84,83 @@ function resolveToken(tokenHash) {
|
|
|
84
84
|
`).get(tokenHash);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function getExternalEngagements() {
|
|
88
|
+
return db.prepare(`
|
|
89
|
+
SELECT id, auth_config FROM engagements
|
|
90
|
+
WHERE auth_mode = 'external' AND auth_config IS NOT NULL
|
|
91
|
+
`).all();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveExternalUser(engagementId, email, roleMapping) {
|
|
95
|
+
const user = db.prepare(`
|
|
96
|
+
SELECT id AS user_id, engagement_id, role, name AS user_name
|
|
97
|
+
FROM users
|
|
98
|
+
WHERE engagement_id = ? AND email = ?
|
|
99
|
+
`).get(engagementId, email);
|
|
100
|
+
if (user) return user;
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
87
105
|
async function authenticateRequest(req) {
|
|
88
106
|
const authHeader = req.headers['authorization'];
|
|
89
107
|
if (!authHeader || !authHeader.startsWith('Bearer ')) return null;
|
|
90
108
|
|
|
91
109
|
const raw = authHeader.slice(7);
|
|
92
|
-
const tokenData = resolveToken(hashToken(raw));
|
|
93
|
-
if (!tokenData) return null;
|
|
94
110
|
|
|
95
|
-
|
|
111
|
+
// Path 1: local token lookup (works for all auth modes)
|
|
112
|
+
const localData = resolveLocalToken(hashToken(raw));
|
|
113
|
+
if (localData && localData.auth_mode === 'local') return localData;
|
|
114
|
+
|
|
115
|
+
// Path 2: external auth — forward token to client app's validate_url
|
|
116
|
+
const externals = getExternalEngagements();
|
|
117
|
+
for (const eng of externals) {
|
|
96
118
|
try {
|
|
97
|
-
const config = JSON.parse(
|
|
98
|
-
if (config.validate_url)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
119
|
+
const config = JSON.parse(eng.auth_config);
|
|
120
|
+
if (!config.validate_url) continue;
|
|
121
|
+
|
|
122
|
+
const resp = await fetch(config.validate_url, {
|
|
123
|
+
headers: { 'Authorization': `Bearer ${raw}` },
|
|
124
|
+
signal: AbortSignal.timeout(5000),
|
|
125
|
+
});
|
|
126
|
+
if (!resp.ok) continue;
|
|
127
|
+
|
|
128
|
+
const identity = await resp.json();
|
|
129
|
+
const email = identity.email;
|
|
130
|
+
if (!email) continue;
|
|
131
|
+
|
|
132
|
+
// Map platform role to engagement role via role_mapping
|
|
133
|
+
const roleMapping = config.role_mapping || {};
|
|
134
|
+
const mappedRole = roleMapping[identity.role] || identity.role;
|
|
135
|
+
|
|
136
|
+
// Find matching local user by email for message attribution
|
|
137
|
+
const user = resolveExternalUser(eng.id, email, roleMapping);
|
|
138
|
+
if (user) return { ...user, auth_mode: 'external' };
|
|
139
|
+
|
|
140
|
+
// User authenticated with platform but no local user record —
|
|
141
|
+
// auto-create so messages are properly attributed
|
|
142
|
+
const userId = `usr_${randomBytes(6).toString('hex')}`;
|
|
143
|
+
const engRole = ['consultant', 'client'].includes(mappedRole) ? mappedRole : 'client';
|
|
144
|
+
db.prepare(`INSERT INTO users (id, engagement_id, name, email, role) VALUES (?, ?, ?, ?, ?)`)
|
|
145
|
+
.run(userId, eng.id, email.split('@')[0], email, engRole);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
user_id: userId,
|
|
149
|
+
engagement_id: eng.id,
|
|
150
|
+
role: engRole,
|
|
151
|
+
user_name: email.split('@')[0],
|
|
152
|
+
auth_mode: 'external',
|
|
153
|
+
};
|
|
105
154
|
} catch (err) {
|
|
106
|
-
console.error(`External auth failed: ${err.message}`);
|
|
107
|
-
return { error: 'auth_backend_unavailable' };
|
|
155
|
+
console.error(`External auth failed for engagement ${eng.id}: ${err.message}`);
|
|
108
156
|
}
|
|
109
157
|
}
|
|
110
158
|
|
|
111
|
-
|
|
159
|
+
// Path 3: local token in an external-auth engagement (consultant-side
|
|
160
|
+
// tokens may still be locally managed)
|
|
161
|
+
if (localData) return localData;
|
|
162
|
+
|
|
163
|
+
return null;
|
|
112
164
|
}
|
|
113
165
|
|
|
114
166
|
// ---------------------------------------------------------------------------
|
|
@@ -278,18 +330,18 @@ const server = createServer(async (req, res) => {
|
|
|
278
330
|
const path = url.pathname;
|
|
279
331
|
const method = req.method;
|
|
280
332
|
|
|
333
|
+
// Health check — no auth, before HTTPS enforcement (Railway internal probes lack x-forwarded-proto)
|
|
334
|
+
if (path === '/health' && method === 'GET') {
|
|
335
|
+
logRequest(req, 200, null);
|
|
336
|
+
return json(res, 200, { ok: true, version: SCHEMA_VERSION });
|
|
337
|
+
}
|
|
338
|
+
|
|
281
339
|
// HTTPS enforcement (Railway terminates TLS)
|
|
282
340
|
if (process.env.RAILWAY_ENVIRONMENT && req.headers['x-forwarded-proto'] !== 'https') {
|
|
283
341
|
logRequest(req, 421, null);
|
|
284
342
|
return json(res, 421, { error: 'https_required' });
|
|
285
343
|
}
|
|
286
344
|
|
|
287
|
-
// Health check — no auth
|
|
288
|
-
if (path === '/health' && method === 'GET') {
|
|
289
|
-
logRequest(req, 200, null);
|
|
290
|
-
return json(res, 200, { ok: true, version: SCHEMA_VERSION });
|
|
291
|
-
}
|
|
292
|
-
|
|
293
345
|
// All other routes require auth
|
|
294
346
|
const authHeader = req.headers['authorization'];
|
|
295
347
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|