@aetheros/kernel-runtime 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/package.json +38 -4
- package/src/agent-memory.js +88 -0
- package/src/audit.js +88 -0
- package/src/authenticated-audit.js +24 -0
- package/src/authenticated-connectors.js +261 -0
- package/src/backups.js +44 -0
- package/src/canonical-json.js +22 -0
- package/src/connectors.js +71 -0
- package/src/effect-binding.js +18 -0
- package/src/effect-journal.js +142 -0
- package/src/encrypted-backups.js +61 -0
- package/src/encrypted-state.js +35 -0
- package/src/envelope-migrations.js +62 -0
- package/src/guarded-restore.js +68 -0
- package/src/index.js +77 -0
- package/src/migrations.js +92 -0
- package/src/release-signatures.js +56 -0
- package/src/transactions.js +50 -0
- package/README.md +0 -3
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {createHash, createPrivateKey, createPublicKey, KeyObject, sign, verify} from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
4
|
+
const REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
5
|
+
const REVISION = /^[a-f0-9]{40}$/;
|
|
6
|
+
function fields(value, names) {
|
|
7
|
+
if (!value || Object.getPrototypeOf(value) !== Object.prototype) throw new Error('Invalid release statement');
|
|
8
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
9
|
+
if (Reflect.ownKeys(value).length !== names.length || Reflect.ownKeys(value).some(key => !names.includes(key) || !descriptors[key].enumerable || !Object.hasOwn(descriptors[key], 'value'))) throw new Error('Invalid release statement');
|
|
10
|
+
}
|
|
11
|
+
function source(repository, revision) {
|
|
12
|
+
if (typeof repository !== 'string' || repository.length > 256 || !REPOSITORY.test(repository) || typeof revision !== 'string' || !REVISION.test(revision)) throw new Error('Expected repository and full revision required');
|
|
13
|
+
}
|
|
14
|
+
function manifestDigest(bytes) {
|
|
15
|
+
if (!Buffer.isBuffer(bytes) || !bytes.length || bytes.length > 1024 * 1024) throw new Error('Manifest must be bounded raw bytes');
|
|
16
|
+
return hash(bytes);
|
|
17
|
+
}
|
|
18
|
+
function keyObject(key, type) {
|
|
19
|
+
let selected;
|
|
20
|
+
try { selected = key instanceof KeyObject ? key : type === 'private' ? createPrivateKey(key) : createPublicKey(key); }
|
|
21
|
+
catch { throw new Error('Invalid release signing key'); }
|
|
22
|
+
if (type === 'public' && selected.type === 'private') selected = createPublicKey(selected);
|
|
23
|
+
if (selected.type !== type || selected.asymmetricKeyType !== 'ed25519') throw new Error('Ed25519 release key required');
|
|
24
|
+
return selected;
|
|
25
|
+
}
|
|
26
|
+
const fingerprint = key => hash(keyObject(key, 'public').export({type: 'spki', format: 'der'}));
|
|
27
|
+
function statement(repository, revision, manifestSha256) {
|
|
28
|
+
source(repository, revision);
|
|
29
|
+
if (typeof manifestSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(manifestSha256)) throw new Error('Invalid manifest digest');
|
|
30
|
+
return {domain: 'aetheros.release.v1', schemaVersion: 1, repository, revision, manifestSha256};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Host explicitly supplies a private key; never called by pack or release workflows. */
|
|
34
|
+
export function signReleaseManifest(manifestBytes, {privateKey, repository, revision} = {}) {
|
|
35
|
+
const payload = statement(repository, revision, manifestDigest(manifestBytes));
|
|
36
|
+
const key = keyObject(privateKey, 'private');
|
|
37
|
+
return {statement: payload, keyId: fingerprint(key), signature: sign(null, Buffer.from(JSON.stringify(payload)), key).toString('base64')};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Expectations and trust anchor must come from consumer policy, never the signed file. */
|
|
41
|
+
export function verifySignedRelease(manifestBytes, envelope, {publicKey, repository, revision} = {}) {
|
|
42
|
+
source(repository, revision);
|
|
43
|
+
fields(envelope, ['statement', 'keyId', 'signature']);
|
|
44
|
+
fields(envelope.statement, ['domain', 'schemaVersion', 'repository', 'revision', 'manifestSha256']);
|
|
45
|
+
const claimed = envelope.statement;
|
|
46
|
+
if (claimed.domain !== 'aetheros.release.v1' || claimed.schemaVersion !== 1) throw new Error('Unsupported release statement');
|
|
47
|
+
const payload = statement(claimed.repository, claimed.revision, claimed.manifestSha256), key = keyObject(publicKey, 'public');
|
|
48
|
+
if (typeof envelope.keyId !== 'string' || envelope.keyId !== fingerprint(key)) throw new Error('Untrusted release signing key');
|
|
49
|
+
if (typeof envelope.signature !== 'string' || !/^[A-Za-z0-9+/]{86}==$/.test(envelope.signature)) throw new Error('Invalid release signature');
|
|
50
|
+
const signature = Buffer.from(envelope.signature, 'base64');
|
|
51
|
+
if (signature.toString('base64') !== envelope.signature || !verify(null, Buffer.from(JSON.stringify(payload)), key, signature)) throw new Error('Release signature verification failed');
|
|
52
|
+
if (payload.repository !== repository || payload.revision !== revision) throw new Error('Release source does not match consumer policy');
|
|
53
|
+
if (manifestDigest(manifestBytes) !== payload.manifestSha256) throw new Error('Signed manifest digest mismatch');
|
|
54
|
+
return {repository, revision, keyId: envelope.keyId, manifestSha256: payload.manifestSha256};
|
|
55
|
+
}
|
|
56
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {randomBytes} from 'node:crypto';
|
|
2
|
+
import {open,mkdir,readFile,unlink} from 'node:fs/promises';
|
|
3
|
+
import {dirname} from 'node:path';
|
|
4
|
+
import {FileStateStore} from './index.js';
|
|
5
|
+
|
|
6
|
+
export class StateBusyError extends Error {
|
|
7
|
+
constructor(){super('State store is locked by another transaction');this.name='StateBusyError';}
|
|
8
|
+
}
|
|
9
|
+
/** Cooperating processes, one canonical path. No stale-lock takeover or implicit retry. */
|
|
10
|
+
export class TransactionalStateStore extends FileStateStore {
|
|
11
|
+
async transaction(update) {
|
|
12
|
+
if(typeof update!=='function')throw new TypeError('Transaction callback required');
|
|
13
|
+
const lock=this.path+'.lock',lease=randomBytes(24).toString('hex');
|
|
14
|
+
await mkdir(dirname(this.path),{recursive:true});
|
|
15
|
+
let handle;
|
|
16
|
+
try{handle=await open(lock,'wx',0o600);}catch(error){if(error.code==='EEXIST')throw new StateBusyError();throw error;}
|
|
17
|
+
try {
|
|
18
|
+
try{await handle.writeFile(lease,'utf8');await handle.sync();}finally{await handle.close();}
|
|
19
|
+
const previous=await super.load();
|
|
20
|
+
const next=await update(structuredClone(previous));
|
|
21
|
+
// Clone before awaiting I/O so a caller cannot change the snapshot during save.
|
|
22
|
+
const snapshot=structuredClone(next);
|
|
23
|
+
await super.save(snapshot);
|
|
24
|
+
return structuredClone(snapshot);
|
|
25
|
+
} finally {
|
|
26
|
+
// Never remove a replacement lock if an external operator changed ownership.
|
|
27
|
+
const owner=await readFile(lock,'utf8').catch(error=>{if(error.code==='ENOENT')return null;throw error;});
|
|
28
|
+
if(owner===lease)await unlink(lock);
|
|
29
|
+
else throw new Error('State lock ownership changed; manual recovery required');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async save(state){const snapshot=structuredClone(state);await this.transaction(()=>snapshot);}
|
|
33
|
+
async putTenant(id,data){
|
|
34
|
+
if(typeof id!=='string'||!id.trim())throw new Error('Tenant id required');
|
|
35
|
+
if(data===undefined)throw new Error('Tenant data required');
|
|
36
|
+
const snapshot=structuredClone(data);
|
|
37
|
+
await this.transaction(state=>{Object.defineProperty(state.tenants,id,{value:snapshot,enumerable:true,writable:true,configurable:true});return state;});
|
|
38
|
+
}
|
|
39
|
+
async updateTenant(id,update){
|
|
40
|
+
if(typeof id!=='string'||!id.trim()||typeof update!=='function')throw new TypeError('Tenant id and update callback required');
|
|
41
|
+
let result;
|
|
42
|
+
await this.transaction(async state=>{
|
|
43
|
+
const previous=Object.hasOwn(state.tenants,id)?structuredClone(state.tenants[id]):null;
|
|
44
|
+
result=structuredClone(await update(previous));
|
|
45
|
+
if(result===undefined)throw new Error('Tenant data required');
|
|
46
|
+
Object.defineProperty(state.tenants,id,{value:result,enumerable:true,writable:true,configurable:true});return state;
|
|
47
|
+
});
|
|
48
|
+
return structuredClone(result);
|
|
49
|
+
}
|
|
50
|
+
}
|
package/README.md
DELETED