@runcore-sh/runcore 0.1.9 → 0.1.10
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/cli.js +232 -11
- package/dist/cli.js.map +1 -1
- package/dist/files/deep-index.d.ts +59 -0
- package/dist/files/deep-index.d.ts.map +1 -0
- package/dist/files/deep-index.js +337 -0
- package/dist/files/deep-index.js.map +1 -0
- package/dist/files/import.d.ts +44 -0
- package/dist/files/import.d.ts.map +1 -0
- package/dist/files/import.js +213 -0
- package/dist/files/import.js.map +1 -0
- package/dist/files/index-local.d.ts +37 -0
- package/dist/files/index-local.d.ts.map +1 -0
- package/dist/files/index-local.js +198 -0
- package/dist/files/index-local.js.map +1 -0
- package/dist/nerve/state.d.ts +1 -1
- package/dist/nerve/state.d.ts.map +1 -1
- package/dist/nerve/state.js +1 -1
- package/dist/nerve/state.js.map +1 -1
- package/dist/posture/engine.d.ts +41 -0
- package/dist/posture/engine.d.ts.map +1 -0
- package/dist/posture/engine.js +217 -0
- package/dist/posture/engine.js.map +1 -0
- package/dist/posture/index.d.ts +11 -0
- package/dist/posture/index.d.ts.map +1 -0
- package/dist/posture/index.js +10 -0
- package/dist/posture/index.js.map +1 -0
- package/dist/posture/middleware.d.ts +30 -0
- package/dist/posture/middleware.d.ts.map +1 -0
- package/dist/posture/middleware.js +92 -0
- package/dist/posture/middleware.js.map +1 -0
- package/dist/posture/types.d.ts +61 -0
- package/dist/posture/types.d.ts.map +1 -0
- package/dist/posture/types.js +48 -0
- package/dist/posture/types.js.map +1 -0
- package/dist/server.d.ts +3 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +191 -45
- package/dist/server.js.map +1 -1
- package/dist/tier/bond.d.ts +51 -0
- package/dist/tier/bond.d.ts.map +1 -0
- package/dist/tier/bond.js +154 -0
- package/dist/tier/bond.js.map +1 -0
- package/dist/tier/freeze.d.ts +21 -0
- package/dist/tier/freeze.d.ts.map +1 -0
- package/dist/tier/freeze.js +73 -0
- package/dist/tier/freeze.js.map +1 -0
- package/dist/tier/gate.d.ts +11 -0
- package/dist/tier/gate.d.ts.map +1 -0
- package/dist/tier/gate.js +25 -0
- package/dist/tier/gate.js.map +1 -0
- package/dist/tier/heartbeat.d.ts +22 -0
- package/dist/tier/heartbeat.d.ts.map +1 -0
- package/dist/tier/heartbeat.js +128 -0
- package/dist/tier/heartbeat.js.map +1 -0
- package/dist/tier/token.d.ts +22 -0
- package/dist/tier/token.d.ts.map +1 -0
- package/dist/tier/token.js +100 -0
- package/dist/tier/token.js.map +1 -0
- package/dist/tier/types.d.ts +44 -0
- package/dist/tier/types.d.ts.map +1 -0
- package/dist/tier/types.js +61 -0
- package/dist/tier/types.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bond handshake — establishes trust after activation.
|
|
3
|
+
*
|
|
4
|
+
* Registration = identity ("I know who you are").
|
|
5
|
+
* Bonding = trust ("I can verify it's you").
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. `runcore activate <token>` saves the JWT locally
|
|
9
|
+
* 2. Immediately calls `bond()` — generates an Ed25519 keypair,
|
|
10
|
+
* POSTs the public key + token JTI to the registry
|
|
11
|
+
* 3. Registry stores the public key alongside the approval record
|
|
12
|
+
* 4. Admin (Dash) sees "bonded" status on next poll
|
|
13
|
+
* 5. All future signed messages between instance and registry
|
|
14
|
+
* use this keypair — freeze acks, heartbeat signatures, etc.
|
|
15
|
+
*
|
|
16
|
+
* The token is a one-time introducer. The keypair is the ongoing relationship.
|
|
17
|
+
*/
|
|
18
|
+
interface BondKeys {
|
|
19
|
+
publicKey: string;
|
|
20
|
+
privateKey: string;
|
|
21
|
+
fingerprint: string;
|
|
22
|
+
bondedAt: string;
|
|
23
|
+
}
|
|
24
|
+
/** Load existing bond keys, or null if not yet bonded. */
|
|
25
|
+
export declare function loadBondKeys(root: string): Promise<BondKeys | null>;
|
|
26
|
+
/** Check if this instance has completed bonding. */
|
|
27
|
+
export declare function isBonded(root: string): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Execute the bond handshake.
|
|
30
|
+
* Called immediately after `runcore activate <token>`.
|
|
31
|
+
*
|
|
32
|
+
* 1. Generate keypair (or load existing)
|
|
33
|
+
* 2. POST public key + JTI to registry
|
|
34
|
+
* 3. Save keys locally on success
|
|
35
|
+
*/
|
|
36
|
+
export declare function bond(root: string, jwt: string, jti: string): Promise<{
|
|
37
|
+
fingerprint: string;
|
|
38
|
+
bonded: boolean;
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* Sign a message with this instance's bond private key.
|
|
42
|
+
* Used for authenticated communication after bonding.
|
|
43
|
+
*/
|
|
44
|
+
export declare function signMessage(root: string, message: string): Promise<string | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Verify a message signed by another instance's bond key.
|
|
47
|
+
* Used by the registry to verify instance identity.
|
|
48
|
+
*/
|
|
49
|
+
export declare function verifyMessage(message: string, signature: string, publicKeyPem: string): boolean;
|
|
50
|
+
export {};
|
|
51
|
+
//# sourceMappingURL=bond.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bond.d.ts","sourceRoot":"","sources":["../../src/tier/bond.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAWH,UAAU,QAAQ;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AA+BD,0DAA0D;AAC1D,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CASzE;AASD,oDAAoD;AACpD,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAG7D;AAED;;;;;;;GAOG;AACH,wBAAsB,IAAI,CACxB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC,CA2BnD;AA4BD;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOxB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GACnB,OAAO,CAQT"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bond handshake — establishes trust after activation.
|
|
3
|
+
*
|
|
4
|
+
* Registration = identity ("I know who you are").
|
|
5
|
+
* Bonding = trust ("I can verify it's you").
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. `runcore activate <token>` saves the JWT locally
|
|
9
|
+
* 2. Immediately calls `bond()` — generates an Ed25519 keypair,
|
|
10
|
+
* POSTs the public key + token JTI to the registry
|
|
11
|
+
* 3. Registry stores the public key alongside the approval record
|
|
12
|
+
* 4. Admin (Dash) sees "bonded" status on next poll
|
|
13
|
+
* 5. All future signed messages between instance and registry
|
|
14
|
+
* use this keypair — freeze acks, heartbeat signatures, etc.
|
|
15
|
+
*
|
|
16
|
+
* The token is a one-time introducer. The keypair is the ongoing relationship.
|
|
17
|
+
*/
|
|
18
|
+
import { generateKeyPairSync, createSign, createVerify } from "node:crypto";
|
|
19
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
20
|
+
import { existsSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { createLogger } from "../utils/logger.js";
|
|
23
|
+
const log = createLogger("bond");
|
|
24
|
+
const REGISTRY_URL = "https://runcore.sh/api/registry";
|
|
25
|
+
const KEYS_DIR = ".core";
|
|
26
|
+
const KEYS_FILE = "bond-keys.json";
|
|
27
|
+
function keysPath(root) {
|
|
28
|
+
return join(root, "brain", KEYS_DIR, KEYS_FILE);
|
|
29
|
+
}
|
|
30
|
+
/** Generate a new Ed25519 keypair for this instance. */
|
|
31
|
+
function generateBondKeys() {
|
|
32
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
|
|
33
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
34
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
35
|
+
});
|
|
36
|
+
// Fingerprint = first 16 chars of base64-encoded public key body
|
|
37
|
+
const pubBody = publicKey
|
|
38
|
+
.replace("-----BEGIN PUBLIC KEY-----", "")
|
|
39
|
+
.replace("-----END PUBLIC KEY-----", "")
|
|
40
|
+
.replace(/\s/g, "");
|
|
41
|
+
const fingerprint = pubBody.slice(0, 16);
|
|
42
|
+
return {
|
|
43
|
+
publicKey,
|
|
44
|
+
privateKey,
|
|
45
|
+
fingerprint,
|
|
46
|
+
bondedAt: new Date().toISOString(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Load existing bond keys, or null if not yet bonded. */
|
|
50
|
+
export async function loadBondKeys(root) {
|
|
51
|
+
const path = keysPath(root);
|
|
52
|
+
if (!existsSync(path))
|
|
53
|
+
return null;
|
|
54
|
+
try {
|
|
55
|
+
const raw = await readFile(path, "utf-8");
|
|
56
|
+
return JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Save bond keys to disk. */
|
|
63
|
+
async function saveBondKeys(root, keys) {
|
|
64
|
+
const dir = join(root, "brain", KEYS_DIR);
|
|
65
|
+
await mkdir(dir, { recursive: true });
|
|
66
|
+
await writeFile(keysPath(root), JSON.stringify(keys, null, 2), "utf-8");
|
|
67
|
+
}
|
|
68
|
+
/** Check if this instance has completed bonding. */
|
|
69
|
+
export async function isBonded(root) {
|
|
70
|
+
const keys = await loadBondKeys(root);
|
|
71
|
+
return keys !== null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Execute the bond handshake.
|
|
75
|
+
* Called immediately after `runcore activate <token>`.
|
|
76
|
+
*
|
|
77
|
+
* 1. Generate keypair (or load existing)
|
|
78
|
+
* 2. POST public key + JTI to registry
|
|
79
|
+
* 3. Save keys locally on success
|
|
80
|
+
*/
|
|
81
|
+
export async function bond(root, jwt, jti) {
|
|
82
|
+
// Check for existing bond
|
|
83
|
+
let keys = await loadBondKeys(root);
|
|
84
|
+
if (keys) {
|
|
85
|
+
log.info(`Already bonded (fingerprint: ${keys.fingerprint})`);
|
|
86
|
+
// Re-announce to registry in case it missed us
|
|
87
|
+
await announceToRegistry(jwt, keys);
|
|
88
|
+
return { fingerprint: keys.fingerprint, bonded: true };
|
|
89
|
+
}
|
|
90
|
+
// Generate new keypair
|
|
91
|
+
log.info("Generating bond keypair...");
|
|
92
|
+
keys = generateBondKeys();
|
|
93
|
+
// Announce to registry
|
|
94
|
+
const success = await announceToRegistry(jwt, keys);
|
|
95
|
+
if (!success) {
|
|
96
|
+
log.warn("Bond handshake failed — will retry on next heartbeat");
|
|
97
|
+
// Save keys locally anyway — we'll retry the announcement
|
|
98
|
+
await saveBondKeys(root, keys);
|
|
99
|
+
return { fingerprint: keys.fingerprint, bonded: false };
|
|
100
|
+
}
|
|
101
|
+
// Save keys
|
|
102
|
+
await saveBondKeys(root, keys);
|
|
103
|
+
log.info(`Bonded successfully (fingerprint: ${keys.fingerprint})`);
|
|
104
|
+
return { fingerprint: keys.fingerprint, bonded: true };
|
|
105
|
+
}
|
|
106
|
+
/** POST public key to the registry bond endpoint. */
|
|
107
|
+
async function announceToRegistry(jwt, keys) {
|
|
108
|
+
try {
|
|
109
|
+
const res = await fetch(`${REGISTRY_URL}/bond`, {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: {
|
|
112
|
+
"Content-Type": "application/json",
|
|
113
|
+
Authorization: `Bearer ${jwt}`,
|
|
114
|
+
},
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
publicKey: keys.publicKey,
|
|
117
|
+
fingerprint: keys.fingerprint,
|
|
118
|
+
bondedAt: keys.bondedAt,
|
|
119
|
+
}),
|
|
120
|
+
signal: AbortSignal.timeout(10_000),
|
|
121
|
+
});
|
|
122
|
+
return res.ok;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Sign a message with this instance's bond private key.
|
|
130
|
+
* Used for authenticated communication after bonding.
|
|
131
|
+
*/
|
|
132
|
+
export async function signMessage(root, message) {
|
|
133
|
+
const keys = await loadBondKeys(root);
|
|
134
|
+
if (!keys)
|
|
135
|
+
return null;
|
|
136
|
+
const signer = createSign("Ed25519");
|
|
137
|
+
signer.update(message);
|
|
138
|
+
return signer.sign(keys.privateKey, "base64url");
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Verify a message signed by another instance's bond key.
|
|
142
|
+
* Used by the registry to verify instance identity.
|
|
143
|
+
*/
|
|
144
|
+
export function verifyMessage(message, signature, publicKeyPem) {
|
|
145
|
+
try {
|
|
146
|
+
const verifier = createVerify("Ed25519");
|
|
147
|
+
verifier.update(message);
|
|
148
|
+
return verifier.verify(publicKeyPem, Buffer.from(signature, "base64url"));
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=bond.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bond.js","sourceRoot":"","sources":["../../src/tier/bond.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freeze — dormant mode for agents.
|
|
3
|
+
*
|
|
4
|
+
* When a freeze signal arrives (via heartbeat or local endpoint):
|
|
5
|
+
* 1. All agents go dormant (mid-task, mid-token — frozen, not killed)
|
|
6
|
+
* 2. Metabolic pulses pause
|
|
7
|
+
* 3. No new work starts
|
|
8
|
+
* 4. State is preserved for triage
|
|
9
|
+
*
|
|
10
|
+
* The operator then reviews the frozen field and selectively resumes.
|
|
11
|
+
*/
|
|
12
|
+
import type { FreezeSignal } from "./types.js";
|
|
13
|
+
export declare function isFrozen(): boolean;
|
|
14
|
+
export declare function getFreezeSignal(): FreezeSignal | null;
|
|
15
|
+
export declare function onFreeze(listener: (signal: FreezeSignal) => void): void;
|
|
16
|
+
export declare function onThaw(listener: () => void): void;
|
|
17
|
+
/** Freeze all agents. Called by heartbeat handler or local /api/freeze endpoint. */
|
|
18
|
+
export declare function freeze(signal: FreezeSignal, root: string): Promise<void>;
|
|
19
|
+
/** Thaw — resume operations after operator review. */
|
|
20
|
+
export declare function thaw(root: string): Promise<void>;
|
|
21
|
+
//# sourceMappingURL=freeze.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"freeze.d.ts","sourceRoot":"","sources":["../../src/tier/freeze.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAM/C,wBAAgB,QAAQ,IAAI,OAAO,CAElC;AAED,wBAAgB,eAAe,IAAI,YAAY,GAAG,IAAI,CAErD;AAED,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,GAAG,IAAI,CAEvE;AAED,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAEjD;AAED,oFAAoF;AACpF,wBAAsB,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0B9E;AAED,sDAAsD;AACtD,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBtD"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freeze — dormant mode for agents.
|
|
3
|
+
*
|
|
4
|
+
* When a freeze signal arrives (via heartbeat or local endpoint):
|
|
5
|
+
* 1. All agents go dormant (mid-task, mid-token — frozen, not killed)
|
|
6
|
+
* 2. Metabolic pulses pause
|
|
7
|
+
* 3. No new work starts
|
|
8
|
+
* 4. State is preserved for triage
|
|
9
|
+
*
|
|
10
|
+
* The operator then reviews the frozen field and selectively resumes.
|
|
11
|
+
*/
|
|
12
|
+
import { appendFile } from "node:fs/promises";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
let frozenState = null;
|
|
15
|
+
let freezeListeners = [];
|
|
16
|
+
let thawListeners = [];
|
|
17
|
+
export function isFrozen() {
|
|
18
|
+
return frozenState !== null;
|
|
19
|
+
}
|
|
20
|
+
export function getFreezeSignal() {
|
|
21
|
+
return frozenState;
|
|
22
|
+
}
|
|
23
|
+
export function onFreeze(listener) {
|
|
24
|
+
freezeListeners.push(listener);
|
|
25
|
+
}
|
|
26
|
+
export function onThaw(listener) {
|
|
27
|
+
thawListeners.push(listener);
|
|
28
|
+
}
|
|
29
|
+
/** Freeze all agents. Called by heartbeat handler or local /api/freeze endpoint. */
|
|
30
|
+
export async function freeze(signal, root) {
|
|
31
|
+
frozenState = signal;
|
|
32
|
+
// Log the freeze event
|
|
33
|
+
const entry = JSON.stringify({
|
|
34
|
+
type: "freeze",
|
|
35
|
+
...signal,
|
|
36
|
+
frozenAt: new Date().toISOString(),
|
|
37
|
+
});
|
|
38
|
+
await appendFile(join(root, "brain", "ops", "audit.jsonl"), entry + "\n").catch(() => { });
|
|
39
|
+
// Notify all listeners (agent pool, metabolic pulse, work queue)
|
|
40
|
+
for (const listener of freezeListeners) {
|
|
41
|
+
try {
|
|
42
|
+
listener(signal);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Don't let a listener failure prevent freeze
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
console.log(`\n FROZEN — ${signal.reason}`);
|
|
49
|
+
console.log(` Issued by: ${signal.issuedBy} at ${signal.issuedAt}`);
|
|
50
|
+
console.log(` All agents dormant. Awaiting operator triage.\n`);
|
|
51
|
+
}
|
|
52
|
+
/** Thaw — resume operations after operator review. */
|
|
53
|
+
export async function thaw(root) {
|
|
54
|
+
if (!frozenState)
|
|
55
|
+
return;
|
|
56
|
+
const entry = JSON.stringify({
|
|
57
|
+
type: "thaw",
|
|
58
|
+
previousFreeze: frozenState.jti,
|
|
59
|
+
thawedAt: new Date().toISOString(),
|
|
60
|
+
});
|
|
61
|
+
await appendFile(join(root, "brain", "ops", "audit.jsonl"), entry + "\n").catch(() => { });
|
|
62
|
+
frozenState = null;
|
|
63
|
+
for (const listener of thawListeners) {
|
|
64
|
+
try {
|
|
65
|
+
listener();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Don't let a listener failure prevent thaw
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
console.log(" THAWED — operations resuming.\n");
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=freeze.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"freeze.js","sourceRoot":"","sources":["../../src/tier/freeze.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,IAAI,WAAW,GAAwB,IAAI,CAAC;AAC5C,IAAI,eAAe,GAA0C,EAAE,CAAC;AAChE,IAAI,aAAa,GAAsB,EAAE,CAAC;AAE1C,MAAM,UAAU,QAAQ;IACtB,OAAO,WAAW,KAAK,IAAI,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,QAAwC;IAC/D,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,QAAoB;IACzC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAC/B,CAAC;AAED,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,MAAoB,EAAE,IAAY;IAC7D,WAAW,GAAG,MAAM,CAAC;IAErB,uBAAuB;IACvB,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC;QAC3B,IAAI,EAAE,QAAQ;QACd,GAAG,MAAM;QACT,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACnC,CAAC,CAAC;IACH,MAAM,UAAU,CACd,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,CAAC,EACzC,KAAK,GAAG,IAAI,CACb,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAElB,iEAAiE;IACjE,KAAK,MAAM,QAAQ,IAAI,eAAe,EAAE,CAAC;QACvC,IAAI,CAAC;YACH,QAAQ,CAAC,MAAM,CAAC,CAAC;QACnB,CAAC;QAAC,MAAM,CAAC;YACP,8CAA8C;QAChD,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,gBAAgB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7C,OAAO,CAAC,GAAG,CAAC,gBAAgB,MAAM,CAAC,QAAQ,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IACrE,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC,CAAC;AACnE,CAAC;AAED,sDAAsD;AACtD,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAY;IACrC,IAAI,CAAC,WAAW;QAAE,OAAO;IAEzB,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC;QAC3B,IAAI,EAAE,MAAM;QACZ,cAAc,EAAE,WAAW,CAAC,GAAG;QAC/B,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACnC,CAAC,CAAC;IACH,MAAM,UAAU,CACd,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,CAAC,EACzC,KAAK,GAAG,IAAI,CACb,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAElB,WAAW,GAAG,IAAI,CAAC;IAEnB,KAAK,MAAM,QAAQ,IAAI,aAAa,EAAE,CAAC;QACrC,IAAI,CAAC;YACH,QAAQ,EAAE,CAAC;QACb,CAAC;QAAC,MAAM,CAAC;YACP,4CAA4C;QAC9C,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability gate — checks tier before allowing features.
|
|
3
|
+
*/
|
|
4
|
+
import { type TierName } from "./types.js";
|
|
5
|
+
export declare function meetsMinimum(current: TierName, required: TierName): boolean;
|
|
6
|
+
export declare function canServe(tier: TierName): boolean;
|
|
7
|
+
export declare function canMesh(tier: TierName): boolean;
|
|
8
|
+
export declare function canSpawn(tier: TierName): boolean;
|
|
9
|
+
export declare function canAlert(tier: TierName): boolean;
|
|
10
|
+
export declare function requireTier(current: TierName, required: TierName): void;
|
|
11
|
+
//# sourceMappingURL=gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/tier/gate.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,QAAQ,EAAyB,MAAM,YAAY,CAAC;AAElE,wBAAgB,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAE3E;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAEhD;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAE/C;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAEhD;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAEhD;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAMvE"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability gate — checks tier before allowing features.
|
|
3
|
+
*/
|
|
4
|
+
import { TIER_LEVEL, TIER_CAPS } from "./types.js";
|
|
5
|
+
export function meetsMinimum(current, required) {
|
|
6
|
+
return TIER_LEVEL[current] >= TIER_LEVEL[required];
|
|
7
|
+
}
|
|
8
|
+
export function canServe(tier) {
|
|
9
|
+
return TIER_CAPS[tier].server;
|
|
10
|
+
}
|
|
11
|
+
export function canMesh(tier) {
|
|
12
|
+
return TIER_CAPS[tier].mesh;
|
|
13
|
+
}
|
|
14
|
+
export function canSpawn(tier) {
|
|
15
|
+
return TIER_CAPS[tier].spawning;
|
|
16
|
+
}
|
|
17
|
+
export function canAlert(tier) {
|
|
18
|
+
return TIER_CAPS[tier].alerting;
|
|
19
|
+
}
|
|
20
|
+
export function requireTier(current, required) {
|
|
21
|
+
if (!meetsMinimum(current, required)) {
|
|
22
|
+
throw new Error(`Tier "${required}" required (current: "${current}"). Run \`runcore register\` to upgrade.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../../src/tier/gate.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAiB,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAElE,MAAM,UAAU,YAAY,CAAC,OAAiB,EAAE,QAAkB;IAChE,OAAO,UAAU,CAAC,OAAO,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAc;IACrC,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,IAAc;IACpC,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAc;IACrC,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAc;IACrC,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAiB,EAAE,QAAkB;IAC/D,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,SAAS,QAAQ,yBAAyB,OAAO,0CAA0C,CAC5F,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry heartbeat — periodic check-in for tier >= byok.
|
|
3
|
+
*
|
|
4
|
+
* Reports: version, tier, uptime.
|
|
5
|
+
* Receives: token validity, freeze signals.
|
|
6
|
+
* Non-blocking, best-effort. Failures are logged, not fatal.
|
|
7
|
+
*/
|
|
8
|
+
import type { TierName, FreezeSignal } from "./types.js";
|
|
9
|
+
export interface HeartbeatResponse {
|
|
10
|
+
valid: boolean;
|
|
11
|
+
frozen?: boolean;
|
|
12
|
+
freeze?: FreezeSignal;
|
|
13
|
+
tier?: TierName;
|
|
14
|
+
}
|
|
15
|
+
export type FreezeHandler = (signal: FreezeSignal) => void;
|
|
16
|
+
export type DowngradeHandler = (newTier: TierName) => void;
|
|
17
|
+
export declare function onFreezeSignal(handler: FreezeHandler): void;
|
|
18
|
+
export declare function onTierDowngrade(handler: DowngradeHandler): void;
|
|
19
|
+
export declare function isFrozen(): boolean;
|
|
20
|
+
export declare function startHeartbeat(jwt: string, tier: TierName, root?: string): void;
|
|
21
|
+
export declare function stopHeartbeat(): void;
|
|
22
|
+
//# sourceMappingURL=heartbeat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heartbeat.d.ts","sourceRoot":"","sources":["../../src/tier/heartbeat.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAWzD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,IAAI,CAAC,EAAE,QAAQ,CAAC;CACjB;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;AAC3D,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;AAK3D,wBAAgB,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAE3D;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAE/D;AAED,wBAAgB,QAAQ,IAAI,OAAO,CAElC;AAkED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAmB/E;AAkBD,wBAAgB,aAAa,IAAI,IAAI,CAKpC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry heartbeat — periodic check-in for tier >= byok.
|
|
3
|
+
*
|
|
4
|
+
* Reports: version, tier, uptime.
|
|
5
|
+
* Receives: token validity, freeze signals.
|
|
6
|
+
* Non-blocking, best-effort. Failures are logged, not fatal.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
const REGISTRY_URL = "https://runcore.sh/api/registry";
|
|
11
|
+
const HEARTBEAT_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
12
|
+
const REVALIDATE_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
13
|
+
let heartbeatTimer = null;
|
|
14
|
+
let revalidateTimer = null;
|
|
15
|
+
let startedAt = Date.now();
|
|
16
|
+
let frozen = false;
|
|
17
|
+
let onFreeze = null;
|
|
18
|
+
let onDowngrade = null;
|
|
19
|
+
export function onFreezeSignal(handler) {
|
|
20
|
+
onFreeze = handler;
|
|
21
|
+
}
|
|
22
|
+
export function onTierDowngrade(handler) {
|
|
23
|
+
onDowngrade = handler;
|
|
24
|
+
}
|
|
25
|
+
export function isFrozen() {
|
|
26
|
+
return frozen;
|
|
27
|
+
}
|
|
28
|
+
function getVersion() {
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse(readFileSync(join(import.meta.dirname, "../../package.json"), "utf-8"));
|
|
31
|
+
return pkg.version ?? "unknown";
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return "unknown";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function sendHeartbeat(jwt, tier) {
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(`${REGISTRY_URL}/heartbeat`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
Authorization: `Bearer ${jwt}`,
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
version: getVersion(),
|
|
47
|
+
tier,
|
|
48
|
+
uptime: Math.floor((Date.now() - startedAt) / 1000),
|
|
49
|
+
}),
|
|
50
|
+
signal: AbortSignal.timeout(10_000),
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok)
|
|
53
|
+
return;
|
|
54
|
+
const data = (await res.json());
|
|
55
|
+
if (data.frozen && data.freeze) {
|
|
56
|
+
frozen = true;
|
|
57
|
+
onFreeze?.(data.freeze);
|
|
58
|
+
}
|
|
59
|
+
if (!data.valid) {
|
|
60
|
+
// Token revoked — downgrade to local
|
|
61
|
+
onDowngrade?.("local");
|
|
62
|
+
}
|
|
63
|
+
if (data.tier && data.tier !== tier) {
|
|
64
|
+
// Tier changed (upgrade or downgrade by admin)
|
|
65
|
+
onDowngrade?.(data.tier);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Best effort — swallow network errors
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function revalidateToken(jwt) {
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch(`${REGISTRY_URL}/validate`, {
|
|
75
|
+
headers: { Authorization: `Bearer ${jwt}` },
|
|
76
|
+
signal: AbortSignal.timeout(10_000),
|
|
77
|
+
});
|
|
78
|
+
if (!res.ok)
|
|
79
|
+
return false;
|
|
80
|
+
const data = (await res.json());
|
|
81
|
+
return data.valid === true;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return true; // Assume valid if we can't reach registry (offline-first)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function startHeartbeat(jwt, tier, root) {
|
|
88
|
+
startedAt = Date.now();
|
|
89
|
+
// Retry bond if not yet confirmed
|
|
90
|
+
if (root) {
|
|
91
|
+
retryBondIfNeeded(root, jwt).catch(() => { });
|
|
92
|
+
}
|
|
93
|
+
// Immediate first heartbeat
|
|
94
|
+
sendHeartbeat(jwt, tier);
|
|
95
|
+
heartbeatTimer = setInterval(() => sendHeartbeat(jwt, tier), HEARTBEAT_INTERVAL_MS);
|
|
96
|
+
heartbeatTimer.unref();
|
|
97
|
+
revalidateTimer = setInterval(async () => {
|
|
98
|
+
const valid = await revalidateToken(jwt);
|
|
99
|
+
if (!valid)
|
|
100
|
+
onDowngrade?.("local");
|
|
101
|
+
}, REVALIDATE_INTERVAL_MS);
|
|
102
|
+
revalidateTimer.unref();
|
|
103
|
+
}
|
|
104
|
+
/** Retry bond announcement if keys exist locally but registry hasn't confirmed. */
|
|
105
|
+
async function retryBondIfNeeded(root, jwt) {
|
|
106
|
+
try {
|
|
107
|
+
const { loadBondKeys, bond } = await import("./bond.js");
|
|
108
|
+
const keys = await loadBondKeys(root);
|
|
109
|
+
if (!keys)
|
|
110
|
+
return; // No keys = not activated yet, nothing to retry
|
|
111
|
+
// Try to announce again — bond() handles the idempotency
|
|
112
|
+
const parts = jwt.split(".");
|
|
113
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
|
|
114
|
+
await bond(root, jwt, payload.jti);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Best effort
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export function stopHeartbeat() {
|
|
121
|
+
if (heartbeatTimer)
|
|
122
|
+
clearInterval(heartbeatTimer);
|
|
123
|
+
if (revalidateTimer)
|
|
124
|
+
clearInterval(revalidateTimer);
|
|
125
|
+
heartbeatTimer = null;
|
|
126
|
+
revalidateTimer = null;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=heartbeat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heartbeat.js","sourceRoot":"","sources":["../../src/tier/heartbeat.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activation token — Ed25519 signed JWT, offline-verifiable.
|
|
3
|
+
*
|
|
4
|
+
* Private key lives in the registry backend (Dash's dead drop / Cloudflare Worker).
|
|
5
|
+
* Public key ships in this package. Tokens validate without network.
|
|
6
|
+
* Revocation is checked periodically (24h) via registry heartbeat.
|
|
7
|
+
*/
|
|
8
|
+
import { type ActivationToken, type TierName } from "./types.js";
|
|
9
|
+
/** Sign a JWT with Ed25519 private key (used by Dash / registry backend) */
|
|
10
|
+
export declare function signToken(payload: ActivationToken, privateKeyPem: string): string;
|
|
11
|
+
/** Load and validate the local activation token. Returns null if none or invalid. */
|
|
12
|
+
export declare function loadActivationToken(root: string): Promise<{
|
|
13
|
+
token: ActivationToken;
|
|
14
|
+
raw: string;
|
|
15
|
+
} | null>;
|
|
16
|
+
/** Store an activation token to disk */
|
|
17
|
+
export declare function saveActivationToken(root: string, jwt: string): Promise<ActivationToken>;
|
|
18
|
+
/** Get the current tier from the stored token, defaulting to "local" */
|
|
19
|
+
export declare function currentTier(root: string): Promise<TierName>;
|
|
20
|
+
/** Set a custom public key (for testing or key rotation) */
|
|
21
|
+
export declare function setPublicKey(pem: string): void;
|
|
22
|
+
//# sourceMappingURL=token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../src/tier/token.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAsCjE,4EAA4E;AAC5E,wBAAgB,SAAS,CACvB,OAAO,EAAE,eAAe,EACxB,aAAa,EAAE,MAAM,GACpB,MAAM,CAUR;AAED,qFAAqF;AACrF,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,KAAK,EAAE,eAAe,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAqBzD;AAED,wCAAwC;AACxC,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,eAAe,CAAC,CAU1B;AAED,wEAAwE;AACxE,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAGjE;AAED,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAG9C"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activation token — Ed25519 signed JWT, offline-verifiable.
|
|
3
|
+
*
|
|
4
|
+
* Private key lives in the registry backend (Dash's dead drop / Cloudflare Worker).
|
|
5
|
+
* Public key ships in this package. Tokens validate without network.
|
|
6
|
+
* Revocation is checked periodically (24h) via registry heartbeat.
|
|
7
|
+
*/
|
|
8
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { createVerify, createSign } from "node:crypto";
|
|
11
|
+
// Ed25519 public key — embedded in package, used for offline token verification.
|
|
12
|
+
// Replace with real key after generating the keypair.
|
|
13
|
+
const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
14
|
+
MCowBQYDK2VwAyEAPLACENTER_REAL_KEY_HERE_AFTER_KEYGEN=
|
|
15
|
+
-----END PUBLIC KEY-----`;
|
|
16
|
+
const TOKEN_DIR = ".core";
|
|
17
|
+
const TOKEN_FILE = "activation.json";
|
|
18
|
+
function tokenPath(root) {
|
|
19
|
+
return join(root, "brain", TOKEN_DIR, TOKEN_FILE);
|
|
20
|
+
}
|
|
21
|
+
/** Decode a compact JWT (header.payload.signature) without verification */
|
|
22
|
+
function decodePayload(jwt) {
|
|
23
|
+
const parts = jwt.split(".");
|
|
24
|
+
if (parts.length !== 3)
|
|
25
|
+
throw new Error("Invalid token format");
|
|
26
|
+
const payload = Buffer.from(parts[1], "base64url").toString("utf-8");
|
|
27
|
+
return JSON.parse(payload);
|
|
28
|
+
}
|
|
29
|
+
/** Verify Ed25519 signature on a JWT */
|
|
30
|
+
function verifySignature(jwt, publicKey) {
|
|
31
|
+
const parts = jwt.split(".");
|
|
32
|
+
if (parts.length !== 3)
|
|
33
|
+
return false;
|
|
34
|
+
const data = `${parts[0]}.${parts[1]}`;
|
|
35
|
+
const signature = Buffer.from(parts[2], "base64url");
|
|
36
|
+
const verifier = createVerify("Ed25519");
|
|
37
|
+
verifier.update(data);
|
|
38
|
+
try {
|
|
39
|
+
return verifier.verify(publicKey, signature);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Sign a JWT with Ed25519 private key (used by Dash / registry backend) */
|
|
46
|
+
export function signToken(payload, privateKeyPem) {
|
|
47
|
+
const header = Buffer.from(JSON.stringify({ alg: "EdDSA", typ: "JWT" })).toString("base64url");
|
|
48
|
+
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
49
|
+
const data = `${header}.${body}`;
|
|
50
|
+
const signer = createSign("Ed25519");
|
|
51
|
+
signer.update(data);
|
|
52
|
+
const signature = signer.sign(privateKeyPem, "base64url");
|
|
53
|
+
return `${data}.${signature}`;
|
|
54
|
+
}
|
|
55
|
+
/** Load and validate the local activation token. Returns null if none or invalid. */
|
|
56
|
+
export async function loadActivationToken(root) {
|
|
57
|
+
try {
|
|
58
|
+
const raw = (await readFile(tokenPath(root), "utf-8")).trim();
|
|
59
|
+
if (!raw)
|
|
60
|
+
return null;
|
|
61
|
+
if (!verifySignature(raw, PUBLIC_KEY_PEM)) {
|
|
62
|
+
console.warn(" Activation token has invalid signature — ignoring.");
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const token = decodePayload(raw);
|
|
66
|
+
if (new Date(token.expires) < new Date()) {
|
|
67
|
+
console.warn(" Activation token expired — running as Local tier.");
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return { token, raw };
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Store an activation token to disk */
|
|
77
|
+
export async function saveActivationToken(root, jwt) {
|
|
78
|
+
if (!verifySignature(jwt, PUBLIC_KEY_PEM)) {
|
|
79
|
+
throw new Error("Token signature verification failed. Token not saved.");
|
|
80
|
+
}
|
|
81
|
+
const token = decodePayload(jwt);
|
|
82
|
+
const dir = join(root, "brain", TOKEN_DIR);
|
|
83
|
+
await mkdir(dir, { recursive: true });
|
|
84
|
+
await writeFile(tokenPath(root), jwt, "utf-8");
|
|
85
|
+
return token;
|
|
86
|
+
}
|
|
87
|
+
/** Get the current tier from the stored token, defaulting to "local" */
|
|
88
|
+
export async function currentTier(root) {
|
|
89
|
+
const result = await loadActivationToken(root);
|
|
90
|
+
return result?.token.tier ?? "local";
|
|
91
|
+
}
|
|
92
|
+
/** Set a custom public key (for testing or key rotation) */
|
|
93
|
+
export function setPublicKey(pem) {
|
|
94
|
+
// Only used in test — production uses the embedded key
|
|
95
|
+
globalThis.__CORE_TIER_PUBKEY = pem;
|
|
96
|
+
}
|
|
97
|
+
function getPublicKey() {
|
|
98
|
+
return globalThis.__CORE_TIER_PUBKEY ?? PUBLIC_KEY_PEM;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=token.js.map
|