@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.
Files changed (73) hide show
  1. package/.env.example +136 -0
  2. package/CHANGELOG.md +296 -0
  3. package/CHARTER.md +125 -0
  4. package/LICENSE +22 -0
  5. package/README.md +783 -2
  6. package/docs/AUTHORSHIP.md +26 -0
  7. package/docs/BENCHMARKS.md +261 -0
  8. package/docs/BOOTSTRAP.md +127 -0
  9. package/docs/BOTS.md +79 -0
  10. package/docs/COVERAGE.md +64 -0
  11. package/docs/DEPLOY.md +246 -0
  12. package/docs/DISCIPLINES.md +29 -0
  13. package/docs/GUARDRAILS-AND-MONORAILS.md +107 -0
  14. package/docs/GUIDED-SETUP.md +296 -0
  15. package/docs/PERMISSIONS.md +118 -0
  16. package/docs/ROADMAP.md +59 -0
  17. package/docs/SETUP.md +259 -0
  18. package/guides/NEURODIVERGENT_SUPPORT.md +278 -0
  19. package/package.json +70 -4
  20. package/server.json +39 -0
  21. package/src/aleph.js +395 -0
  22. package/src/annotations.js +135 -0
  23. package/src/approve-throttle.js +54 -0
  24. package/src/atlas-role.js +58 -0
  25. package/src/conformance.js +688 -0
  26. package/src/content_analyzer.js +450 -0
  27. package/src/database.js +811 -0
  28. package/src/disciplines.js +26 -0
  29. package/src/dispatch.js +279 -0
  30. package/src/env.js +52 -0
  31. package/src/floor.js +187 -0
  32. package/src/gaps.js +476 -0
  33. package/src/guides.js +48 -0
  34. package/src/http-remote.js +269 -0
  35. package/src/index.js +390 -0
  36. package/src/invites.js +167 -0
  37. package/src/layers.js +449 -0
  38. package/src/lessons-to-checks.js +366 -0
  39. package/src/logger.js +299 -0
  40. package/src/metadata.js +61 -0
  41. package/src/module-path.js +32 -0
  42. package/src/mongo-connect.js +48 -0
  43. package/src/named-items.js +208 -0
  44. package/src/oauth-provider.js +838 -0
  45. package/src/persons.js +116 -0
  46. package/src/provenance.js +257 -0
  47. package/src/redact.js +231 -0
  48. package/src/request-log.js +124 -0
  49. package/src/scopes.js +97 -0
  50. package/src/security-headers.js +26 -0
  51. package/src/session-graph.js +945 -0
  52. package/src/session.js +1097 -0
  53. package/src/setup.js +1313 -0
  54. package/src/startup-pages.js +354 -0
  55. package/src/startup-payload.js +126 -0
  56. package/src/tools.js +4271 -0
  57. package/src/transcript-session.js +106 -0
  58. package/src/validation.js +1163 -0
  59. package/src/version.js +2 -0
  60. package/templates/BOARD.md +72 -0
  61. package/templates/BOTHANDOFF.md +57 -0
  62. package/templates/CHARACTER.md +30 -0
  63. package/templates/HANDOFF.md +63 -0
  64. package/templates/IDENTITY.md +29 -0
  65. package/templates/INDEX.md +82 -0
  66. package/templates/INSTALL.md +29 -0
  67. package/templates/PROCEDURE.md +39 -0
  68. package/templates/ROSTER.md +43 -0
  69. package/templates/SEED.md +65 -0
  70. package/templates/STAGING.md +37 -0
  71. package/templates/STANDARD.md +99 -0
  72. package/templates/THREAD.md +43 -0
  73. 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
+ }