@crouton-kit/crouter 0.3.39 → 0.3.41
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/build-root.js +1 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +472 -9
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +284 -76
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +31 -24
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +52 -15
- package/dist/clients/attach/attach-cmd.js +706 -733
- package/dist/clients/attach/chat-view.js +11 -0
- package/dist/clients/web/server.js +5 -28
- package/dist/clients/web/web-cmd.js +1 -1
- package/dist/commands/__tests__/revive-now-gate.test.js +60 -0
- package/dist/commands/canvas-rebuild-index.js +5 -5
- package/dist/commands/node-snapshot.js +9 -1
- package/dist/commands/node.js +37 -17
- package/dist/commands/push.js +8 -0
- package/dist/commands/revive.d.ts +10 -0
- package/dist/commands/revive.js +28 -10
- package/dist/commands/sys/__tests__/setup-core.test.js +19 -0
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/setup-core.js +3 -2
- package/dist/commands/sys/setup.js +1 -1
- package/dist/commands/worktree.d.ts +2 -0
- package/dist/commands/worktree.js +94 -0
- package/dist/core/__tests__/boot.test.js +4 -4
- package/dist/core/__tests__/canvas.test.js +19 -7
- package/dist/core/__tests__/child-followup.test.js +15 -5
- package/dist/core/__tests__/daemon-boot.test.js +6 -1
- package/dist/core/__tests__/daemon-wedge.test.js +18 -1
- package/dist/core/__tests__/fault-classifier.test.js +30 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +20 -0
- package/dist/core/__tests__/full/spike-harness.test.js +8 -5
- package/dist/core/__tests__/grace-clock.test.js +18 -2
- package/dist/core/__tests__/host-teardown-process-group.test.js +465 -0
- package/dist/core/__tests__/prune-to-limit.test.js +14 -0
- package/dist/core/__tests__/review-model-floor.test.js +32 -0
- package/dist/core/__tests__/revive.test.js +18 -4
- package/dist/core/__tests__/session-cycles.test.js +77 -0
- package/dist/core/__tests__/worktree.test.js +85 -0
- package/dist/core/canvas/boot.js +12 -7
- package/dist/core/canvas/canvas.d.ts +27 -8
- package/dist/core/canvas/canvas.js +54 -26
- package/dist/core/canvas/db.js +14 -0
- package/dist/core/canvas/history.js +1 -0
- package/dist/core/canvas/paths.d.ts +10 -9
- package/dist/core/canvas/paths.js +10 -9
- package/dist/core/canvas/pid.d.ts +155 -1
- package/dist/core/canvas/pid.js +306 -1
- package/dist/core/canvas/status-glyph.d.ts +7 -0
- package/dist/core/canvas/status-glyph.js +10 -1
- package/dist/core/canvas/types.d.ts +34 -0
- package/dist/core/fault-classifier.js +5 -1
- package/dist/core/runtime/bearings.js +4 -0
- package/dist/core/runtime/branded-host.d.ts +7 -0
- package/dist/core/runtime/branded-host.js +44 -17
- package/dist/core/runtime/broker-sdk.js +28 -68
- package/dist/core/runtime/broker.js +35 -4
- package/dist/core/runtime/host.d.ts +3 -3
- package/dist/core/runtime/host.js +148 -33
- package/dist/core/runtime/launch.d.ts +15 -15
- package/dist/core/runtime/launch.js +58 -4
- package/dist/core/runtime/naming.js +3 -2
- package/dist/core/runtime/nodes.d.ts +3 -1
- package/dist/core/runtime/nodes.js +1 -0
- package/dist/core/runtime/pi-cli.d.ts +2 -0
- package/dist/core/runtime/pi-cli.js +51 -0
- package/dist/core/runtime/placement.d.ts +1 -1
- package/dist/core/runtime/placement.js +5 -2
- package/dist/core/runtime/recap.js +2 -1
- package/dist/core/runtime/revive.d.ts +21 -15
- package/dist/core/runtime/revive.js +87 -52
- package/dist/core/runtime/session-cycles.d.ts +30 -0
- package/dist/core/runtime/session-cycles.js +77 -0
- package/dist/core/runtime/spawn.d.ts +4 -0
- package/dist/core/runtime/spawn.js +152 -100
- package/dist/core/runtime/tmux.d.ts +7 -10
- package/dist/core/runtime/tmux.js +9 -11
- package/dist/core/worktree.d.ts +35 -0
- package/dist/core/worktree.js +158 -0
- package/dist/daemon/crtrd.d.ts +17 -8
- package/dist/daemon/crtrd.js +191 -40
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +1 -0
- package/dist/pi-extensions/canvas-context-intro.js +34 -23
- package/dist/pi-extensions/canvas-stophook.js +11 -5
- package/dist/web-client/assets/{index-CbO8L0mN.js → index-B00YpRQ1.js} +20 -20
- package/dist/web-client/assets/index-DrkcvANq.css +2 -0
- package/dist/web-client/index.html +2 -2
- package/docs/compat/hearth-crtr-v1.md +191 -0
- package/docs/public-api.md +75 -0
- package/package.json +3 -4
- package/dist/core/__tests__/hearth-bootstrap.test.js +0 -136
- package/dist/core/hearth/__tests__/model-auth-guest.test.js +0 -151
- package/dist/core/hearth/config.d.ts +0 -3
- package/dist/core/hearth/config.js +0 -108
- package/dist/core/hearth/guest-env.d.ts +0 -9
- package/dist/core/hearth/guest-env.js +0 -27
- package/dist/core/hearth/index.d.ts +0 -4
- package/dist/core/hearth/index.js +0 -4
- package/dist/core/hearth/model-auth-guest.d.ts +0 -8
- package/dist/core/hearth/model-auth-guest.js +0 -430
- package/dist/core/hearth/provider.d.ts +0 -36
- package/dist/core/hearth/provider.js +0 -10
- package/dist/core/hearth/providers/__tests__/sweep-and-release.test.js +0 -254
- package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +0 -12
- package/dist/core/hearth/providers/blaxel-bootstrap.js +0 -147
- package/dist/core/hearth/providers/blaxel-home.d.ts +0 -54
- package/dist/core/hearth/providers/blaxel-home.js +0 -386
- package/dist/core/hearth/providers/blaxel.d.ts +0 -36
- package/dist/core/hearth/providers/blaxel.js +0 -364
- package/dist/core/hearth/providers/types.d.ts +0 -93
- package/dist/core/hearth/types.d.ts +0 -155
- package/dist/hearth/control-plane/__tests__/error-serialization.test.js +0 -29
- package/dist/hearth/control-plane/__tests__/node-message.test.js +0 -60
- package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.js +0 -44
- package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.js +0 -49
- package/dist/hearth/control-plane/__tests__/relay-security.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/relay-security.test.js +0 -314
- package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.js +0 -133
- package/dist/hearth/control-plane/__tests__/trigger-delivery.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/trigger-delivery.test.js +0 -170
- package/dist/hearth/control-plane/__tests__/wake-roll.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/wake-roll.test.js +0 -230
- package/dist/hearth/control-plane/__tests__/webhook-ingress.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/webhook-ingress.test.js +0 -167
- package/dist/hearth/control-plane/config.d.ts +0 -21
- package/dist/hearth/control-plane/config.js +0 -77
- package/dist/hearth/control-plane/db.d.ts +0 -30
- package/dist/hearth/control-plane/db.js +0 -561
- package/dist/hearth/control-plane/hearth-target.d.ts +0 -23
- package/dist/hearth/control-plane/hearth-target.js +0 -68
- package/dist/hearth/control-plane/ingress/rate-limit.d.ts +0 -24
- package/dist/hearth/control-plane/ingress/rate-limit.js +0 -100
- package/dist/hearth/control-plane/ingress/route-store.d.ts +0 -31
- package/dist/hearth/control-plane/ingress/route-store.js +0 -61
- package/dist/hearth/control-plane/ingress/webhook-delivery-store.d.ts +0 -41
- package/dist/hearth/control-plane/ingress/webhook-delivery-store.js +0 -69
- package/dist/hearth/control-plane/ingress/webhook-route.d.ts +0 -55
- package/dist/hearth/control-plane/ingress/webhook-route.js +0 -285
- package/dist/hearth/control-plane/main.d.ts +0 -1
- package/dist/hearth/control-plane/main.js +0 -88
- package/dist/hearth/control-plane/node-message.d.ts +0 -31
- package/dist/hearth/control-plane/node-message.js +0 -98
- package/dist/hearth/control-plane/register.d.ts +0 -15
- package/dist/hearth/control-plane/register.js +0 -34
- package/dist/hearth/control-plane/registry.d.ts +0 -22
- package/dist/hearth/control-plane/registry.js +0 -168
- package/dist/hearth/control-plane/relay.d.ts +0 -44
- package/dist/hearth/control-plane/relay.js +0 -711
- package/dist/hearth/control-plane/scheduler/fire-store.d.ts +0 -36
- package/dist/hearth/control-plane/scheduler/fire-store.js +0 -73
- package/dist/hearth/control-plane/scheduler/recurrence.d.ts +0 -7
- package/dist/hearth/control-plane/scheduler/recurrence.js +0 -58
- package/dist/hearth/control-plane/scheduler/scan-loop.d.ts +0 -38
- package/dist/hearth/control-plane/scheduler/scan-loop.js +0 -138
- package/dist/hearth/control-plane/scheduler/schedule-store.d.ts +0 -32
- package/dist/hearth/control-plane/scheduler/schedule-store.js +0 -66
- package/dist/hearth/control-plane/secrets.d.ts +0 -31
- package/dist/hearth/control-plane/secrets.js +0 -134
- package/dist/hearth/control-plane/server.d.ts +0 -27
- package/dist/hearth/control-plane/server.js +0 -482
- package/dist/hearth/control-plane/serving.d.ts +0 -15
- package/dist/hearth/control-plane/serving.js +0 -106
- package/dist/hearth/control-plane/session.d.ts +0 -68
- package/dist/hearth/control-plane/session.js +0 -273
- package/dist/hearth/control-plane/triggers/acl.d.ts +0 -14
- package/dist/hearth/control-plane/triggers/acl.js +0 -52
- package/dist/hearth/control-plane/triggers/audit-store.d.ts +0 -38
- package/dist/hearth/control-plane/triggers/audit-store.js +0 -79
- package/dist/hearth/control-plane/triggers/deliver.d.ts +0 -43
- package/dist/hearth/control-plane/triggers/deliver.js +0 -76
- package/dist/hearth/control-plane/triggers/envelope.d.ts +0 -29
- package/dist/hearth/control-plane/triggers/envelope.js +0 -38
- package/dist/hearth/control-plane/types.d.ts +0 -86
- package/dist/hearth/control-plane/types.js +0 -1
- package/dist/hearth/control-plane/wake.d.ts +0 -86
- package/dist/hearth/control-plane/wake.js +0 -550
- package/dist/web-client/assets/index-DwO46Cs5.css +0 -2
- /package/dist/{core/__tests__/hearth-bootstrap.test.d.ts → commands/__tests__/revive-now-gate.test.d.ts} +0 -0
- /package/dist/{core/hearth/__tests__/model-auth-guest.test.d.ts → commands/sys/__tests__/setup-core.test.d.ts} +0 -0
- /package/dist/core/{hearth/providers/__tests__/sweep-and-release.test.d.ts → __tests__/fault-classifier.test.d.ts} +0 -0
- /package/dist/core/{hearth/providers/types.js → __tests__/host-teardown-process-group.test.d.ts} +0 -0
- /package/dist/core/{hearth/types.js → __tests__/review-model-floor.test.d.ts} +0 -0
- /package/dist/{hearth/control-plane/__tests__/error-serialization.test.d.ts → core/__tests__/session-cycles.test.d.ts} +0 -0
- /package/dist/{hearth/control-plane/__tests__/node-message.test.d.ts → core/__tests__/worktree.test.d.ts} +0 -0
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
|
-
import { general } from '../../core/errors.js';
|
|
3
|
-
export const HEARTH_SESSION_COOKIE = 'hearth_session';
|
|
4
|
-
const SESSION_TOKENS_ENV = 'CRTR_HEARTH_CP_SESSION_TOKENS';
|
|
5
|
-
// Real Hearth login seam (not implemented in this cutover increment): the
|
|
6
|
-
// future login/issuance flow plugs in by creating or resolving a tenant and
|
|
7
|
-
// minting one of the token records consumed by `createTenantSessionStore`.
|
|
8
|
-
// This module only validates pre-provisioned records and performs the optional
|
|
9
|
-
// one-time query-token-to-cookie swap for the M0 handoff.
|
|
10
|
-
function sha256Hex(token) {
|
|
11
|
-
return createHash('sha256').update(token, 'utf8').digest('hex');
|
|
12
|
-
}
|
|
13
|
-
function normalizeHash(hash) {
|
|
14
|
-
const trimmed = hash.trim();
|
|
15
|
-
const body = trimmed.startsWith('sha256:') ? trimmed.slice('sha256:'.length) : trimmed;
|
|
16
|
-
return /^[0-9a-fA-F]{64}$/.test(body) ? body.toLowerCase() : '';
|
|
17
|
-
}
|
|
18
|
-
function tokenHash(seed, field) {
|
|
19
|
-
const fromHash = seed.token_hash === undefined ? '' : normalizeHash(seed.token_hash);
|
|
20
|
-
if (fromHash !== '')
|
|
21
|
-
return fromHash;
|
|
22
|
-
const token = seed.token?.trim() ?? '';
|
|
23
|
-
if (token !== '')
|
|
24
|
-
return sha256Hex(token);
|
|
25
|
-
throw general(`${field} requires token or token_hash`);
|
|
26
|
-
}
|
|
27
|
-
function optionalQueryTokenHash(seed) {
|
|
28
|
-
if (seed.query_token_hash !== undefined) {
|
|
29
|
-
const normalized = normalizeHash(seed.query_token_hash);
|
|
30
|
-
if (normalized === '')
|
|
31
|
-
throw general('query_token_hash must be a sha256 hash');
|
|
32
|
-
return normalized;
|
|
33
|
-
}
|
|
34
|
-
const token = seed.query_token?.trim() ?? '';
|
|
35
|
-
return token === '' ? null : sha256Hex(token);
|
|
36
|
-
}
|
|
37
|
-
function nonEmpty(name, value) {
|
|
38
|
-
const trimmed = value.trim();
|
|
39
|
-
if (trimmed === '')
|
|
40
|
-
throw general(`${name} must not be empty`, { field: name });
|
|
41
|
-
return trimmed;
|
|
42
|
-
}
|
|
43
|
-
export function createTenantSessionStore(seeds) {
|
|
44
|
-
const sessionHashes = new Set();
|
|
45
|
-
const queryHashes = new Set();
|
|
46
|
-
const records = seeds.map((seed) => {
|
|
47
|
-
const normalized = {
|
|
48
|
-
tenant_id: nonEmpty('tenant_id', seed.tenant_id),
|
|
49
|
-
user_id: nonEmpty('user_id', seed.user_id),
|
|
50
|
-
token_hash: tokenHash(seed, 'tenant session token'),
|
|
51
|
-
token: seed.token?.trim() || null,
|
|
52
|
-
query_token_hash: optionalQueryTokenHash(seed),
|
|
53
|
-
};
|
|
54
|
-
if (sessionHashes.has(normalized.token_hash))
|
|
55
|
-
throw general('duplicate tenant session token hash');
|
|
56
|
-
sessionHashes.add(normalized.token_hash);
|
|
57
|
-
if (normalized.query_token_hash !== null) {
|
|
58
|
-
if (queryHashes.has(normalized.query_token_hash))
|
|
59
|
-
throw general('duplicate tenant query token hash');
|
|
60
|
-
queryHashes.add(normalized.query_token_hash);
|
|
61
|
-
}
|
|
62
|
-
return normalized;
|
|
63
|
-
});
|
|
64
|
-
return { records, consumed_query_token_hashes: new Set() };
|
|
65
|
-
}
|
|
66
|
-
let envStoreCache = null;
|
|
67
|
-
export function loadTenantSessionStore(env = process.env) {
|
|
68
|
-
const raw = env[SESSION_TOKENS_ENV];
|
|
69
|
-
if (envStoreCache !== null && envStoreCache.raw === raw)
|
|
70
|
-
return envStoreCache.store;
|
|
71
|
-
if (raw === undefined || raw.trim() === '') {
|
|
72
|
-
const empty = createTenantSessionStore([]);
|
|
73
|
-
envStoreCache = { raw, store: empty };
|
|
74
|
-
return empty;
|
|
75
|
-
}
|
|
76
|
-
let parsed;
|
|
77
|
-
try {
|
|
78
|
-
parsed = JSON.parse(raw);
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
throw general(`${SESSION_TOKENS_ENV} must be JSON`);
|
|
82
|
-
}
|
|
83
|
-
if (!Array.isArray(parsed))
|
|
84
|
-
throw general(`${SESSION_TOKENS_ENV} must be a JSON array`);
|
|
85
|
-
const store = createTenantSessionStore(parsed);
|
|
86
|
-
envStoreCache = { raw, store };
|
|
87
|
-
return store;
|
|
88
|
-
}
|
|
89
|
-
function parseCookieHeader(header) {
|
|
90
|
-
const out = {};
|
|
91
|
-
if (header === undefined || header.trim() === '')
|
|
92
|
-
return out;
|
|
93
|
-
for (const part of header.split(';')) {
|
|
94
|
-
const idx = part.indexOf('=');
|
|
95
|
-
if (idx <= 0)
|
|
96
|
-
continue;
|
|
97
|
-
const key = part.slice(0, idx).trim();
|
|
98
|
-
const value = part.slice(idx + 1).trim();
|
|
99
|
-
if (key !== '')
|
|
100
|
-
out[key] = value;
|
|
101
|
-
}
|
|
102
|
-
return out;
|
|
103
|
-
}
|
|
104
|
-
function requestUrl(req) {
|
|
105
|
-
return new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
106
|
-
}
|
|
107
|
-
function queryToken(req) {
|
|
108
|
-
const token = requestUrl(req).searchParams.get('token')?.trim() ?? '';
|
|
109
|
-
return token === '' ? null : token;
|
|
110
|
-
}
|
|
111
|
-
function bearerToken(req) {
|
|
112
|
-
const authorization = req.headers.authorization;
|
|
113
|
-
if (typeof authorization !== 'string')
|
|
114
|
-
return null;
|
|
115
|
-
const match = /^Bearer\s+(.+)$/.exec(authorization.trim());
|
|
116
|
-
const token = match?.[1]?.trim() ?? '';
|
|
117
|
-
return token === '' ? null : token;
|
|
118
|
-
}
|
|
119
|
-
function cookieToken(req) {
|
|
120
|
-
const token = parseCookieHeader(req.headers.cookie)[HEARTH_SESSION_COOKIE];
|
|
121
|
-
if (token === undefined || token.trim() === '')
|
|
122
|
-
return null;
|
|
123
|
-
try {
|
|
124
|
-
return decodeURIComponent(token.trim());
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
function constantTimeMatch(expectedHash, candidateHash) {
|
|
131
|
-
if (expectedHash === '')
|
|
132
|
-
return false;
|
|
133
|
-
return timingSafeEqual(Buffer.from(expectedHash, 'hex'), Buffer.from(candidateHash, 'hex'));
|
|
134
|
-
}
|
|
135
|
-
function lookupBySessionToken(token, store) {
|
|
136
|
-
const trimmed = token.trim();
|
|
137
|
-
if (trimmed === '')
|
|
138
|
-
return null;
|
|
139
|
-
const candidate = sha256Hex(trimmed);
|
|
140
|
-
let match = null;
|
|
141
|
-
for (const record of store.records) {
|
|
142
|
-
if (constantTimeMatch(record.token_hash, candidate)) {
|
|
143
|
-
match = { tenant_id: record.tenant_id, user_id: record.user_id };
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return match;
|
|
147
|
-
}
|
|
148
|
-
function lookupByQueryToken(token, store) {
|
|
149
|
-
const trimmed = token.trim();
|
|
150
|
-
if (trimmed === '')
|
|
151
|
-
return null;
|
|
152
|
-
const candidate = sha256Hex(trimmed);
|
|
153
|
-
let match = null;
|
|
154
|
-
for (const record of store.records) {
|
|
155
|
-
if (record.query_token_hash !== null && constantTimeMatch(record.query_token_hash, candidate)) {
|
|
156
|
-
match = record;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return match;
|
|
160
|
-
}
|
|
161
|
-
function trustedOrigin(req, publicOrigin) {
|
|
162
|
-
const expected = publicOrigin.trim();
|
|
163
|
-
if (expected === '')
|
|
164
|
-
throw general('publicOrigin is required for tenant session origin checks');
|
|
165
|
-
const origin = req.headers.origin?.trim() ?? '';
|
|
166
|
-
return origin !== '' && origin === expected;
|
|
167
|
-
}
|
|
168
|
-
/** Origin policy for the one-time query-token swap on a safe top-level
|
|
169
|
-
* GET/HEAD navigation. Ordinary top-level navigations (the only way this
|
|
170
|
-
* swap is reachable — see `swapQueryTokenForCookie`) do not reliably carry
|
|
171
|
-
* an `Origin` header at all, so requiring one regressed the M0 open-link
|
|
172
|
-
* handoff into an unusable 401. The token itself is a one-time secret: a
|
|
173
|
-
* party that can already supply the correct `?token=` value has already
|
|
174
|
-
* obtained the secret, so Origin adds no protection against that. If a
|
|
175
|
-
* browser-style `Origin` header IS present (e.g. a cross-origin fetch/XHR
|
|
176
|
-
* rather than a plain top-level navigation), it must still match
|
|
177
|
-
* `publicOrigin` exactly — this only relaxes the "no Origin header at all"
|
|
178
|
-
* case, it does not accept a mismatched one. */
|
|
179
|
-
function originAcceptableForQueryTokenSwap(req, publicOrigin) {
|
|
180
|
-
const origin = req.headers.origin?.trim() ?? '';
|
|
181
|
-
if (origin === '')
|
|
182
|
-
return true;
|
|
183
|
-
return origin === publicOrigin.trim();
|
|
184
|
-
}
|
|
185
|
-
export function setSessionCookie(res, token, options = {}) {
|
|
186
|
-
const maxAge = options.maxAgeSeconds ?? 365 * 24 * 60 * 60;
|
|
187
|
-
const path = options.path ?? '/';
|
|
188
|
-
const sameSite = options.sameSite ?? 'Lax';
|
|
189
|
-
const secure = options.secure ?? true;
|
|
190
|
-
const parts = [
|
|
191
|
-
`${HEARTH_SESSION_COOKIE}=${encodeURIComponent(token)}`,
|
|
192
|
-
`Path=${path}`,
|
|
193
|
-
'HttpOnly',
|
|
194
|
-
`SameSite=${sameSite}`,
|
|
195
|
-
`Max-Age=${maxAge}`,
|
|
196
|
-
];
|
|
197
|
-
if (secure)
|
|
198
|
-
parts.push('Secure');
|
|
199
|
-
res.setHeader('Set-Cookie', parts.join('; '));
|
|
200
|
-
}
|
|
201
|
-
export function clearSessionCookie(res, options = {}) {
|
|
202
|
-
setSessionCookie(res, '', { ...options, maxAgeSeconds: 0 });
|
|
203
|
-
}
|
|
204
|
-
/** Validate durable session credentials only: HttpOnly cookie for browsers or
|
|
205
|
-
* Bearer for non-browser clients. Query tokens are intentionally ignored here
|
|
206
|
-
* so a copied `?token=` URL is never durable authorization. */
|
|
207
|
-
export function resolveTenantSession(req, options = {}) {
|
|
208
|
-
const store = options.store ?? loadTenantSessionStore();
|
|
209
|
-
const cookie = cookieToken(req);
|
|
210
|
-
if (cookie !== null) {
|
|
211
|
-
const session = lookupBySessionToken(cookie, store);
|
|
212
|
-
return session === null ? null : { ...session, source: 'cookie' };
|
|
213
|
-
}
|
|
214
|
-
const bearer = bearerToken(req);
|
|
215
|
-
if (bearer !== null) {
|
|
216
|
-
if ((req.headers.origin?.trim() ?? '') !== '')
|
|
217
|
-
return null;
|
|
218
|
-
const session = lookupBySessionToken(bearer, store);
|
|
219
|
-
return session === null ? null : { ...session, source: 'bearer' };
|
|
220
|
-
}
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
/** State-changing browser lanes require a valid session AND a trusted Origin;
|
|
224
|
-
* query tokens are not accepted here. Bearer remains available only to
|
|
225
|
-
* non-browser clients (no Origin header). */
|
|
226
|
-
export function resolveTenantWriteAuth(req, options) {
|
|
227
|
-
const auth = resolveTenantSession(req, options);
|
|
228
|
-
if (auth === null)
|
|
229
|
-
return null;
|
|
230
|
-
if (auth.source === 'cookie')
|
|
231
|
-
return trustedOrigin(req, options.publicOrigin) ? auth : null;
|
|
232
|
-
return auth.source === 'bearer' ? auth : null;
|
|
233
|
-
}
|
|
234
|
-
/** Consume an optional M0 handoff `?token=` exactly once and swap it to the
|
|
235
|
-
* HttpOnly/Secure/SameSite Hearth session cookie. This is the only query-token
|
|
236
|
-
* path. Safe top-level navigations (`GET`/`HEAD`) are permitted without a
|
|
237
|
-
* matching `Origin` header (see `originAcceptableForQueryTokenSwap`); any
|
|
238
|
-
* other method still requires a trusted Origin, matching the write-lane
|
|
239
|
-
* policy. Returns null on replay, invalid token, missing token, or a
|
|
240
|
-
* rejected Origin. */
|
|
241
|
-
export function swapQueryTokenForCookie(req, res, options) {
|
|
242
|
-
const method = req.method ?? 'GET';
|
|
243
|
-
const isSafeTopLevelNavigation = method === 'GET' || method === 'HEAD';
|
|
244
|
-
const originOk = isSafeTopLevelNavigation
|
|
245
|
-
? originAcceptableForQueryTokenSwap(req, options.publicOrigin)
|
|
246
|
-
: trustedOrigin(req, options.publicOrigin);
|
|
247
|
-
if (!originOk)
|
|
248
|
-
return null;
|
|
249
|
-
const store = options.store ?? loadTenantSessionStore();
|
|
250
|
-
const token = queryToken(req);
|
|
251
|
-
if (token === null)
|
|
252
|
-
return null;
|
|
253
|
-
const record = lookupByQueryToken(token, store);
|
|
254
|
-
if (record === null || record.query_token_hash === null)
|
|
255
|
-
return null;
|
|
256
|
-
if (store.consumed_query_token_hashes.has(record.query_token_hash))
|
|
257
|
-
return null;
|
|
258
|
-
if (record.token === null) {
|
|
259
|
-
throw general('query token swap requires a raw pre-provisioned session token for Set-Cookie');
|
|
260
|
-
}
|
|
261
|
-
store.consumed_query_token_hashes.add(record.query_token_hash);
|
|
262
|
-
setSessionCookie(res, record.token, options.cookie);
|
|
263
|
-
return { tenant_id: record.tenant_id, user_id: record.user_id, source: 'query-swap' };
|
|
264
|
-
}
|
|
265
|
-
export function tenantSessionAuthRedactedMessage(message) {
|
|
266
|
-
return message
|
|
267
|
-
.replace(/(Authorization:\s*Bearer\s+)[^\s]+/gi, '$1[redacted]')
|
|
268
|
-
.replace(/([?&]token=)[^&#\s]+/gi, '$1[redacted]')
|
|
269
|
-
.replace(new RegExp(`(${HEARTH_SESSION_COOKIE}=)[^;\\s]+`, 'gi'), '$1[redacted]');
|
|
270
|
-
}
|
|
271
|
-
export function resetTenantSessionStoreCacheForTests() {
|
|
272
|
-
envStoreCache = null;
|
|
273
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { HomeId } from '../types.js';
|
|
2
|
-
import type { TriggerSource } from './envelope.js';
|
|
3
|
-
export interface AuthorizeTriggerResult {
|
|
4
|
-
ok: boolean;
|
|
5
|
-
reason?: string;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Structural authorization for delivering `source`/`sourceRef` to `homeId`.
|
|
9
|
-
* Rejects when the source row is absent, when the target `homeId` differs from
|
|
10
|
-
* the source-bound home (cross-home targeting), or when the target home is
|
|
11
|
-
* absent or terminated. Returns `{ ok: true }` only for a source whose FK
|
|
12
|
-
* home matches the target and resolves to a live home.
|
|
13
|
-
*/
|
|
14
|
-
export declare function authorizeTrigger(source: TriggerSource, sourceRef: string, homeId: HomeId): AuthorizeTriggerResult;
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// control-plane/triggers/acl.ts — the R-TRIG-4 STRUCTURAL trigger authorization
|
|
2
|
-
// guard (master plan P5.2). This is the FK-integrity check only: "which source
|
|
3
|
-
// may target which home". The permission/ownership POLICY beyond this
|
|
4
|
-
// (per-user grants, capability scoping, etc.) is explicitly post-cutover
|
|
5
|
-
// (part-plan gap note 11) and is NOT implemented here.
|
|
6
|
-
//
|
|
7
|
-
// The guard: a trigger's bound source row (`schedule_fire` for cron,
|
|
8
|
-
// `ingress_route` for webhook) carries a `home_id` FK. A delivery is
|
|
9
|
-
// authorized only when the target `home_id` EQUALS that source-bound home_id
|
|
10
|
-
// (no cross-home targeting) AND that home resolves to an existing,
|
|
11
|
-
// non-terminated `home` row. Anything else is rejected before any wake/exec.
|
|
12
|
-
import { getControlPlaneDb } from '../db.js';
|
|
13
|
-
import { getHomeById } from '../registry.js';
|
|
14
|
-
/** Terminated/terminating homes are not valid delivery targets — a trigger
|
|
15
|
-
* must never wake or exec against a home that is being torn down. */
|
|
16
|
-
const DEAD_HOME_STATUSES = new Set(['terminating', 'terminated']);
|
|
17
|
-
/** Read the `home_id` a source row (`schedule_fire.fire_id` for cron,
|
|
18
|
-
* `ingress_route.route_id` for webhook) is bound to. `null` when the source
|
|
19
|
-
* row does not exist — a hard reject (no home to target). */
|
|
20
|
-
function sourceBoundHomeId(source, sourceRef) {
|
|
21
|
-
const db = getControlPlaneDb();
|
|
22
|
-
if (source === 'cron') {
|
|
23
|
-
const row = db.prepare('SELECT home_id FROM schedule_fire WHERE fire_id = ?').get(sourceRef);
|
|
24
|
-
return row === undefined ? null : row.home_id;
|
|
25
|
-
}
|
|
26
|
-
const row = db.prepare('SELECT home_id FROM ingress_route WHERE route_id = ?').get(sourceRef);
|
|
27
|
-
return row === undefined ? null : row.home_id;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Structural authorization for delivering `source`/`sourceRef` to `homeId`.
|
|
31
|
-
* Rejects when the source row is absent, when the target `homeId` differs from
|
|
32
|
-
* the source-bound home (cross-home targeting), or when the target home is
|
|
33
|
-
* absent or terminated. Returns `{ ok: true }` only for a source whose FK
|
|
34
|
-
* home matches the target and resolves to a live home.
|
|
35
|
-
*/
|
|
36
|
-
export function authorizeTrigger(source, sourceRef, homeId) {
|
|
37
|
-
const boundHomeId = sourceBoundHomeId(source, sourceRef);
|
|
38
|
-
if (boundHomeId === null) {
|
|
39
|
-
return { ok: false, reason: 'source not found' };
|
|
40
|
-
}
|
|
41
|
-
if (boundHomeId !== homeId) {
|
|
42
|
-
return { ok: false, reason: 'cross-home targeting rejected' };
|
|
43
|
-
}
|
|
44
|
-
const home = getHomeById(homeId);
|
|
45
|
-
if (home === null) {
|
|
46
|
-
return { ok: false, reason: 'target home not found' };
|
|
47
|
-
}
|
|
48
|
-
if (DEAD_HOME_STATUSES.has(home.status)) {
|
|
49
|
-
return { ok: false, reason: `target home ${home.status}` };
|
|
50
|
-
}
|
|
51
|
-
return { ok: true };
|
|
52
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export type TriggerSource = 'cron' | 'webhook';
|
|
2
|
-
export type TriggerTier = 'critical' | 'urgent' | 'normal' | 'deferred';
|
|
3
|
-
export type TriggerOutcome = 'delivered' | 'rejected' | 'failed';
|
|
4
|
-
export interface TriggerAuditRow {
|
|
5
|
-
audit_id: string;
|
|
6
|
-
tenant_id: string;
|
|
7
|
-
user_id: string;
|
|
8
|
-
home_id: string;
|
|
9
|
-
source: TriggerSource;
|
|
10
|
-
source_ref: string;
|
|
11
|
-
trigger_id: string;
|
|
12
|
-
tier: TriggerTier;
|
|
13
|
-
outcome: TriggerOutcome;
|
|
14
|
-
reason: string | null;
|
|
15
|
-
payload_sha256: string | null;
|
|
16
|
-
metadata_json: Record<string, unknown> | null;
|
|
17
|
-
at: string;
|
|
18
|
-
}
|
|
19
|
-
export interface RecordTriggerAuditInput {
|
|
20
|
-
audit_id?: string;
|
|
21
|
-
source: TriggerSource;
|
|
22
|
-
source_ref: string;
|
|
23
|
-
trigger_id: string;
|
|
24
|
-
tier: TriggerTier;
|
|
25
|
-
outcome: TriggerOutcome;
|
|
26
|
-
reason?: string | null;
|
|
27
|
-
payload_sha256?: string | null;
|
|
28
|
-
metadata_json?: Record<string, unknown> | null;
|
|
29
|
-
at?: string;
|
|
30
|
-
}
|
|
31
|
-
export interface ListTriggerAuditOptions {
|
|
32
|
-
from?: string;
|
|
33
|
-
to?: string;
|
|
34
|
-
limit?: number;
|
|
35
|
-
}
|
|
36
|
-
export declare function recordTriggerAudit(input: RecordTriggerAuditInput): TriggerAuditRow;
|
|
37
|
-
export declare function getTriggerAuditById(auditId: string): TriggerAuditRow | null;
|
|
38
|
-
export declare function listTriggerAudit(homeId: string, options?: ListTriggerAuditOptions): TriggerAuditRow[];
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { getControlPlaneDb } from '../db.js';
|
|
3
|
-
function nowIso() {
|
|
4
|
-
return new Date().toISOString();
|
|
5
|
-
}
|
|
6
|
-
function sourceIdentity(source, sourceRef) {
|
|
7
|
-
if (source === 'cron') {
|
|
8
|
-
const row = getControlPlaneDb()
|
|
9
|
-
.prepare('SELECT home_id, tenant_id, user_id FROM schedule_fire WHERE fire_id = ?')
|
|
10
|
-
.get(sourceRef);
|
|
11
|
-
if (row === undefined)
|
|
12
|
-
throw new Error(`schedule fire not found: ${sourceRef}`);
|
|
13
|
-
return row;
|
|
14
|
-
}
|
|
15
|
-
const row = getControlPlaneDb()
|
|
16
|
-
.prepare('SELECT home_id, tenant_id, user_id FROM ingress_route WHERE route_id = ?')
|
|
17
|
-
.get(sourceRef);
|
|
18
|
-
if (row === undefined)
|
|
19
|
-
throw new Error(`ingress route not found: ${sourceRef}`);
|
|
20
|
-
return row;
|
|
21
|
-
}
|
|
22
|
-
function auditFrom(row) {
|
|
23
|
-
const metadata = row['metadata_json'];
|
|
24
|
-
return {
|
|
25
|
-
audit_id: row['audit_id'],
|
|
26
|
-
tenant_id: row['tenant_id'],
|
|
27
|
-
user_id: row['user_id'],
|
|
28
|
-
home_id: row['home_id'],
|
|
29
|
-
source: row['source'],
|
|
30
|
-
source_ref: row['source_ref'],
|
|
31
|
-
trigger_id: row['trigger_id'],
|
|
32
|
-
tier: row['tier'],
|
|
33
|
-
outcome: row['outcome'],
|
|
34
|
-
reason: row['reason'] ?? null,
|
|
35
|
-
payload_sha256: row['payload_sha256'] ?? null,
|
|
36
|
-
metadata_json: metadata === null ? null : JSON.parse(metadata),
|
|
37
|
-
at: row['at'],
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
export function recordTriggerAudit(input) {
|
|
41
|
-
const auditId = input.audit_id ?? `audit-${randomUUID()}`;
|
|
42
|
-
const at = input.at ?? nowIso();
|
|
43
|
-
const identity = sourceIdentity(input.source, input.source_ref);
|
|
44
|
-
getControlPlaneDb()
|
|
45
|
-
.prepare(`INSERT INTO trigger_audit (
|
|
46
|
-
audit_id, tenant_id, user_id, home_id, source, source_ref, trigger_id,
|
|
47
|
-
tier, outcome, reason, payload_sha256, metadata_json, at
|
|
48
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
49
|
-
.run(auditId, identity.tenant_id, identity.user_id, identity.home_id, input.source, input.source_ref, input.trigger_id, input.tier, input.outcome, input.reason ?? null, input.payload_sha256 ?? null, input.metadata_json == null ? null : JSON.stringify(input.metadata_json), at);
|
|
50
|
-
return getTriggerAuditById(auditId);
|
|
51
|
-
}
|
|
52
|
-
export function getTriggerAuditById(auditId) {
|
|
53
|
-
const row = getControlPlaneDb().prepare('SELECT * FROM trigger_audit WHERE audit_id = ?').get(auditId);
|
|
54
|
-
return row === undefined ? null : auditFrom(row);
|
|
55
|
-
}
|
|
56
|
-
export function listTriggerAudit(homeId, options = {}) {
|
|
57
|
-
const limit = options.limit ?? 100;
|
|
58
|
-
const db = getControlPlaneDb();
|
|
59
|
-
let rows;
|
|
60
|
-
if (options.from !== undefined && options.to !== undefined) {
|
|
61
|
-
rows = db
|
|
62
|
-
.prepare('SELECT * FROM trigger_audit WHERE home_id = ? AND at >= ? AND at <= ? ORDER BY at DESC LIMIT ?')
|
|
63
|
-
.all(homeId, options.from, options.to, limit);
|
|
64
|
-
}
|
|
65
|
-
else if (options.from !== undefined) {
|
|
66
|
-
rows = db
|
|
67
|
-
.prepare('SELECT * FROM trigger_audit WHERE home_id = ? AND at >= ? ORDER BY at DESC LIMIT ?')
|
|
68
|
-
.all(homeId, options.from, limit);
|
|
69
|
-
}
|
|
70
|
-
else if (options.to !== undefined) {
|
|
71
|
-
rows = db
|
|
72
|
-
.prepare('SELECT * FROM trigger_audit WHERE home_id = ? AND at <= ? ORDER BY at DESC LIMIT ?')
|
|
73
|
-
.all(homeId, options.to, limit);
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
rows = db.prepare('SELECT * FROM trigger_audit WHERE home_id = ? ORDER BY at DESC LIMIT ?').all(homeId, limit);
|
|
77
|
-
}
|
|
78
|
-
return rows.map(auditFrom);
|
|
79
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { HomeId } from '../types.js';
|
|
2
|
-
import { recordTriggerAudit } from './audit-store.js';
|
|
3
|
-
import { authorizeTrigger } from './acl.js';
|
|
4
|
-
import { type TriggerEnvelope } from './envelope.js';
|
|
5
|
-
import { wakeAndExecNodeMessage, type NodeMessageTier } from '../node-message.js';
|
|
6
|
-
export type DeliveryOutcome = 'delivered' | 'rejected' | 'failed';
|
|
7
|
-
export interface DeliverTriggerInput {
|
|
8
|
-
home_id: HomeId;
|
|
9
|
-
/** The source row this delivery is bound to: `schedule_fire.fire_id` (cron)
|
|
10
|
-
* or `ingress_route.route_id` (webhook). Used for both the ACL FK guard and
|
|
11
|
-
* the audit `source_ref`. */
|
|
12
|
-
source_ref: string;
|
|
13
|
-
envelope: TriggerEnvelope;
|
|
14
|
-
tier: NodeMessageTier;
|
|
15
|
-
/** Optional pre-computed payload hash (e.g. webhook raw-body sha256). When
|
|
16
|
-
* omitted, a hash over the serialized envelope payload is recorded. The raw
|
|
17
|
-
* payload itself is NEVER stored. */
|
|
18
|
-
payload_sha256?: string;
|
|
19
|
-
/** Bounded, non-sensitive audit metadata (e.g. byte length, content-type).
|
|
20
|
-
* Never the raw payload. */
|
|
21
|
-
metadata?: Record<string, unknown>;
|
|
22
|
-
}
|
|
23
|
-
export interface DeliverTriggerResult {
|
|
24
|
-
outcome: DeliveryOutcome;
|
|
25
|
-
reason?: string;
|
|
26
|
-
}
|
|
27
|
-
/** Indirection seam so a test/smoke script can fake the guard, the exec
|
|
28
|
-
* primitive, and the audit writer without a real DB/provider. Production
|
|
29
|
-
* never reassigns these. */
|
|
30
|
-
export declare const deliverInternals: {
|
|
31
|
-
authorizeTrigger: typeof authorizeTrigger;
|
|
32
|
-
wakeAndExecNodeMessage: typeof wakeAndExecNodeMessage;
|
|
33
|
-
recordTriggerAudit: typeof recordTriggerAudit;
|
|
34
|
-
};
|
|
35
|
-
/**
|
|
36
|
-
* Deliver `input.envelope` to `input.home_id` through the P4.4 wake+exec
|
|
37
|
-
* primitive, gated by the FK-integrity ACL and recorded in the trigger audit.
|
|
38
|
-
* Returns `{ outcome: 'rejected' }` (no wake/exec) for an unauthorized target,
|
|
39
|
-
* `{ outcome: 'delivered' }` after a successful in-guest `node msg`, or
|
|
40
|
-
* `{ outcome: 'failed' }` when the wake/exec threw. Every path writes exactly
|
|
41
|
-
* one audit row (rejected/delivered/failed).
|
|
42
|
-
*/
|
|
43
|
-
export declare function deliverTrigger(input: DeliverTriggerInput): Promise<DeliverTriggerResult>;
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// control-plane/triggers/deliver.ts — the single SEAM-5 trigger delivery
|
|
2
|
-
// chokepoint (master plan P5.3). BOTH producers (cron scan loop, webhook
|
|
3
|
-
// ingress) deliver through here and only here, so the R-TRIG-4 hooks
|
|
4
|
-
// (authorize + audit) can never be bypassed by a producer.
|
|
5
|
-
//
|
|
6
|
-
// Flow: (1) `authorizeTrigger` (FK-integrity guard, acl.ts) — an unauthorized
|
|
7
|
-
// trigger records a `rejected` audit row and returns WITHOUT ever waking or
|
|
8
|
-
// exec'ing a home; (2) on authorize, call the P4.4 primitive
|
|
9
|
-
// `wakeAndExecNodeMessage(home_id, serializeEnvelope(envelope), tier)` — which
|
|
10
|
-
// wakes the home FIRST, then runs `crtr node msg --to <target> --tier <tier>`
|
|
11
|
-
// in-guest with the serialized envelope on STDIN (never in argv/heredoc/
|
|
12
|
-
// base64); (3) record a `delivered` or `failed` audit row.
|
|
13
|
-
//
|
|
14
|
-
// This module NEVER touches provider exec directly and NEVER writes a home's
|
|
15
|
-
// inbox file — the P4.4 primitive is the only delivery path. It also never
|
|
16
|
-
// persists the raw untrusted payload: the audit row carries a content HASH and
|
|
17
|
-
// bounded metadata only (R-CP-8 / audit-is-metadata-only).
|
|
18
|
-
import { createHash } from 'node:crypto';
|
|
19
|
-
import { redactSecrets } from '../secrets.js';
|
|
20
|
-
import { recordTriggerAudit } from './audit-store.js';
|
|
21
|
-
import { authorizeTrigger } from './acl.js';
|
|
22
|
-
import { serializeEnvelope } from './envelope.js';
|
|
23
|
-
import { wakeAndExecNodeMessage } from '../node-message.js';
|
|
24
|
-
/** Indirection seam so a test/smoke script can fake the guard, the exec
|
|
25
|
-
* primitive, and the audit writer without a real DB/provider. Production
|
|
26
|
-
* never reassigns these. */
|
|
27
|
-
export const deliverInternals = {
|
|
28
|
-
authorizeTrigger,
|
|
29
|
-
wakeAndExecNodeMessage,
|
|
30
|
-
recordTriggerAudit,
|
|
31
|
-
};
|
|
32
|
-
function hashPayload(payload) {
|
|
33
|
-
return createHash('sha256').update(JSON.stringify(payload ?? null)).digest('hex');
|
|
34
|
-
}
|
|
35
|
-
/** Bound + redact an error into an audit `reason`. The P4.4 primitive's error
|
|
36
|
-
* messages do not include the payload body, but this is defense-in-depth:
|
|
37
|
-
* truncate and strip any resolved secret before it lands in a DB row. */
|
|
38
|
-
function auditReason(error) {
|
|
39
|
-
const raw = error instanceof Error ? error.message : String(error);
|
|
40
|
-
return redactSecrets(raw).slice(0, 200);
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Deliver `input.envelope` to `input.home_id` through the P4.4 wake+exec
|
|
44
|
-
* primitive, gated by the FK-integrity ACL and recorded in the trigger audit.
|
|
45
|
-
* Returns `{ outcome: 'rejected' }` (no wake/exec) for an unauthorized target,
|
|
46
|
-
* `{ outcome: 'delivered' }` after a successful in-guest `node msg`, or
|
|
47
|
-
* `{ outcome: 'failed' }` when the wake/exec threw. Every path writes exactly
|
|
48
|
-
* one audit row (rejected/delivered/failed).
|
|
49
|
-
*/
|
|
50
|
-
export async function deliverTrigger(input) {
|
|
51
|
-
const { home_id, source_ref, envelope, tier } = input;
|
|
52
|
-
const payloadSha256 = input.payload_sha256 ?? hashPayload(envelope.payload);
|
|
53
|
-
const auditBase = {
|
|
54
|
-
source: envelope.source,
|
|
55
|
-
source_ref,
|
|
56
|
-
trigger_id: envelope.trigger_id,
|
|
57
|
-
tier,
|
|
58
|
-
payload_sha256: payloadSha256,
|
|
59
|
-
metadata_json: input.metadata ?? null,
|
|
60
|
-
};
|
|
61
|
-
const authorized = deliverInternals.authorizeTrigger(envelope.source, source_ref, home_id);
|
|
62
|
-
if (!authorized.ok) {
|
|
63
|
-
deliverInternals.recordTriggerAudit({ ...auditBase, outcome: 'rejected', reason: authorized.reason ?? 'unauthorized' });
|
|
64
|
-
return { outcome: 'rejected', reason: authorized.reason };
|
|
65
|
-
}
|
|
66
|
-
try {
|
|
67
|
-
await deliverInternals.wakeAndExecNodeMessage(home_id, serializeEnvelope(envelope), tier);
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
const reason = auditReason(error);
|
|
71
|
-
deliverInternals.recordTriggerAudit({ ...auditBase, outcome: 'failed', reason });
|
|
72
|
-
return { outcome: 'failed', reason };
|
|
73
|
-
}
|
|
74
|
-
deliverInternals.recordTriggerAudit({ ...auditBase, outcome: 'delivered' });
|
|
75
|
-
return { outcome: 'delivered' };
|
|
76
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export type TriggerSource = 'cron' | 'webhook';
|
|
2
|
-
/**
|
|
3
|
-
* The delivered envelope. `payload` is deliberately `unknown` — it is
|
|
4
|
-
* untrusted external content and callers/home-side code must narrow it
|
|
5
|
-
* defensively. `trigger_id` is the home-side dedupe key: the durable
|
|
6
|
-
* `schedule_fire.fire_id` for cron, a stable route-derived id for webhook.
|
|
7
|
-
*/
|
|
8
|
-
export interface TriggerEnvelope {
|
|
9
|
-
source: TriggerSource;
|
|
10
|
-
trigger_id: string;
|
|
11
|
-
payload: unknown;
|
|
12
|
-
received_at: string;
|
|
13
|
-
}
|
|
14
|
-
export interface BuildTriggerEnvelopeInput {
|
|
15
|
-
source: TriggerSource;
|
|
16
|
-
trigger_id: string;
|
|
17
|
-
payload: unknown;
|
|
18
|
-
/** Override the received-at stamp (tests/replays); defaults to now. */
|
|
19
|
-
received_at?: string;
|
|
20
|
-
}
|
|
21
|
-
/** Build a canonical envelope. Takes NO routing-key argument by construction
|
|
22
|
-
* (R-ING-3) — `payload` is channel-shaped content only. */
|
|
23
|
-
export declare function buildTriggerEnvelope(input: BuildTriggerEnvelopeInput): TriggerEnvelope;
|
|
24
|
-
/** Serialize an envelope to the compact JSON body carried on the `node msg`
|
|
25
|
-
* stdin. Emits EXACTLY the four canonical fields in a fixed order — any
|
|
26
|
-
* extra property on the passed object (e.g. a stray routing key) is dropped
|
|
27
|
-
* here, so the home only ever sees `{source, trigger_id, payload,
|
|
28
|
-
* received_at}`. */
|
|
29
|
-
export declare function serializeEnvelope(envelope: TriggerEnvelope): string;
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// control-plane/triggers/envelope.ts — the R-TRIG-2 trigger delivery envelope
|
|
2
|
-
// (master plan P5.1). The exact JSON shape delivered in-guest to a home agent
|
|
3
|
-
// over `crtr node msg --to <target>` (stdin body), for both cron and webhook
|
|
4
|
-
// sources.
|
|
5
|
-
//
|
|
6
|
-
// Two hard invariants this module encodes:
|
|
7
|
-
// 1. `payload` is UNTRUSTED external data. For a webhook it is attacker-
|
|
8
|
-
// controlled request-body content; for cron it is the operator-authored
|
|
9
|
-
// `schedule.delivery_payload`. Home agents / scaffolding must treat it as
|
|
10
|
-
// data, never as instructions (the C7 image defaults say so too).
|
|
11
|
-
// 2. The envelope NEVER carries the ingress routing key (R-ING-3). The
|
|
12
|
-
// builder takes no routing-key argument, and `serializeEnvelope` emits
|
|
13
|
-
// ONLY the four canonical fields, so a routing key can never leak to a
|
|
14
|
-
// home even if a caller stuffed one into an ad-hoc object.
|
|
15
|
-
/** Build a canonical envelope. Takes NO routing-key argument by construction
|
|
16
|
-
* (R-ING-3) — `payload` is channel-shaped content only. */
|
|
17
|
-
export function buildTriggerEnvelope(input) {
|
|
18
|
-
return {
|
|
19
|
-
source: input.source,
|
|
20
|
-
trigger_id: input.trigger_id,
|
|
21
|
-
payload: input.payload,
|
|
22
|
-
received_at: input.received_at ?? new Date().toISOString(),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
/** Serialize an envelope to the compact JSON body carried on the `node msg`
|
|
26
|
-
* stdin. Emits EXACTLY the four canonical fields in a fixed order — any
|
|
27
|
-
* extra property on the passed object (e.g. a stray routing key) is dropped
|
|
28
|
-
* here, so the home only ever sees `{source, trigger_id, payload,
|
|
29
|
-
* received_at}`. */
|
|
30
|
-
export function serializeEnvelope(envelope) {
|
|
31
|
-
const canonical = {
|
|
32
|
-
source: envelope.source,
|
|
33
|
-
trigger_id: envelope.trigger_id,
|
|
34
|
-
payload: envelope.payload,
|
|
35
|
-
received_at: envelope.received_at,
|
|
36
|
-
};
|
|
37
|
-
return JSON.stringify(canonical);
|
|
38
|
-
}
|