@wenathlan/saddle 2.0.151 → 2.0.152
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 +3 -3
- package/biome.jsonc +12 -5
- package/dist/App.d.ts +3 -0
- package/dist/App.js +50 -0
- package/dist/Sol/Sol.d.ts +3 -0
- package/dist/Sol/Sol.js +46 -0
- package/dist/Sol/agentbrowser/agentbrowser.d.ts +9 -0
- package/dist/Sol/agentbrowser/agentbrowser.js +121 -0
- package/dist/Sol/architecture/architecture.d.ts +9 -0
- package/dist/Sol/architecture/architecture.js +93 -0
- package/dist/Sol/compute/compute.d.ts +9 -0
- package/dist/Sol/compute/compute.js +22 -0
- package/dist/Sol/console/console.d.ts +15 -0
- package/dist/Sol/console/console.js +292 -0
- package/dist/Sol/dashboard/dashboard.d.ts +10 -0
- package/dist/Sol/dashboard/dashboard.js +181 -0
- package/dist/Sol/docs/docs.d.ts +9 -0
- package/dist/Sol/docs/docs.js +28 -0
- package/dist/Sol/home/home.d.ts +35 -0
- package/dist/Sol/home/home.js +239 -0
- package/dist/Sol/integrations/integrations.d.ts +9 -0
- package/dist/Sol/integrations/integrations.js +83 -0
- package/dist/Sol/intro/intro.d.ts +7 -0
- package/dist/Sol/intro/intro.js +102 -0
- package/dist/Sol/login/login.d.ts +16 -0
- package/dist/Sol/login/login.js +49 -0
- package/dist/Sol/notfound/notfound.d.ts +9 -0
- package/dist/Sol/notfound/notfound.js +15 -0
- package/dist/Sol/onboarding/onboarding.d.ts +25 -0
- package/dist/Sol/onboarding/onboarding.js +60 -0
- package/dist/Sol/playground/playground.d.ts +9 -0
- package/dist/Sol/playground/playground.js +127 -0
- package/dist/Sol/register/register.d.ts +16 -0
- package/dist/Sol/register/register.js +193 -0
- package/dist/Sol/shell/Shell.d.ts +124 -0
- package/dist/Sol/shell/Shell.js +335 -0
- package/dist/Sol/wingets/ComputeBootladder.d.ts +6 -0
- package/dist/Sol/wingets/ComputeBootladder.js +71 -0
- package/dist/Sol/wingets/ComputeModalitycatalog.d.ts +7 -0
- package/dist/Sol/wingets/ComputeModalitycatalog.js +71 -0
- package/dist/Sol/wingets/ConsoleApibadge.d.ts +7 -0
- package/dist/Sol/wingets/ConsoleApibadge.js +5 -0
- package/dist/Sol/wingets/ConsoleEdgelaw.d.ts +6 -0
- package/dist/Sol/wingets/ConsoleEdgelaw.js +36 -0
- package/dist/Sol/wingets/ConsoleEventstimeline.d.ts +19 -0
- package/dist/Sol/wingets/ConsoleEventstimeline.js +12 -0
- package/dist/Sol/wingets/ConsoleSnapshotcontrols.d.ts +8 -0
- package/dist/Sol/wingets/ConsoleSnapshotcontrols.js +8 -0
- package/dist/Sol/wingets/ConsoleSpecpanel.d.ts +21 -0
- package/dist/Sol/wingets/ConsoleSpecpanel.js +75 -0
- package/dist/Sol/wingets/ConsoleTerminal.d.ts +22 -0
- package/dist/Sol/wingets/ConsoleTerminal.js +77 -0
- package/dist/Sol/wingets/DashboardAccountpanel.d.ts +18 -0
- package/dist/Sol/wingets/DashboardAccountpanel.js +65 -0
- package/dist/Sol/wingets/DashboardAdminpanel.d.ts +6 -0
- package/dist/Sol/wingets/DashboardAdminpanel.js +90 -0
- package/dist/Sol/wingets/DashboardAuditlog.d.ts +8 -0
- package/dist/Sol/wingets/DashboardAuditlog.js +50 -0
- package/dist/Sol/wingets/DashboardCreatesandboxform.d.ts +10 -0
- package/dist/Sol/wingets/DashboardCreatesandboxform.js +71 -0
- package/dist/Sol/wingets/DashboardEventstimeline.d.ts +8 -0
- package/dist/Sol/wingets/DashboardEventstimeline.js +83 -0
- package/dist/Sol/wingets/DashboardGlobalsandboxestable.d.ts +8 -0
- package/dist/Sol/wingets/DashboardGlobalsandboxestable.js +57 -0
- package/dist/Sol/wingets/DashboardMeshnodestable.d.ts +8 -0
- package/dist/Sol/wingets/DashboardMeshnodestable.js +103 -0
- package/dist/Sol/wingets/DashboardSandboxlist.d.ts +10 -0
- package/dist/Sol/wingets/DashboardSandboxlist.js +163 -0
- package/dist/Sol/wingets/DashboardUserstable.d.ts +8 -0
- package/dist/Sol/wingets/DashboardUserstable.js +78 -0
- package/dist/Sol/wingets/HomeEntrygate.d.ts +23 -0
- package/dist/Sol/wingets/HomeEntrygate.js +73 -0
- package/dist/Sol/wingets/HomeMetricstrip.d.ts +9 -0
- package/dist/Sol/wingets/HomeMetricstrip.js +4 -0
- package/dist/Sol/wingets/HomeRuntimediagram.d.ts +1 -0
- package/dist/Sol/wingets/HomeRuntimediagram.js +22 -0
- package/dist/Sol/wingets/LoginForm.d.ts +19 -0
- package/dist/Sol/wingets/LoginForm.js +141 -0
- package/dist/Sol/wingets/RegisterPasswordconfirm.d.ts +13 -0
- package/dist/Sol/wingets/RegisterPasswordconfirm.js +16 -0
- package/dist/Sol/wingets/RegisterPasswordmeter.d.ts +19 -0
- package/dist/Sol/wingets/RegisterPasswordmeter.js +65 -0
- package/dist/Sol/wingets/RegisterUsernamerules.d.ts +11 -0
- package/dist/Sol/wingets/RegisterUsernamerules.js +16 -0
- package/dist/Sol/wingets/index.d.ts +39 -0
- package/dist/Sol/wingets/index.js +39 -0
- package/dist/alternatives.d.ts +508 -0
- package/dist/alternatives.js +950 -0
- package/dist/api.d.ts +154 -0
- package/dist/api.js +174 -0
- package/dist/auth.d.ts +268 -0
- package/dist/auth.js +476 -0
- package/dist/automation.d.ts +425 -0
- package/dist/automation.js +566 -0
- package/dist/browser.d.ts +561 -0
- package/dist/browser.js +1058 -0
- package/dist/capacitor.config.d.ts +45 -0
- package/dist/capacitor.config.js +48 -0
- package/dist/catalog.d.ts +43 -0
- package/dist/catalog.js +44 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +168 -0
- package/dist/communication.d.ts +181 -0
- package/dist/communication.js +376 -0
- package/dist/compute.d.ts +1160 -0
- package/dist/compute.js +2272 -0
- package/dist/dashboardlib.d.ts +58 -0
- package/dist/dashboardlib.js +161 -0
- package/dist/db.d.ts +569 -0
- package/dist/db.js +1080 -0
- package/dist/distribution.d.ts +528 -0
- package/dist/distribution.js +810 -0
- package/dist/drizzle.config.d.ts +20 -0
- package/dist/drizzle.config.js +18 -0
- package/dist/edgelandes.d.ts +70 -0
- package/dist/edgelandes.host.d.ts +26 -0
- package/dist/edgelandes.host.js +1089 -0
- package/dist/edgelandes.inproc.d.ts +27 -0
- package/dist/edgelandes.inproc.js +952 -0
- package/dist/edgelandes.js +1133 -0
- package/dist/edgelandes.ninety.d.ts +31 -0
- package/dist/edgelandes.ninety.js +174 -0
- package/dist/edgelandes.virtual.d.ts +25 -0
- package/dist/edgelandes.virtual.js +629 -0
- package/dist/edgeplane.d.ts +114 -0
- package/dist/edgeplane.js +305 -0
- package/dist/examplelocaljob.d.ts +1 -0
- package/{docs/example-localjob.ts → dist/examplelocaljob.js} +0 -1
- package/dist/execution.d.ts +529 -0
- package/dist/execution.js +760 -0
- package/dist/family.d.ts +16 -0
- package/dist/family.js +42 -0
- package/dist/familyurl.d.ts +21 -0
- package/dist/familyurl.js +24 -0
- package/dist/format.d.ts +18 -0
- package/dist/format.js +59 -0
- package/dist/foundation.d.ts +172 -0
- package/dist/foundation.js +312 -0
- package/dist/index.d.ts +735 -0
- package/dist/index.js +1020 -0
- package/dist/integration.d.ts +235 -0
- package/dist/integration.js +228 -0
- package/dist/intelligence.d.ts +93 -0
- package/dist/intelligence.js +144 -0
- package/dist/isolation.d.ts +128 -0
- package/dist/isolation.js +95 -0
- package/dist/labels.d.ts +665 -0
- package/dist/labels.js +751 -0
- package/dist/localauth.d.ts +127 -0
- package/dist/localauth.js +413 -0
- package/dist/media.d.ts +343 -0
- package/dist/media.js +985 -0
- package/dist/mesh.d.ts +177 -0
- package/dist/mesh.js +468 -0
- package/dist/modalityregistry.d.ts +109 -0
- package/dist/modalityregistry.js +1386 -0
- package/dist/modes.d.ts +358 -0
- package/dist/modes.js +133 -0
- package/dist/onboardingstate.d.ts +79 -0
- package/dist/onboardingstate.js +282 -0
- package/dist/orchestrator.d.ts +1338 -0
- package/dist/orchestrator.js +4310 -0
- package/dist/packager/targetcli.js +4 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +14 -0
- package/dist/performance.d.ts +415 -0
- package/dist/performance.js +826 -0
- package/dist/quantum.d.ts +809 -0
- package/dist/quantum.js +2072 -0
- package/dist/render.d.ts +380 -0
- package/dist/render.js +1072 -0
- package/dist/runall.d.ts +2 -0
- package/dist/runall.js +39 -0
- package/dist/runtimeprobe.d.ts +4 -0
- package/dist/runtimeprobe.js +8 -0
- package/dist/sandbox.d.ts +355 -0
- package/dist/sandbox.js +1721 -0
- package/dist/sandboxenvprofile.d.ts +29 -0
- package/dist/sandboxenvprofile.js +89 -0
- package/dist/sandboxprofile.d.ts +137 -0
- package/dist/sandboxprofile.js +241 -0
- package/dist/sandboxqueue.d.ts +108 -0
- package/dist/sandboxqueue.js +201 -0
- package/dist/sandboxquota.d.ts +121 -0
- package/dist/sandboxquota.js +236 -0
- package/dist/scheduler.d.ts +595 -0
- package/dist/scheduler.js +1258 -0
- package/dist/scriptlanes.d.ts +182 -0
- package/dist/scriptlanes.js +660 -0
- package/dist/scripts.buildengine.d.ts +1 -0
- package/dist/scripts.buildengine.js +39 -0
- package/dist/scripts.securitygate.d.ts +1 -0
- package/dist/scripts.securitygate.js +68 -0
- package/dist/scripts.validateflatnative.d.ts +1 -0
- package/dist/scripts.validateflatnative.js +72 -0
- package/dist/security.d.ts +500 -0
- package/dist/security.js +1169 -0
- package/dist/seedcatalog.d.ts +13 -0
- package/dist/seedcatalog.js +46 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +53 -0
- package/dist/speccatalog.d.ts +31 -0
- package/dist/speccatalog.js +163 -0
- package/dist/tiers.d.ts +1001 -0
- package/dist/tiers.js +2169 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +5 -0
- package/dist/vaulthandoff.d.ts +141 -0
- package/dist/vaulthandoff.js +444 -0
- package/dist/virtual.d.ts +640 -0
- package/dist/virtual.js +1284 -0
- package/dist/virtualcpu.d.ts +315 -0
- package/dist/virtualcpu.js +1180 -0
- package/dist/virtualgpu.d.ts +344 -0
- package/dist/virtualgpu.js +1000 -0
- package/dist/virtualization.d.ts +606 -0
- package/dist/virtualization.js +1186 -0
- package/dist/virtualmemory.d.ts +817 -0
- package/dist/virtualmemory.js +1795 -0
- package/dist/vite.config.d.ts +3 -0
- package/dist/vite.config.js +43 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +25 -0
- package/dist/webserver.d.ts +166 -0
- package/dist/webserver.js +2226 -0
- package/dist/workloadlaw.d.ts +133 -0
- package/dist/workloadlaw.js +193 -0
- package/docs/artifactavailability.md +2 -2
- package/docs/{containerplatforms-2.1.0.md → containerplatforms.2.1.0.md} +1 -1
- package/docs/{documentmap-1.8.12.md → documentmap.1.8.12.md} +1 -1
- package/docs/ninety.modalities.md +108 -0
- package/docs/planning.ninety.md +391 -0
- package/docs/{releasenotes-1.8.13.md → releasenotes.1.8.13.md} +2 -2
- package/docs/{releasenotes-1.8.14.md → releasenotes.1.8.14.md} +1 -1
- package/docs/{repo-synthesis-1.8.16.md → repo.synthesis.1.8.16.md} +1 -1
- package/docs/{todo-1.8.15.md → todo.1.8.15.md} +1 -1
- package/docs/{todo-1.8.16.md → todo.1.8.16.md} +5 -5
- package/docs/todo.md +2 -2
- package/docs/workloadlaw.md +104 -0
- package/index.ts +1 -1
- package/manifest.json +1 -1
- package/modalityregistry.ts +1371 -34
- package/orchestrator.ts +1 -1
- package/package.json +73 -33
- package/docs/CONVERSA.txt +0 -1523
- package/docs/architecture-1.8.19.png +0 -0
- package/docs/assets/architecture.svg +0 -45
- package/docs/assets/saddle-browser-trace.webp +0 -0
- package/docs/assets/saddle-hero-bridge.webp +0 -0
- package/docs/assets/saddle-mark.webp +0 -0
- package/docs/assets/saddle-runtime-map.webp +0 -0
- package/docs/assets/saddlemark.svg +0 -13
- package/docs/saddle.archive.1.8.17.tar.gz.gpg +0 -0
- /package/docs/{architecture-1.8.19.md → architecture.1.8.19.md} +0 -0
- /package/docs/{architecture-1.8.19.mmd → architecture.1.8.19.mmd} +0 -0
- /package/docs/{artifactresearch-1.8.9.md → artifactresearch.1.8.9.md} +0 -0
- /package/docs/{batches-1816-1820.md → batches.1816.to.1820.md} +0 -0
- /package/docs/{brancharchive-2026-08-18.md → brancharchive.2026.08.18.md} +0 -0
- /package/docs/{containerplatforms-1.8.17.md → containerplatforms.1.8.17.md} +0 -0
- /package/docs/{e2ugh-engine.md → e2ugh.engine.md} +0 -0
- /package/docs/{flatbuildresearch-1.8.11.md → flatbuildresearch.1.8.11.md} +0 -0
- /package/docs/{licenseaudit-1.8.12.md → licenseaudit.1.8.12.md} +0 -0
- /package/docs/{mobileconversionresearch-1.8.10.md → mobileconversionresearch.1.8.10.md} +0 -0
- /package/docs/{native-wrappers.md → native.wrappers.md} +0 -0
- /package/docs/{nativeidentityresearch-1.8.11.md → nativeidentityresearch.1.8.11.md} +0 -0
- /package/docs/{releasenotes-1.8.12.md → releasenotes.1.8.12.md} +0 -0
- /package/docs/{releasenotes-1.8.15.md → releasenotes.1.8.15.md} +0 -0
- /package/docs/{releasenotes-1.8.16.md → releasenotes.1.8.16.md} +0 -0
- /package/docs/{releasenotes-1.8.17.md → releasenotes.1.8.17.md} +0 -0
- /package/docs/{releasenotes-1.8.18.md → releasenotes.1.8.18.md} +0 -0
- /package/docs/{releasenotes-1.8.19.md → releasenotes.1.8.19.md} +0 -0
- /package/docs/{reorganization-1.8.8.md → reorganization.1.8.8.md} +0 -0
- /package/docs/{reorganization-1.8.9.md → reorganization.1.8.9.md} +0 -0
- /package/docs/{repo-analysis-1.8.21.json → repo.analysis.1.8.21.json} +0 -0
- /package/docs/{repo-candidates-1.8.21.jsonl → repo.candidates.1.8.21.jsonl} +0 -0
- /package/docs/{repo-research-1.8.16.md → repo.research.1.8.16.md} +0 -0
- /package/docs/{repo-research-1.8.21.md → repo.research.1.8.21.md} +0 -0
- /package/docs/{repo-selected-1.8.21.json → repo.selected.1.8.21.json} +0 -0
- /package/docs/{repo-synthesis-1.8.21.md → repo.synthesis.1.8.21.md} +0 -0
- /package/docs/{research-1.8.18-isolation.md → research.1.8.18.isolation.md} +0 -0
- /package/docs/{research-1.8.19-virtual-browser.md → research.1.8.19.virtual.browser.md} +0 -0
- /package/docs/{scanningresearch-1.8.11.md → scanningresearch.1.8.11.md} +0 -0
- /package/docs/{securityaudit-1.8.7.md → securityaudit.1.8.7.md} +0 -0
- /package/docs/{securityreferences-1.8.11.md → securityreferences.1.8.11.md} +0 -0
- /package/docs/{securityresearch-1.8.11.md → securityresearch.1.8.11.md} +0 -0
- /package/docs/{signingresearch-1.8.12.md → signingresearch.1.8.12.md} +0 -0
- /package/docs/{tauri-glib-risk-1.8.11.md → tauri.glib.risk.1.8.11.md} +0 -0
- /package/docs/{todo-1.8.18.md → todo.1.8.18.md} +0 -0
- /package/docs/{todo-1.8.19.md → todo.1.8.19.md} +0 -0
- /package/docs/{toolchainresearch-1.8.9.md → toolchainresearch.1.8.9.md} +0 -0
- /package/docs/{web-duplicatecleanup.md → web.duplicatecleanup.md} +0 -0
- /package/docs/{web-ideas.md → web.ideas.md} +0 -0
- /package/docs/{workflowimprovements-2026-08-19.md → workflowimprovements.2026.08.19.md} +0 -0
package/dist/mesh.d.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mesh.ts — signed node-to-node communication for the saddle web mesh
|
|
3
|
+
* (v7-BACK).
|
|
4
|
+
*
|
|
5
|
+
* the mesh connects clone nodes to the main authority: requests are
|
|
6
|
+
* signed with HMAC-SHA256 over `${timestamp}.${method}.${path}.${bodyhash}`,
|
|
7
|
+
* protected by a 60-second anti-replay window with a 1024-entry nonce
|
|
8
|
+
* cache, and may optionally encrypt payloads with AES-256-GCM using the
|
|
9
|
+
* SADDLE_MESH_KEY hex secret. the module also carries the node role
|
|
10
|
+
* (SADDLE_ROLE=main|clone|standalone, default standalone), the signed
|
|
11
|
+
* fetch client towards SADDLE_MAIN_URL and the 60-second heartbeat
|
|
12
|
+
* loop clones run against the main registry.
|
|
13
|
+
*
|
|
14
|
+
* contexts (9): types, noderole, requestsigning, antireplay,
|
|
15
|
+
* payloadcrypto, meshmessages, mainclient, heartbeat, sessionforwarding.
|
|
16
|
+
*
|
|
17
|
+
* rules: lowercase identifiers, english jsdoc in third person, no emoji,
|
|
18
|
+
* try/catch on every fallible path, node:* modules plus the global
|
|
19
|
+
* fetch only, zero dependencies, no hardcoded localhost (the main url
|
|
20
|
+
* always comes from the environment).
|
|
21
|
+
*/
|
|
22
|
+
import type { IncomingMessage } from 'node:http';
|
|
23
|
+
/** the node role resolved from saddle_role. */
|
|
24
|
+
export type NodeRole = 'main' | 'clone' | 'standalone';
|
|
25
|
+
/** the mesh verification outcome: the ok arm or the rejection payload
|
|
26
|
+
* consumed by writeerror. */
|
|
27
|
+
export type MeshVerdict = {
|
|
28
|
+
ok: true;
|
|
29
|
+
} | {
|
|
30
|
+
ok: false;
|
|
31
|
+
status: number;
|
|
32
|
+
code: string;
|
|
33
|
+
message: string;
|
|
34
|
+
};
|
|
35
|
+
/** the mesh wire envelope {type, from, data, ts}; when the mesh key is
|
|
36
|
+
* configured the data field is replaced by {enc: '<hex>'}. */
|
|
37
|
+
export type MeshEnvelope = {
|
|
38
|
+
type: string;
|
|
39
|
+
from: string;
|
|
40
|
+
data: unknown;
|
|
41
|
+
ts: string;
|
|
42
|
+
};
|
|
43
|
+
/** the opened mesh message produced by openmeshmessage. */
|
|
44
|
+
export type MeshMessage = {
|
|
45
|
+
type: string;
|
|
46
|
+
from: string;
|
|
47
|
+
data: unknown;
|
|
48
|
+
ts: string;
|
|
49
|
+
};
|
|
50
|
+
/** the main node response relayed by the signed client: status, the
|
|
51
|
+
* parsed json body (null when not json) and the set-cookie relay. */
|
|
52
|
+
export type MainResponse = {
|
|
53
|
+
status: number;
|
|
54
|
+
body: unknown;
|
|
55
|
+
setcookie: string[];
|
|
56
|
+
};
|
|
57
|
+
/** the announcing node description of the clone heartbeat loop. */
|
|
58
|
+
export type HeartbeatSelf = {
|
|
59
|
+
url: string;
|
|
60
|
+
region?: string;
|
|
61
|
+
rolename?: string;
|
|
62
|
+
};
|
|
63
|
+
/** the node role: main, clone or standalone. */
|
|
64
|
+
export declare const role: NodeRole;
|
|
65
|
+
/**
|
|
66
|
+
* reads the shared mesh secret from the environment.
|
|
67
|
+
*
|
|
68
|
+
* @returns the secret or an empty string when unconfigured.
|
|
69
|
+
*/
|
|
70
|
+
export declare function meshsecret(): string;
|
|
71
|
+
/**
|
|
72
|
+
* reads the main node base url (no trailing slash) from the
|
|
73
|
+
* environment; never a hardcoded address.
|
|
74
|
+
*
|
|
75
|
+
* @returns the trimmed base url or an empty string.
|
|
76
|
+
*/
|
|
77
|
+
export declare function mainurl(): string;
|
|
78
|
+
/**
|
|
79
|
+
* computes the sha256 hex digest of one request body.
|
|
80
|
+
*
|
|
81
|
+
* @param body the exact raw body string ('' for GET).
|
|
82
|
+
* @returns the hex body hash.
|
|
83
|
+
*/
|
|
84
|
+
export declare function bodyhash(body: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* signs one mesh request: HMAC-SHA256 hex over the string
|
|
87
|
+
* `${timestamp}.${method}.${path}.${bodyhash}`.
|
|
88
|
+
*
|
|
89
|
+
* @param method the upper-case http method.
|
|
90
|
+
* @param path the full request path (e.g. /api/v1/mesh/register).
|
|
91
|
+
* @param body the exact raw body string ('' for GET).
|
|
92
|
+
* @param secret the shared mesh secret.
|
|
93
|
+
* @param timestamp the millisecond timestamp string
|
|
94
|
+
* (defaults to now).
|
|
95
|
+
* @returns the hex signature.
|
|
96
|
+
*/
|
|
97
|
+
export declare function sign(method: string, path: string, body: string, secret: string, timestamp?: string): string;
|
|
98
|
+
/**
|
|
99
|
+
* verifies one incoming mesh request: the x-saddle-timestamp header must
|
|
100
|
+
* sit inside the 60-second window, the x-saddle-signature header must
|
|
101
|
+
* match the recomputed HMAC over the raw body, and the signature must
|
|
102
|
+
* not have been seen before (replay protection).
|
|
103
|
+
*
|
|
104
|
+
* @param req the incoming request.
|
|
105
|
+
* @param rawbody the exact raw request body ('' for GET).
|
|
106
|
+
* @param secret the shared mesh secret.
|
|
107
|
+
* @returns the verification outcome.
|
|
108
|
+
*/
|
|
109
|
+
export declare function verifymesh(req: IncomingMessage, rawbody: string, secret: string): MeshVerdict;
|
|
110
|
+
/**
|
|
111
|
+
* encrypts one utf-8 payload with AES-256-GCM; the output is hex
|
|
112
|
+
* `iv(12) || ciphertext || tag(16)`.
|
|
113
|
+
*
|
|
114
|
+
* @param plaintext the payload to encrypt.
|
|
115
|
+
* @returns the hex envelope or null when the key is
|
|
116
|
+
* not configured.
|
|
117
|
+
*/
|
|
118
|
+
export declare function encrypt(plaintext: string): string | null;
|
|
119
|
+
/**
|
|
120
|
+
* decrypts one AES-256-GCM hex envelope produced by encrypt.
|
|
121
|
+
*
|
|
122
|
+
* @param envelope the hex iv || ciphertext || tag payload.
|
|
123
|
+
* @returns the utf-8 plaintext or null on any failure.
|
|
124
|
+
*/
|
|
125
|
+
export declare function decrypt(envelope: string): string | null;
|
|
126
|
+
/**
|
|
127
|
+
* builds one mesh message envelope {type, from, data, ts}; when the
|
|
128
|
+
* mesh key is configured the data field is replaced by the GCM
|
|
129
|
+
* envelope {enc: '<hex>'}.
|
|
130
|
+
*
|
|
131
|
+
* @param type the message type (e.g. 'heartbeat').
|
|
132
|
+
* @param from the sender node id or url.
|
|
133
|
+
* @param data the message payload.
|
|
134
|
+
* @returns the wire envelope.
|
|
135
|
+
*/
|
|
136
|
+
export declare function meshmessage(type: string, from: string, data: unknown): MeshEnvelope;
|
|
137
|
+
/**
|
|
138
|
+
* opens one mesh message envelope produced by meshmessage, decrypting
|
|
139
|
+
* the data field when needed.
|
|
140
|
+
*
|
|
141
|
+
* @param envelope the wire envelope.
|
|
142
|
+
* @returns the opened message or null on tampering.
|
|
143
|
+
*/
|
|
144
|
+
export declare function openmeshmessage(envelope: unknown): MeshMessage | null;
|
|
145
|
+
/**
|
|
146
|
+
* performs one signed POST towards the main node described by
|
|
147
|
+
* SADDLE_MAIN_URL using the global fetch; the signature headers are
|
|
148
|
+
* computed with the shared SADDLE_MESH_SECRET.
|
|
149
|
+
*
|
|
150
|
+
* @param path the full api path (e.g. /api/v1/auth/login).
|
|
151
|
+
* @param body the json-serializable payload.
|
|
152
|
+
* @returns the response status, the parsed json body and the
|
|
153
|
+
* set-cookie relay.
|
|
154
|
+
* @throws when the mesh is unconfigured, the
|
|
155
|
+
* request fails or the body is not json.
|
|
156
|
+
*/
|
|
157
|
+
export declare function postmain(path: string, body: unknown): Promise<MainResponse>;
|
|
158
|
+
/**
|
|
159
|
+
* runs the clone registry loop: one immediate registration followed by
|
|
160
|
+
* heartbeats every 60 seconds, all signed towards the main node. the
|
|
161
|
+
* interval is unref'd so the process can still exit cleanly.
|
|
162
|
+
*
|
|
163
|
+
* @param self the announcing node description.
|
|
164
|
+
* @returns the heartbeat timer or null when the
|
|
165
|
+
* node is not a clone or the mesh is unconfigured.
|
|
166
|
+
*/
|
|
167
|
+
export declare function startheartbeat(self: HeartbeatSelf): NodeJS.Timeout | null;
|
|
168
|
+
/**
|
|
169
|
+
* forwards one auth request (register or login) from a clone to the
|
|
170
|
+
* main authority; the caller caches the returned session locally when
|
|
171
|
+
* the main node accepts it.
|
|
172
|
+
*
|
|
173
|
+
* @param action the auth action.
|
|
174
|
+
* @param body the credentials payload.
|
|
175
|
+
* @returns the main response verbatim.
|
|
176
|
+
*/
|
|
177
|
+
export declare function forwardauth(action: 'register' | 'login', body: unknown): Promise<MainResponse>;
|
package/dist/mesh.js
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mesh.ts — signed node-to-node communication for the saddle web mesh
|
|
3
|
+
* (v7-BACK).
|
|
4
|
+
*
|
|
5
|
+
* the mesh connects clone nodes to the main authority: requests are
|
|
6
|
+
* signed with HMAC-SHA256 over `${timestamp}.${method}.${path}.${bodyhash}`,
|
|
7
|
+
* protected by a 60-second anti-replay window with a 1024-entry nonce
|
|
8
|
+
* cache, and may optionally encrypt payloads with AES-256-GCM using the
|
|
9
|
+
* SADDLE_MESH_KEY hex secret. the module also carries the node role
|
|
10
|
+
* (SADDLE_ROLE=main|clone|standalone, default standalone), the signed
|
|
11
|
+
* fetch client towards SADDLE_MAIN_URL and the 60-second heartbeat
|
|
12
|
+
* loop clones run against the main registry.
|
|
13
|
+
*
|
|
14
|
+
* contexts (9): types, noderole, requestsigning, antireplay,
|
|
15
|
+
* payloadcrypto, meshmessages, mainclient, heartbeat, sessionforwarding.
|
|
16
|
+
*
|
|
17
|
+
* rules: lowercase identifiers, english jsdoc in third person, no emoji,
|
|
18
|
+
* try/catch on every fallible path, node:* modules plus the global
|
|
19
|
+
* fetch only, zero dependencies, no hardcoded localhost (the main url
|
|
20
|
+
* always comes from the environment).
|
|
21
|
+
*/
|
|
22
|
+
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, timingSafeEqual, } from 'node:crypto';
|
|
23
|
+
import process from 'node:process';
|
|
24
|
+
/* ------------------------------------------------------------------ */
|
|
25
|
+
/* context: noderole */
|
|
26
|
+
/* ------------------------------------------------------------------ */
|
|
27
|
+
/**
|
|
28
|
+
* resolves the node role once at import time: saddle_role accepts the
|
|
29
|
+
* values main, clone and standalone; anything else (including unset)
|
|
30
|
+
* falls back to standalone so a bare `node web/server.ts` boot behaves
|
|
31
|
+
* as a self-contained authority.
|
|
32
|
+
*
|
|
33
|
+
* @returns the resolved role.
|
|
34
|
+
*/
|
|
35
|
+
function resolverole() {
|
|
36
|
+
const value = String(process.env.SADDLE_ROLE ?? 'standalone').toLowerCase();
|
|
37
|
+
if (value === 'main' || value === 'clone' || value === 'standalone') {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
return 'standalone';
|
|
41
|
+
}
|
|
42
|
+
/** the node role: main, clone or standalone. */
|
|
43
|
+
export const role = resolverole();
|
|
44
|
+
/**
|
|
45
|
+
* reads the shared mesh secret from the environment.
|
|
46
|
+
*
|
|
47
|
+
* @returns the secret or an empty string when unconfigured.
|
|
48
|
+
*/
|
|
49
|
+
export function meshsecret() {
|
|
50
|
+
return String(process.env.SADDLE_MESH_SECRET ?? '');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* reads the main node base url (no trailing slash) from the
|
|
54
|
+
* environment; never a hardcoded address.
|
|
55
|
+
*
|
|
56
|
+
* @returns the trimmed base url or an empty string.
|
|
57
|
+
*/
|
|
58
|
+
export function mainurl() {
|
|
59
|
+
return String(process.env.SADDLE_MAIN_URL ?? '').replace(/\/+$/, '');
|
|
60
|
+
}
|
|
61
|
+
/* ------------------------------------------------------------------ */
|
|
62
|
+
/* context: requestsigning */
|
|
63
|
+
/* ------------------------------------------------------------------ */
|
|
64
|
+
/** the anti-replay window in milliseconds. */
|
|
65
|
+
const replaywindowms = 60 * 1000;
|
|
66
|
+
/** the nonce cache ceiling (simple LRU eviction past this size). */
|
|
67
|
+
const noncelimit = 1024;
|
|
68
|
+
/**
|
|
69
|
+
* computes the sha256 hex digest of one request body.
|
|
70
|
+
*
|
|
71
|
+
* @param body the exact raw body string ('' for GET).
|
|
72
|
+
* @returns the hex body hash.
|
|
73
|
+
*/
|
|
74
|
+
export function bodyhash(body) {
|
|
75
|
+
return createHash('sha256').update(String(body)).digest('hex');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* signs one mesh request: HMAC-SHA256 hex over the string
|
|
79
|
+
* `${timestamp}.${method}.${path}.${bodyhash}`.
|
|
80
|
+
*
|
|
81
|
+
* @param method the upper-case http method.
|
|
82
|
+
* @param path the full request path (e.g. /api/v1/mesh/register).
|
|
83
|
+
* @param body the exact raw body string ('' for GET).
|
|
84
|
+
* @param secret the shared mesh secret.
|
|
85
|
+
* @param timestamp the millisecond timestamp string
|
|
86
|
+
* (defaults to now).
|
|
87
|
+
* @returns the hex signature.
|
|
88
|
+
*/
|
|
89
|
+
export function sign(method, path, body, secret, timestamp = String(Date.now())) {
|
|
90
|
+
return createHmac('sha256', String(secret))
|
|
91
|
+
.update(`${timestamp}.${String(method).toUpperCase()}.${path}.${bodyhash(body)}`)
|
|
92
|
+
.digest('hex');
|
|
93
|
+
}
|
|
94
|
+
/* ------------------------------------------------------------------ */
|
|
95
|
+
/* context: antireplay */
|
|
96
|
+
/* ------------------------------------------------------------------ */
|
|
97
|
+
/** seen signature nonces with insertion order for LRU eviction. */
|
|
98
|
+
const nonces = new Map();
|
|
99
|
+
/**
|
|
100
|
+
* records one signature in the nonce cache, evicting the oldest entry
|
|
101
|
+
* past the 1024-entry ceiling.
|
|
102
|
+
*
|
|
103
|
+
* @param signature the verified signature hex.
|
|
104
|
+
* @returns void.
|
|
105
|
+
*/
|
|
106
|
+
function recordnonce(signature) {
|
|
107
|
+
if (nonces.has(signature)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
nonces.set(signature, Date.now());
|
|
111
|
+
while (nonces.size > noncelimit) {
|
|
112
|
+
const oldest = nonces.keys().next().value;
|
|
113
|
+
if (oldest === undefined) {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
nonces.delete(oldest);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* verifies one incoming mesh request: the x-saddle-timestamp header must
|
|
121
|
+
* sit inside the 60-second window, the x-saddle-signature header must
|
|
122
|
+
* match the recomputed HMAC over the raw body, and the signature must
|
|
123
|
+
* not have been seen before (replay protection).
|
|
124
|
+
*
|
|
125
|
+
* @param req the incoming request.
|
|
126
|
+
* @param rawbody the exact raw request body ('' for GET).
|
|
127
|
+
* @param secret the shared mesh secret.
|
|
128
|
+
* @returns the verification outcome.
|
|
129
|
+
*/
|
|
130
|
+
export function verifymesh(req, rawbody, secret) {
|
|
131
|
+
try {
|
|
132
|
+
const timestamp = req.headers?.['x-saddle-timestamp'];
|
|
133
|
+
const signature = req.headers?.['x-saddle-signature'];
|
|
134
|
+
if (typeof timestamp !== 'string' || typeof signature !== 'string') {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
status: 401,
|
|
138
|
+
code: 'mesh-unauthenticated',
|
|
139
|
+
message: 'mesh requests require x-saddle-timestamp and x-saddle-signature headers',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const skew = Math.abs(Date.now() - Number.parseInt(timestamp, 10));
|
|
143
|
+
if (!Number.isFinite(skew) || skew > replaywindowms) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
status: 401,
|
|
147
|
+
code: 'mesh-stale-timestamp',
|
|
148
|
+
message: 'mesh timestamp outside the 60 second window',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const path = String(req.url ?? '/').split('?')[0];
|
|
152
|
+
const expected = sign(req.method ?? 'GET', path, String(rawbody), secret, timestamp);
|
|
153
|
+
const left = Buffer.from(expected, 'hex');
|
|
154
|
+
const right = Buffer.from(signature, 'hex');
|
|
155
|
+
if (left.length !== right.length || !timingSafeEqual(left, right)) {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
status: 401,
|
|
159
|
+
code: 'mesh-bad-signature',
|
|
160
|
+
message: 'mesh signature mismatch',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (nonces.has(signature)) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
status: 401,
|
|
167
|
+
code: 'mesh-replay',
|
|
168
|
+
message: 'mesh signature was already used',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
recordnonce(signature);
|
|
172
|
+
return { ok: true };
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
status: 401,
|
|
178
|
+
code: 'mesh-verify-failed',
|
|
179
|
+
message: error instanceof Error ? error.message : String(error),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/* ------------------------------------------------------------------ */
|
|
184
|
+
/* context: payloadcrypto (optional AES-256-GCM) */
|
|
185
|
+
/* ------------------------------------------------------------------ */
|
|
186
|
+
/** the authenticated payload key length in bytes. */
|
|
187
|
+
const meshkeylen = 32;
|
|
188
|
+
/** the GCM initialization vector length in bytes. */
|
|
189
|
+
const ivlen = 12;
|
|
190
|
+
/** the GCM authentication tag length in bytes. */
|
|
191
|
+
const taglen = 16;
|
|
192
|
+
/**
|
|
193
|
+
* resolves the optional SADDLE_MESH_KEY (32 bytes hex) used for payload
|
|
194
|
+
* encryption between nodes.
|
|
195
|
+
*
|
|
196
|
+
* @returns the key or null when unset/invalid.
|
|
197
|
+
*/
|
|
198
|
+
function meshkey() {
|
|
199
|
+
try {
|
|
200
|
+
const raw = String(process.env.SADDLE_MESH_KEY ?? '');
|
|
201
|
+
if (raw.length === 0) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const key = Buffer.from(raw, 'hex');
|
|
205
|
+
return key.length === meshkeylen ? key : null;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* encrypts one utf-8 payload with AES-256-GCM; the output is hex
|
|
213
|
+
* `iv(12) || ciphertext || tag(16)`.
|
|
214
|
+
*
|
|
215
|
+
* @param plaintext the payload to encrypt.
|
|
216
|
+
* @returns the hex envelope or null when the key is
|
|
217
|
+
* not configured.
|
|
218
|
+
*/
|
|
219
|
+
export function encrypt(plaintext) {
|
|
220
|
+
try {
|
|
221
|
+
const key = meshkey();
|
|
222
|
+
if (key === null) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const iv = randomBytes(ivlen);
|
|
226
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
227
|
+
const ciphertext = Buffer.concat([
|
|
228
|
+
cipher.update(String(plaintext), 'utf8'),
|
|
229
|
+
cipher.final(),
|
|
230
|
+
]);
|
|
231
|
+
const tag = cipher.getAuthTag();
|
|
232
|
+
return Buffer.concat([iv, ciphertext, tag]).toString('hex');
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* decrypts one AES-256-GCM hex envelope produced by encrypt.
|
|
240
|
+
*
|
|
241
|
+
* @param envelope the hex iv || ciphertext || tag payload.
|
|
242
|
+
* @returns the utf-8 plaintext or null on any failure.
|
|
243
|
+
*/
|
|
244
|
+
export function decrypt(envelope) {
|
|
245
|
+
try {
|
|
246
|
+
const key = meshkey();
|
|
247
|
+
if (key === null) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
const raw = Buffer.from(String(envelope), 'hex');
|
|
251
|
+
if (raw.length <= ivlen + taglen) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const iv = raw.subarray(0, ivlen);
|
|
255
|
+
const tag = raw.subarray(raw.length - taglen);
|
|
256
|
+
const ciphertext = raw.subarray(ivlen, raw.length - taglen);
|
|
257
|
+
const decipher = createDecipheriv('aes-256-gcm', key, iv);
|
|
258
|
+
decipher.setAuthTag(tag);
|
|
259
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/* ------------------------------------------------------------------ */
|
|
266
|
+
/* context: meshmessages */
|
|
267
|
+
/* ------------------------------------------------------------------ */
|
|
268
|
+
/**
|
|
269
|
+
* builds one mesh message envelope {type, from, data, ts}; when the
|
|
270
|
+
* mesh key is configured the data field is replaced by the GCM
|
|
271
|
+
* envelope {enc: '<hex>'}.
|
|
272
|
+
*
|
|
273
|
+
* @param type the message type (e.g. 'heartbeat').
|
|
274
|
+
* @param from the sender node id or url.
|
|
275
|
+
* @param data the message payload.
|
|
276
|
+
* @returns the wire envelope.
|
|
277
|
+
*/
|
|
278
|
+
export function meshmessage(type, from, data) {
|
|
279
|
+
const envelope = { type, from, data, ts: new Date().toISOString() };
|
|
280
|
+
const key = meshkey();
|
|
281
|
+
if (key !== null) {
|
|
282
|
+
const sealed = encrypt(JSON.stringify(envelope.data));
|
|
283
|
+
if (sealed !== null) {
|
|
284
|
+
return { ...envelope, data: { enc: sealed } };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return envelope;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* opens one mesh message envelope produced by meshmessage, decrypting
|
|
291
|
+
* the data field when needed.
|
|
292
|
+
*
|
|
293
|
+
* @param envelope the wire envelope.
|
|
294
|
+
* @returns the opened message or null on tampering.
|
|
295
|
+
*/
|
|
296
|
+
export function openmeshmessage(envelope) {
|
|
297
|
+
try {
|
|
298
|
+
if (envelope === null || typeof envelope !== 'object') {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const message = envelope;
|
|
302
|
+
let data = message.data;
|
|
303
|
+
if (data !== null &&
|
|
304
|
+
typeof data === 'object' &&
|
|
305
|
+
typeof data.enc === 'string') {
|
|
306
|
+
const opened = decrypt(data.enc);
|
|
307
|
+
if (opened === null) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
data = JSON.parse(opened);
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
type: String(message.type ?? ''),
|
|
314
|
+
from: String(message.from ?? ''),
|
|
315
|
+
data,
|
|
316
|
+
ts: String(message.ts ?? ''),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/* ------------------------------------------------------------------ */
|
|
324
|
+
/* context: mainclient */
|
|
325
|
+
/* ------------------------------------------------------------------ */
|
|
326
|
+
/** the signed request timeout in milliseconds. */
|
|
327
|
+
const requesttimeoutms = 10 * 1000;
|
|
328
|
+
/**
|
|
329
|
+
* performs one signed POST towards the main node described by
|
|
330
|
+
* SADDLE_MAIN_URL using the global fetch; the signature headers are
|
|
331
|
+
* computed with the shared SADDLE_MESH_SECRET.
|
|
332
|
+
*
|
|
333
|
+
* @param path the full api path (e.g. /api/v1/auth/login).
|
|
334
|
+
* @param body the json-serializable payload.
|
|
335
|
+
* @returns the response status, the parsed json body and the
|
|
336
|
+
* set-cookie relay.
|
|
337
|
+
* @throws when the mesh is unconfigured, the
|
|
338
|
+
* request fails or the body is not json.
|
|
339
|
+
*/
|
|
340
|
+
export async function postmain(path, body) {
|
|
341
|
+
const base = mainurl();
|
|
342
|
+
const secret = meshsecret();
|
|
343
|
+
if (base.length === 0 || secret.length === 0) {
|
|
344
|
+
throw Object.assign(new Error('SADDLE_MAIN_URL and SADDLE_MESH_SECRET are required for mesh forwarding'), { code: 'mesh-unconfigured' });
|
|
345
|
+
}
|
|
346
|
+
const payload = JSON.stringify(body ?? {});
|
|
347
|
+
const timestamp = String(Date.now());
|
|
348
|
+
const signature = sign('POST', path, payload, secret, timestamp);
|
|
349
|
+
let response;
|
|
350
|
+
try {
|
|
351
|
+
response = await fetch(`${base}${path}`, {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
headers: {
|
|
354
|
+
'content-type': 'application/json',
|
|
355
|
+
'x-saddle-timestamp': timestamp,
|
|
356
|
+
'x-saddle-signature': signature,
|
|
357
|
+
},
|
|
358
|
+
body: payload,
|
|
359
|
+
signal: AbortSignal.timeout(requesttimeoutms),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
throw Object.assign(new Error(`mesh request to ${path} failed: ${error instanceof Error ? error.message : String(error)}`), { code: 'mesh-unreachable' });
|
|
364
|
+
}
|
|
365
|
+
let parsed = null;
|
|
366
|
+
try {
|
|
367
|
+
parsed = await response.json();
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
parsed = null;
|
|
371
|
+
}
|
|
372
|
+
let setcookie = [];
|
|
373
|
+
try {
|
|
374
|
+
setcookie =
|
|
375
|
+
typeof response.headers.getSetCookie === 'function'
|
|
376
|
+
? response.headers.getSetCookie()
|
|
377
|
+
: [];
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
setcookie = [];
|
|
381
|
+
}
|
|
382
|
+
return { status: response.status, body: parsed, setcookie };
|
|
383
|
+
}
|
|
384
|
+
/* ------------------------------------------------------------------ */
|
|
385
|
+
/* context: heartbeat */
|
|
386
|
+
/* ------------------------------------------------------------------ */
|
|
387
|
+
/** the clone heartbeat period in milliseconds. */
|
|
388
|
+
const heartbeatperiodms = 60 * 1000;
|
|
389
|
+
/**
|
|
390
|
+
* runs the clone registry loop: one immediate registration followed by
|
|
391
|
+
* heartbeats every 60 seconds, all signed towards the main node. the
|
|
392
|
+
* interval is unref'd so the process can still exit cleanly.
|
|
393
|
+
*
|
|
394
|
+
* @param self the announcing node description.
|
|
395
|
+
* @returns the heartbeat timer or null when the
|
|
396
|
+
* node is not a clone or the mesh is unconfigured.
|
|
397
|
+
*/
|
|
398
|
+
export function startheartbeat(self) {
|
|
399
|
+
if (role !== 'clone' ||
|
|
400
|
+
mainurl().length === 0 ||
|
|
401
|
+
meshsecret().length === 0 ||
|
|
402
|
+
String(self?.url ?? '').length === 0) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
const beat = async () => {
|
|
406
|
+
try {
|
|
407
|
+
const registered = await postmain('/api/v1/mesh/register', self);
|
|
408
|
+
if (registered.status !== 200 && registered.status !== 201) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
const nodeid = registered.body !== null && typeof registered.body === 'object'
|
|
412
|
+
? String(registered.body?.nodeid ??
|
|
413
|
+
registered.body?.id ??
|
|
414
|
+
'')
|
|
415
|
+
: '';
|
|
416
|
+
if (nodeid.length === 0) {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
const pulsed = await postmain('/api/v1/mesh/heartbeat', { nodeid });
|
|
420
|
+
return pulsed.status === 200;
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
/* the next tick retries; the clone keeps serving locally */
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
/* boot ladder: the immediate beat may race the main node's own boot,
|
|
428
|
+
* so short retries run until the first success; afterwards the steady
|
|
429
|
+
* 60 second interval keeps the registry fresh. */
|
|
430
|
+
let established = false;
|
|
431
|
+
const runbeat = async () => {
|
|
432
|
+
const ok = await beat();
|
|
433
|
+
if (ok) {
|
|
434
|
+
established = true;
|
|
435
|
+
}
|
|
436
|
+
return ok;
|
|
437
|
+
};
|
|
438
|
+
runbeat();
|
|
439
|
+
for (const delay of [2000, 5000, 10000, 20000]) {
|
|
440
|
+
const retry = setTimeout(() => {
|
|
441
|
+
if (!established) {
|
|
442
|
+
runbeat();
|
|
443
|
+
}
|
|
444
|
+
}, delay);
|
|
445
|
+
retry.unref?.();
|
|
446
|
+
}
|
|
447
|
+
const timer = setInterval(() => {
|
|
448
|
+
runbeat();
|
|
449
|
+
}, heartbeatperiodms);
|
|
450
|
+
timer.unref?.();
|
|
451
|
+
return timer;
|
|
452
|
+
}
|
|
453
|
+
/* ------------------------------------------------------------------ */
|
|
454
|
+
/* context: sessionforwarding */
|
|
455
|
+
/* ------------------------------------------------------------------ */
|
|
456
|
+
/**
|
|
457
|
+
* forwards one auth request (register or login) from a clone to the
|
|
458
|
+
* main authority; the caller caches the returned session locally when
|
|
459
|
+
* the main node accepts it.
|
|
460
|
+
*
|
|
461
|
+
* @param action the auth action.
|
|
462
|
+
* @param body the credentials payload.
|
|
463
|
+
* @returns the main response verbatim.
|
|
464
|
+
*/
|
|
465
|
+
export function forwardauth(action, body) {
|
|
466
|
+
const path = action === 'register' ? '/api/v1/auth/register' : '/api/v1/auth/login';
|
|
467
|
+
return postmain(path, body);
|
|
468
|
+
}
|