@dreamforgestudiollc/emet 0.0.0-stage → 0.2.4
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/.env.example +136 -0
- package/CHANGELOG.md +296 -0
- package/CHARTER.md +125 -0
- package/LICENSE +22 -0
- package/README.md +783 -2
- package/docs/AUTHORSHIP.md +26 -0
- package/docs/BENCHMARKS.md +261 -0
- package/docs/BOOTSTRAP.md +127 -0
- package/docs/BOTS.md +79 -0
- package/docs/COVERAGE.md +64 -0
- package/docs/DEPLOY.md +246 -0
- package/docs/DISCIPLINES.md +29 -0
- package/docs/GUARDRAILS-AND-MONORAILS.md +107 -0
- package/docs/GUIDED-SETUP.md +296 -0
- package/docs/PERMISSIONS.md +118 -0
- package/docs/ROADMAP.md +59 -0
- package/docs/SETUP.md +259 -0
- package/guides/NEURODIVERGENT_SUPPORT.md +278 -0
- package/package.json +70 -4
- package/server.json +39 -0
- package/src/aleph.js +395 -0
- package/src/annotations.js +135 -0
- package/src/approve-throttle.js +54 -0
- package/src/atlas-role.js +58 -0
- package/src/conformance.js +688 -0
- package/src/content_analyzer.js +450 -0
- package/src/database.js +811 -0
- package/src/disciplines.js +26 -0
- package/src/dispatch.js +279 -0
- package/src/env.js +52 -0
- package/src/floor.js +187 -0
- package/src/gaps.js +476 -0
- package/src/guides.js +48 -0
- package/src/http-remote.js +269 -0
- package/src/index.js +390 -0
- package/src/invites.js +167 -0
- package/src/layers.js +449 -0
- package/src/lessons-to-checks.js +366 -0
- package/src/logger.js +299 -0
- package/src/metadata.js +61 -0
- package/src/module-path.js +32 -0
- package/src/mongo-connect.js +48 -0
- package/src/named-items.js +208 -0
- package/src/oauth-provider.js +838 -0
- package/src/persons.js +116 -0
- package/src/provenance.js +257 -0
- package/src/redact.js +231 -0
- package/src/request-log.js +124 -0
- package/src/scopes.js +97 -0
- package/src/security-headers.js +26 -0
- package/src/session-graph.js +945 -0
- package/src/session.js +1097 -0
- package/src/setup.js +1313 -0
- package/src/startup-pages.js +354 -0
- package/src/startup-payload.js +126 -0
- package/src/tools.js +4271 -0
- package/src/transcript-session.js +106 -0
- package/src/validation.js +1163 -0
- package/src/version.js +2 -0
- package/templates/BOARD.md +72 -0
- package/templates/BOTHANDOFF.md +57 -0
- package/templates/CHARACTER.md +30 -0
- package/templates/HANDOFF.md +63 -0
- package/templates/IDENTITY.md +29 -0
- package/templates/INDEX.md +82 -0
- package/templates/INSTALL.md +29 -0
- package/templates/PROCEDURE.md +39 -0
- package/templates/ROSTER.md +43 -0
- package/templates/SEED.md +65 -0
- package/templates/STAGING.md +37 -0
- package/templates/STANDARD.md +99 -0
- package/templates/THREAD.md +43 -0
- package/templates/USER.md +29 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EMET - Enduring Memory & Epistemic Tiers
|
|
3
|
+
* Copyright (c) 2026 Dreamforge Studio LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the MIT License - see LICENSE
|
|
6
|
+
*
|
|
7
|
+
* Request / auth logging for the HTTP transport (work list H4, 2026-09-07).
|
|
8
|
+
*
|
|
9
|
+
* Why: on 2026-09-07 a connector was added at the wrong URL, the browser
|
|
10
|
+
* authorization ran to completion, the client then POSTed to `/` and got a 404,
|
|
11
|
+
* and the user saw "an error". Railway's logs held three startup lines and
|
|
12
|
+
* nothing else. A server that cannot say what it answered cannot be diagnosed
|
|
13
|
+
* without reproducing the failure by hand - which is what happened.
|
|
14
|
+
*
|
|
15
|
+
* What is logged: one line per request on the OAuth and MCP paths, plus any
|
|
16
|
+
* request anywhere that ends 4xx/5xx (so a POST to a path that does not exist
|
|
17
|
+
* is visible). Fields: method, path, status, latency, client ip, and the
|
|
18
|
+
* identifying-but-not-secret parts of the exchange - client_id (a CIMD URL or
|
|
19
|
+
* an opaque registration id), grant_type, response_type, resource, the JSON-RPC
|
|
20
|
+
* method, tool name and the NAMES of the tool's parameters on /mcp (never their
|
|
21
|
+
* values - added 2026-09-27 so parameter use can be measured), and a reason on failures.
|
|
22
|
+
*
|
|
23
|
+
* What is never logged: request or response bodies, authorization codes,
|
|
24
|
+
* tokens, code verifiers, passphrases, or query strings as a whole. The line
|
|
25
|
+
* is still passed through the redaction scrubber before it is written -
|
|
26
|
+
* belt and braces; a control that depends on the author remembering is a rule.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { redactText } from './redact.js';
|
|
30
|
+
|
|
31
|
+
// Paths always logged, whatever the status.
|
|
32
|
+
export const LOGGED_PATH = /^\/(?:authorize|token|register|revoke|oauth\/approve|mcp|\.well-known\/[^\s]*)$/;
|
|
33
|
+
|
|
34
|
+
const CLIP = 160;
|
|
35
|
+
const clip = (v) => String(v).slice(0, CLIP);
|
|
36
|
+
|
|
37
|
+
/** Whether a finished request should produce a log line. */
|
|
38
|
+
export function shouldLog(path, statusCode) {
|
|
39
|
+
return LOGGED_PATH.test(path) || statusCode >= 400;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Builds the log entry for one finished request. Pure: takes the request
|
|
44
|
+
* fields it needs rather than the request object, so it is testable without
|
|
45
|
+
* express. `body` is whatever the parser left on req.body (may be undefined
|
|
46
|
+
* on a 401 that stopped before parsing), `query` is req.query.
|
|
47
|
+
*/
|
|
48
|
+
export function requestLogEntry({ method, path, statusCode, ms, ip, query = {}, body, wwwAuthenticate, reason }) {
|
|
49
|
+
const entry = { t: new Date().toISOString(), req: `${method} ${path}`, status: statusCode, ms: Math.round(ms) };
|
|
50
|
+
if (ip) entry.ip = ip;
|
|
51
|
+
|
|
52
|
+
const q = (query && typeof query === 'object') ? query : {};
|
|
53
|
+
const b = (body && typeof body === 'object' && !Array.isArray(body)) ? body : {};
|
|
54
|
+
|
|
55
|
+
const clientId = q.client_id ?? b.client_id;
|
|
56
|
+
if (clientId !== undefined) entry.client = clip(clientId);
|
|
57
|
+
if (b.grant_type) entry.grant = clip(b.grant_type);
|
|
58
|
+
if (q.response_type) entry.response_type = clip(q.response_type);
|
|
59
|
+
const resource = q.resource ?? b.resource;
|
|
60
|
+
if (resource !== undefined && resource !== null && resource !== '') entry.resource = clip(resource);
|
|
61
|
+
if (q.redirect_uri ?? b.redirect_uri) {
|
|
62
|
+
try { entry.redirect_host = new URL(q.redirect_uri ?? b.redirect_uri).host; } catch { entry.redirect_host = 'invalid'; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (path === '/mcp') {
|
|
66
|
+
if (Array.isArray(body)) entry.rpc = `batch(${body.length})`;
|
|
67
|
+
else if (b.method) {
|
|
68
|
+
entry.rpc = clip(b.method);
|
|
69
|
+
if (b.method === 'tools/call' && b.params && b.params.name) {
|
|
70
|
+
entry.tool = clip(b.params.name);
|
|
71
|
+
// Parameter NAMES only (the owner, 2026-09-27, code audit 1.6) - values never.
|
|
72
|
+
const a = b.params.arguments;
|
|
73
|
+
if (a && typeof a === 'object' && !Array.isArray(a)) entry.params = Object.keys(a).sort().map(clip);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (reason) entry.reason = clip(reason);
|
|
79
|
+
else if (statusCode === 401 && wwwAuthenticate) {
|
|
80
|
+
const m = String(wwwAuthenticate).match(/error="([^"]+)"/);
|
|
81
|
+
if (m) entry.reason = m[1];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return entry;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The rendered line. Scrubbed even though nothing secret should be in it. */
|
|
88
|
+
export function formatRequestLog(entry) {
|
|
89
|
+
return '[emet][req] ' + redactText(JSON.stringify(entry));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Express middleware. Installed before the auth router so it sees every
|
|
94
|
+
* request; logs on `finish`, when the status and any parsed body are known.
|
|
95
|
+
*/
|
|
96
|
+
export function requestLogger({ enabled = true, write = (line) => console.error(line) } = {}) {
|
|
97
|
+
return function emetRequestLog(req, res, next) {
|
|
98
|
+
if (!enabled) return next();
|
|
99
|
+
const t0 = process.hrtime.bigint();
|
|
100
|
+
// Capture the path NOW. The SDK's auth router mounts /authorize, /token,
|
|
101
|
+
// /register and the well-known documents as sub-routers, and express
|
|
102
|
+
// strips the mount prefix from req.path while a sub-router is handling
|
|
103
|
+
// the request - at `finish` it can read as '/' with status 200, which the
|
|
104
|
+
// filter would then skip. Found live on the first deploy (2026-09-07): the
|
|
105
|
+
// /.well-known line was the one missing from the log.
|
|
106
|
+
const path = String(req.originalUrl || req.url || '/').split('?')[0] || '/';
|
|
107
|
+
res.on('finish', () => {
|
|
108
|
+
try {
|
|
109
|
+
if (!shouldLog(path, res.statusCode)) return;
|
|
110
|
+
const ms = Number(process.hrtime.bigint() - t0) / 1e6;
|
|
111
|
+
const entry = requestLogEntry({
|
|
112
|
+
method: req.method, path, statusCode: res.statusCode, ms, ip: req.ip,
|
|
113
|
+
query: req.query, body: req.body,
|
|
114
|
+
wwwAuthenticate: res.getHeader && res.getHeader('www-authenticate'),
|
|
115
|
+
reason: res.locals && res.locals.reason
|
|
116
|
+
});
|
|
117
|
+
write(formatRequestLog(entry));
|
|
118
|
+
} catch (e) {
|
|
119
|
+
write('[emet][req] log failed: ' + (e && e.message));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
next();
|
|
123
|
+
};
|
|
124
|
+
}
|
package/src/scopes.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool scopes. Advertised as emet.read and emet.write.
|
|
3
|
+
* Empty list = owner / full access (today's tokens).
|
|
4
|
+
* A guest token will carry only the scopes it was granted.
|
|
5
|
+
*
|
|
6
|
+
* The scope is DERIVED from the tool's annotation (2026-09-27, code audit 1.2):
|
|
7
|
+
* a tool whose readOnlyHint is true needs emet.read, every other tool needs
|
|
8
|
+
* emet.write. Until then two hand-kept sets lived here beside the annotation
|
|
9
|
+
* table and the tool list - a third place a tool name had to be added, with no
|
|
10
|
+
* guard when it was not (an unlisted tool silently fell to emet.write).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { TOOL_ANNOTATIONS } from './annotations.js';
|
|
14
|
+
|
|
15
|
+
// A tool whose scope deliberately differs from its annotation.
|
|
16
|
+
// emet_status without probe is a read, so a guest can see that the store is up.
|
|
17
|
+
// emet_status with probe: true writes a document, so scopeForTool returns
|
|
18
|
+
// emet.write for that call. emet_session_open stamps the drop list a board
|
|
19
|
+
// edit trusts, so it requires emet.write even though a quick read of the
|
|
20
|
+
// annotation table is not the whole story — the override list no longer
|
|
21
|
+
// grants it to a read token.
|
|
22
|
+
const SCOPE_OVERRIDES = Object.freeze({
|
|
23
|
+
emet_status: 'emet.read',
|
|
24
|
+
emet_initialize: 'emet.read'
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export function scopeForTool(name, args = null) {
|
|
28
|
+
// Opening a session stamps the drop list the board guard trusts, and the
|
|
29
|
+
// status probe writes. Both need emet.write. A status call without probe
|
|
30
|
+
// stays a read.
|
|
31
|
+
if (name === 'emet_session_open') return 'emet.write';
|
|
32
|
+
if (name === 'emet_status' && args && args.probe === true) return 'emet.write';
|
|
33
|
+
if (SCOPE_OVERRIDES[name]) return SCOPE_OVERRIDES[name];
|
|
34
|
+
const a = TOOL_ANNOTATIONS[name];
|
|
35
|
+
if (a && a.readOnlyHint === true) return 'emet.read';
|
|
36
|
+
return 'emet.write';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function scopesAllow(scopes, name, opts = {}) {
|
|
40
|
+
// A presented HTTP token with no scopes is not the owner. Stdio (no token)
|
|
41
|
+
// and a passphrase token that carries emet / * still are.
|
|
42
|
+
if (opts.http === true && opts.authenticated === true && (!Array.isArray(scopes) || scopes.length === 0)) return false;
|
|
43
|
+
if (!Array.isArray(scopes) || scopes.length === 0) return true;
|
|
44
|
+
if (scopes.includes('*') || scopes.includes('emet')) return true;
|
|
45
|
+
const need = scopeForTool(name, opts.args || null);
|
|
46
|
+
return scopes.includes(need);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Security audit 2026-09-29: an empty scope list means OWNER / full access
|
|
51
|
+
* (scopesAllow above), which is right for a passphrase login and wrong for a
|
|
52
|
+
* guest. An invite that carries no usable scopes must not mint a token, or a
|
|
53
|
+
* guest would silently become the owner. Returns the invite's scopes when at
|
|
54
|
+
* least one is a real scope, else null (the caller refuses the login).
|
|
55
|
+
* Invites are frozen today (invites.js); this holds the line for when they are not.
|
|
56
|
+
*/
|
|
57
|
+
export const KNOWN_SCOPES = Object.freeze(['emet.read', 'emet.write']);
|
|
58
|
+
export function guestScopesOrNull(scopes) {
|
|
59
|
+
if (!Array.isArray(scopes)) return null;
|
|
60
|
+
const kept = [...new Set(scopes.filter((s) => KNOWN_SCOPES.includes(s)))];
|
|
61
|
+
return kept.length ? kept : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Refresh may only keep or narrow what was granted (multi-user report v2,
|
|
66
|
+
* defect 1, 2026-10-01). Until then a refresh grant replaced the stored scopes
|
|
67
|
+
* with whatever the client asked for, so a read-only invite token - or any
|
|
68
|
+
* restricted token - could refresh to `emet` or `*` and hold full access.
|
|
69
|
+
*
|
|
70
|
+
* Both sides are expanded before comparing, so an alias can never widen:
|
|
71
|
+
* - granted [] or containing `emet` / `*` is FULL (what scopesAllow honours):
|
|
72
|
+
* every request is within it - a passphrase token keeps refreshing exactly
|
|
73
|
+
* as before, with whatever scope its host sends;
|
|
74
|
+
* - otherwise a requested `emet` or `*` stands for every known scope
|
|
75
|
+
* (KNOWN_SCOPES - all scopeForTool can ask for), and each requested scope
|
|
76
|
+
* must already be granted. Any other requested string must be granted
|
|
77
|
+
* verbatim.
|
|
78
|
+
* Returns the scopes that are not covered (empty = the request is within).
|
|
79
|
+
*/
|
|
80
|
+
export const FULL_SCOPE_ALIASES = Object.freeze(['emet', '*']);
|
|
81
|
+
export function isFullScopeList(scopes) {
|
|
82
|
+
return !Array.isArray(scopes) || scopes.length === 0 || scopes.some((s) => FULL_SCOPE_ALIASES.includes(s));
|
|
83
|
+
}
|
|
84
|
+
export function scopesNotGranted(granted, requested) {
|
|
85
|
+
if (isFullScopeList(granted)) return [];
|
|
86
|
+
const have = new Set(granted);
|
|
87
|
+
const wider = [];
|
|
88
|
+
for (const r of requested || []) {
|
|
89
|
+
const needs = FULL_SCOPE_ALIASES.includes(r) ? KNOWN_SCOPES : [r];
|
|
90
|
+
if (!needs.every((n) => have.has(n))) wider.push(r);
|
|
91
|
+
}
|
|
92
|
+
return [...new Set(wider)];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function deniedMessage(name, scopes, args = null) {
|
|
96
|
+
return `This login is not allowed to call ${name}. Granted: ${(scopes || []).join(', ') || '(none)'}. Needs ${scopeForTool(name, args)}.`;
|
|
97
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline headers on every HTTP response.
|
|
3
|
+
*
|
|
4
|
+
* The consent page sets its own Content-Security-Policy. These four headers
|
|
5
|
+
* are set again just before the status line is written, so a later handler
|
|
6
|
+
* cannot drop them, and they do not replace that policy.
|
|
7
|
+
*/
|
|
8
|
+
export const SECURITY_HEADERS = Object.freeze({
|
|
9
|
+
'X-Content-Type-Options': 'nosniff',
|
|
10
|
+
'X-Frame-Options': 'DENY',
|
|
11
|
+
'Referrer-Policy': 'no-referrer',
|
|
12
|
+
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export function securityHeaders(_req, res, next) {
|
|
16
|
+
const apply = () => {
|
|
17
|
+
for (const [name, value] of Object.entries(SECURITY_HEADERS)) res.setHeader(name, value);
|
|
18
|
+
};
|
|
19
|
+
apply();
|
|
20
|
+
const writeHead = res.writeHead;
|
|
21
|
+
res.writeHead = function writeHeadWithSecurityHeaders(...args) {
|
|
22
|
+
apply();
|
|
23
|
+
return writeHead.apply(this, args);
|
|
24
|
+
};
|
|
25
|
+
next();
|
|
26
|
+
}
|