@xshieldai/aegis-guard 0.2.0 → 0.3.1

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 CHANGED
@@ -1,4 +1,10 @@
1
- # @rocketlang/aegis-guard
1
+ # @xshieldai/aegis-guard
2
+
3
+ > **🔍 Verification status (2026-10-05 IST — v0.3.1)**
4
+ > - **Tests:** ✅ **70/70 passing** in the repository; the npm package ships `src/` only, so run them from a clone ([tests/aegis-guard.test.ts](https://github.com/rocketlang/aegis/blob/master/packages/aegis-guard/tests/aegis-guard.test.ts) — `bun test`)
5
+ > - **Examples:** ✅ runnable quickstart, in the repository (not in the npm package): [examples/quickstart.ts](https://github.com/rocketlang/aegis/blob/master/packages/aegis-guard/examples/quickstart.ts) — `bun run examples/quickstart.ts` shows all 5 Locks in action with receipts
6
+ > - **Live demo:** ⚠️ planned (Tier 3, see [PROOF-STACK.md](https://github.com/rocketlang/aegis/blob/master/PROOF-STACK.md))
7
+ > - **Phase-1 limits:** documented in the v0.2.0 ACC section below
2
8
 
3
9
  AEGIS Guard SDK — reusable approval-token, nonce, idempotency, SENSE, and quality-evidence primitives for AEGIS-governed services.
4
10
 
@@ -19,9 +25,9 @@ The Five Locks were proven across 13 batches (62–74) of carbonx-backend. This
19
25
  ## Install
20
26
 
21
27
  ```bash
22
- bun add @rocketlang/aegis-guard
28
+ bun add @xshieldai/aegis-guard
23
29
  # or
24
- npm install @rocketlang/aegis-guard
30
+ npm install @xshieldai/aegis-guard
25
31
  ```
26
32
 
27
33
  ## Usage
@@ -29,7 +35,7 @@ npm install @rocketlang/aegis-guard
29
35
  ### LOCK_1 + LOCK_2 — decision + identity
30
36
 
31
37
  ```typescript
32
- import { verifyApprovalToken, verifyScopedApprovalToken } from '@rocketlang/aegis-guard';
38
+ import { verifyApprovalToken, verifyScopedApprovalToken } from '@xshieldai/aegis-guard';
33
39
 
34
40
  // LOCK_1 — base token verification (service_id + capability + operation)
35
41
  const payload = verifyApprovalToken(token, 'my-service', 'settle', 'record_settle');
@@ -44,7 +50,7 @@ const payload = verifyScopedApprovalToken(
44
50
  ### LOCK_3 — observability (SENSE)
45
51
 
46
52
  ```typescript
47
- import { emitAegisSenseEvent, digestApprovalToken, configureSenseTransport } from '@rocketlang/aegis-guard';
53
+ import { emitAegisSenseEvent, digestApprovalToken, configureSenseTransport } from '@xshieldai/aegis-guard';
48
54
 
49
55
  // Wire your logger (default: process.stdout JSON)
50
56
  configureSenseTransport((event) => logger.info(event, `SENSE:${event.event_type}`));
@@ -67,7 +73,7 @@ emitAegisSenseEvent({
67
73
  ### LOCK_4 — rollback guard (idempotency check)
68
74
 
69
75
  ```typescript
70
- import { checkIdempotency, buildIdempotencyFingerprint } from '@rocketlang/aegis-guard';
76
+ import { checkIdempotency, buildIdempotencyFingerprint } from '@xshieldai/aegis-guard';
71
77
 
72
78
  const existing = await db.findByExternalRef(args.externalRef);
73
79
  const fp = buildIdempotencyFingerprint({ amount: args.amount, vessel_id: args.vesselId });
@@ -80,7 +86,7 @@ if (isDuplicate && !safeNoOp) throw new Error('payload mismatch on duplicate ext
80
86
  ### LOCK_5 — nonce replay prevention
81
87
 
82
88
  ```typescript
83
- import { verifyAndConsumeNonce } from '@rocketlang/aegis-guard';
89
+ import { verifyAndConsumeNonce } from '@xshieldai/aegis-guard';
84
90
 
85
91
  // Requires nonce in payload; throws IrrNoApprovalError on missing or replayed nonce
86
92
  await verifyAndConsumeNonce(payload, redisNonceStore);
@@ -89,7 +95,7 @@ await verifyAndConsumeNonce(payload, redisNonceStore);
89
95
  ### Quality evidence
90
96
 
91
97
  ```typescript
92
- import { buildQualityMaskAtPromotion, meetsHgQualityRequirement } from '@rocketlang/aegis-guard';
98
+ import { buildQualityMaskAtPromotion, meetsHgQualityRequirement } from '@xshieldai/aegis-guard';
93
99
 
94
100
  const mask = buildQualityMaskAtPromotion({
95
101
  tests_passed: true,
@@ -105,7 +111,7 @@ const ready = meetsHgQualityRequirement('HG-2B-financial', mask);
105
111
  The default `defaultNonceStore` is in-memory (single-process only). Multi-instance deployments must provide a Redis-backed store:
106
112
 
107
113
  ```typescript
108
- import { type NonceStore } from '@rocketlang/aegis-guard';
114
+ import { type NonceStore } from '@xshieldai/aegis-guard';
109
115
 
110
116
  const redisNonceStore: NonceStore = {
111
117
  async consumeNonce(nonce, ttlMs) {
@@ -139,7 +145,7 @@ v0.2.0 behaves identically to v0.1.0 — no emission, no state, no side effect.
139
145
  ### Wire it in 3 lines
140
146
 
141
147
  ```typescript
142
- import { setEventBus, type EventBus, type AccReceipt } from '@rocketlang/aegis-guard';
148
+ import { setEventBus, type EventBus, type AccReceipt } from '@xshieldai/aegis-guard';
143
149
 
144
150
  const myBus: EventBus = {
145
151
  emit: (r: AccReceipt) => console.log(`[ACC] ${r.event_type} verdict=${r.verdict} ${r.summary}`),
@@ -194,12 +200,12 @@ consumers ingest these events without translation.
194
200
  etc.) are a consumer choice — implement the `EventBus` interface and
195
201
  call `setEventBus(yourBus)`.
196
202
 
197
- ### Use with `@rocketlang/aegis-suite`
203
+ ### Use with `@xshieldai/aegis-suite`
198
204
 
199
205
  If you installed the meta-package, you can wire all 6 primitives in one call:
200
206
 
201
207
  ```typescript
202
- import { wireAllToBus } from '@rocketlang/aegis-suite'; // available in suite v0.2.0+
208
+ import { wireAllToBus } from '@xshieldai/aegis-suite'; // available in suite v0.2.0+
203
209
  wireAllToBus(); // default: in-memory bus + SQLite writer to ~/.aegis/acc-events.db
204
210
  ```
205
211
 
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@xshieldai/aegis-guard",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "AEGIS Guard SDK — Five Locks primitives (approval-token, nonce, idempotency, SENSE, quality-evidence) + opt-in Agentic Control Center event bus for AEGIS-governed services",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
7
7
  "author": "Capt. Anil Sharma <capt.anil.sharma@powerpbox.org>",
8
- "homepage": "https://github.com/rocketlang/aegis/tree/main/packages/aegis-guard",
8
+ "homepage": "https://github.com/rocketlang/aegis/tree/master/packages/aegis-guard",
9
9
  "repository": {
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/rocketlang/aegis.git",
@@ -42,7 +42,8 @@
42
42
  ],
43
43
  "scripts": {
44
44
  "test": "bun test tests/aegis-guard.test.ts",
45
- "typecheck": "tsc --noEmit"
45
+ "typecheck": "tsc --noEmit",
46
+ "prepublishOnly": "node ../../scripts/publish-guard.mjs"
46
47
  },
47
48
  "devDependencies": {
48
49
  "typescript": "^5.4.0",
package/src/acc-bus.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Copyright (c) 2026 Capt. Anil Sharma (rocketlang). All rights reserved.
3
3
  // See LICENSE for details.
4
4
  //
5
- // @rocketlang/aegis-guard — opt-in ACC event bus integration (v0.2.0)
5
+ // @xshieldai/aegis-guard — opt-in ACC event bus integration (v0.2.0)
6
6
  // @rule:ACC-003 — Opt-in event bus. emit only when setEventBus() called.
7
7
  // @rule:ACC-004 — Lightweight OSS receipt shape (strict subset of EE PRAMANA).
8
8
  // @rule:ACC-YK-003 — Stateless-primitive contract preserved. No bus = no emit.
@@ -6,6 +6,7 @@ import { createHash } from 'crypto';
6
6
  import { IrrNoApprovalError } from './errors.js';
7
7
  import { type NonceStore, defaultNonceStore } from './nonce.js';
8
8
  import { emitAccReceipt } from './acc-bus.js';
9
+ import { signApprovalJwt, verifyApprovalJwt } from './signing.js';
9
10
 
10
11
  // Token may arrive up to 60s before local clock (NTP tolerance).
11
12
  const CLOCK_SKEW_MS = 60_000;
@@ -28,14 +29,18 @@ export function digestApprovalToken(token: string): string {
28
29
  return createHash('sha256').update(token).digest('hex').slice(0, 24);
29
30
  }
30
31
 
31
- // @rule:AEG-E-016 — test/dev helper: encode a payload as base64url JSON.
32
- // Production tokens are minted by the AEGIS PROOF system (port 4850), not by services.
32
+ // @rule:AEG-E-016 @rule:KGT-002 — mint an AEGIS-signed EdDSA JWT (KGT-T1.1).
33
+ // Signing requires the AEGIS private key (~/.aegis/approval-signing.key) — present on
34
+ // the AEGIS box, auto-provisioned by the :4850 dashboard. Throws where the key is absent:
35
+ // a service that cannot sign cannot mint, by design.
33
36
  export function mintApprovalToken(payload: ApprovalTokenPayload): string {
34
- return Buffer.from(JSON.stringify(payload)).toString('base64url');
37
+ return signApprovalJwt(payload);
35
38
  }
36
39
 
37
40
  // @rule:AEG-E-016 — token must match service_id + capability + operation exactly.
38
- // Token format: base64url(JSON). Production replacement: JWT signed by AEGIS key at port 4850.
41
+ // @rule:KGT-002 — signature verified FIRST, fail-closed: unsigned/tampered/alg:none/
42
+ // wrong-key tokens are rejected before any scope check. Legacy base64url(JSON) tokens
43
+ // (pre-KGT-T1.1, forgeable) are rejected outright.
39
44
  export function verifyApprovalToken(
40
45
  token: string,
41
46
  expectedServiceId: string,
@@ -45,13 +50,11 @@ export function verifyApprovalToken(
45
50
  // @rule:ACC-003 @rule:ACC-004 — emit ACC receipt on success OR failure
46
51
  const scope = `${expectedServiceId}/${expectedCapability}/${expectedOperation}`;
47
52
  try {
48
- let payload: ApprovalTokenPayload;
49
- try {
50
- const decoded = Buffer.from(token, 'base64url').toString('utf8');
51
- payload = JSON.parse(decoded) as ApprovalTokenPayload;
52
- } catch {
53
- throw new IrrNoApprovalError(expectedCapability, 'token could not be decoded');
53
+ const jwt = verifyApprovalJwt(token);
54
+ if (!jwt.ok) {
55
+ throw new IrrNoApprovalError(expectedCapability, `KGT-002: ${jwt.reason}`);
54
56
  }
57
+ const payload = jwt.payload as ApprovalTokenPayload;
55
58
 
56
59
  if (payload.service_id !== expectedServiceId) {
57
60
  throw new IrrNoApprovalError(
package/src/envelope.ts CHANGED
@@ -1,4 +1,4 @@
1
- // @ankr/aegis-guard — Agent Session Envelope helpers (ASE-T020)
1
+ // @xshieldai/aegis-guard — Agent Session Envelope helpers (ASE-T020)
2
2
  //
3
3
  // issueEnvelope — POST /api/v1/aegis/session — proxy-native agent frameworks use this at startup
4
4
  // verifyEnvelope — GET /api/v1/aegis/sessions/:id/audit — check seal integrity + drift
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- // @rocketlang/aegis-guard — AEGIS Guard SDK public API
1
+ // @xshieldai/aegis-guard — AEGIS Guard SDK public API
2
2
  // Five Locks proved in carbonx-backend (batches 62-74). Batch 93 makes them reusable.
3
3
  // v0.2.0 adds opt-in Agentic Control Center (ACC) event bus integration.
4
4
 
@@ -48,6 +48,19 @@ export {
48
48
  verifyScopedApprovalToken,
49
49
  } from './approval-token.js';
50
50
 
51
+ // @rule:KGT-002 — KGT-T1.1 (v0.3.0): approval tokens are AEGIS-signed EdDSA JWTs.
52
+ // ensureSigningKeypair is called by the AEGIS dashboard at boot (minting authority);
53
+ // verifier-only boxes pin the public key via ~/.aegis/approval-signing.pub or
54
+ // AEGIS_APPROVAL_PUBKEY_PEM (served at :4850 /api/v2/enforcement/signing-key).
55
+ export {
56
+ APPROVAL_JWT_ALG,
57
+ type JwtVerifyResult,
58
+ ensureSigningKeypair,
59
+ getPublicKeyPem,
60
+ signApprovalJwt,
61
+ verifyApprovalJwt,
62
+ } from './signing.js';
63
+
51
64
  export {
52
65
  type IssueEnvelopeParams,
53
66
  type EnvelopeIssueResult,
package/src/signing.ts ADDED
@@ -0,0 +1,172 @@
1
+ // @rule:AEG-E-016 — approval tokens are AEGIS-signed; an unsigned approval is a forgery
2
+ // @rule:KGT-002 — fail-closed: no key material → verification DENIES, never falls open
3
+ //
4
+ // KGT-T1.1 — Ed25519 JWT signing for approval tokens.
5
+ // Before this module, mintApprovalToken emitted base64url(JSON): any client could
6
+ // fabricate an "approval". Now AEGIS (:4850, the minting authority) holds the private
7
+ // key; every verifier needs only the public key. Same key-management pattern as
8
+ // kernel/mudrika.ts: persistent per-machine key material under ~/.aegis, mode 600,
9
+ // generated once. Zero-dep — node:crypto ed25519 only.
10
+
11
+ import {
12
+ generateKeyPairSync,
13
+ createPrivateKey,
14
+ createPublicKey,
15
+ sign as edSign,
16
+ verify as edVerify,
17
+ type KeyObject,
18
+ } from 'crypto';
19
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
20
+ import { join } from 'path';
21
+ import { homedir } from 'os';
22
+
23
+ export const APPROVAL_JWT_ALG = 'EdDSA';
24
+
25
+ const KEY_FILE = 'approval-signing.key'; // PKCS8 PEM (private) — mode 600
26
+ const PUB_FILE = 'approval-signing.pub'; // SPKI PEM (public) — mode 644
27
+
28
+ function aegisDir(): string {
29
+ return process.env.AEGIS_DIR ?? join(process.env.HOME ?? homedir(), '.aegis');
30
+ }
31
+
32
+ // Cache keyed by resolved dir so tests (which point AEGIS_DIR at a temp dir) and
33
+ // long-lived services both get one disk read, not one per verify.
34
+ let cache: { dir: string; priv: KeyObject | null; pub: KeyObject | null } | null = null;
35
+
36
+ function loadKeys(): { priv: KeyObject | null; pub: KeyObject | null } {
37
+ const dir = aegisDir();
38
+ if (cache && cache.dir === dir) return cache;
39
+
40
+ let priv: KeyObject | null = null;
41
+ let pub: KeyObject | null = null;
42
+
43
+ const keyPath = join(dir, KEY_FILE);
44
+ if (existsSync(keyPath)) {
45
+ try {
46
+ priv = createPrivateKey(readFileSync(keyPath, 'utf8'));
47
+ pub = createPublicKey(priv);
48
+ } catch { priv = null; pub = null; }
49
+ }
50
+
51
+ // Verifier-only deployments: public key via env PEM or the .pub file.
52
+ if (!pub) {
53
+ const envPem = process.env.AEGIS_APPROVAL_PUBKEY_PEM;
54
+ const pubPath = join(dir, PUB_FILE);
55
+ try {
56
+ if (envPem) pub = createPublicKey(envPem);
57
+ else if (existsSync(pubPath)) pub = createPublicKey(readFileSync(pubPath, 'utf8'));
58
+ } catch { pub = null; }
59
+ }
60
+
61
+ cache = { dir, priv, pub };
62
+ return cache;
63
+ }
64
+
65
+ // Generate the per-machine keypair if absent. Called lazily by the mint path and by
66
+ // the AEGIS dashboard at boot (the authority guarantees the key exists on its box).
67
+ export function ensureSigningKeypair(): { publicKeyPem: string } {
68
+ const dir = aegisDir();
69
+ const keyPath = join(dir, KEY_FILE);
70
+ const pubPath = join(dir, PUB_FILE);
71
+
72
+ if (!existsSync(keyPath)) {
73
+ const { privateKey, publicKey } = generateKeyPairSync('ed25519');
74
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
75
+ writeFileSync(keyPath, privateKey.export({ type: 'pkcs8', format: 'pem' }), { mode: 0o600 });
76
+ writeFileSync(pubPath, publicKey.export({ type: 'spki', format: 'pem' }), { mode: 0o644 });
77
+ cache = null;
78
+ } else if (!existsSync(pubPath)) {
79
+ // key exists but pub was lost — re-derive, never regenerate (verifiers may hold the pub)
80
+ const priv = createPrivateKey(readFileSync(keyPath, 'utf8'));
81
+ writeFileSync(pubPath, createPublicKey(priv).export({ type: 'spki', format: 'pem' }), { mode: 0o644 });
82
+ cache = null;
83
+ }
84
+
85
+ const { pub } = loadKeys();
86
+ return { publicKeyPem: pub!.export({ type: 'spki', format: 'pem' }).toString() };
87
+ }
88
+
89
+ export function getPublicKeyPem(): string | null {
90
+ const { pub } = loadKeys();
91
+ return pub ? pub.export({ type: 'spki', format: 'pem' }).toString() : null;
92
+ }
93
+
94
+ function b64url(buf: Buffer | string): string {
95
+ return Buffer.from(buf).toString('base64url');
96
+ }
97
+
98
+ // Sign a payload as a compact EdDSA JWT: b64url(header).b64url(payload).b64url(sig).
99
+ // Throws if no private key — only the AEGIS box (or a box it provisioned) can mint.
100
+ export function signApprovalJwt(payload: Record<string, unknown>): string {
101
+ ensureSigningKeypair();
102
+ const { priv } = loadKeys();
103
+ if (!priv) {
104
+ throw new Error(
105
+ 'KGT-002: no AEGIS approval signing key — cannot mint (fail-closed). ' +
106
+ `Expected ${join(aegisDir(), KEY_FILE)}; the AEGIS dashboard (:4850) provisions it at boot.`,
107
+ );
108
+ }
109
+ const header = b64url(JSON.stringify({ alg: APPROVAL_JWT_ALG, typ: 'JWT' }));
110
+ const body = b64url(JSON.stringify(payload));
111
+ const sig = edSign(null, Buffer.from(`${header}.${body}`), priv);
112
+ return `${header}.${body}.${b64url(sig)}`;
113
+ }
114
+
115
+ export type JwtVerifyResult =
116
+ | { ok: true; payload: Record<string, unknown> }
117
+ | { ok: false; reason: string };
118
+
119
+ // Verify signature + structure ONLY (scope/expiry stay in approval-token.ts).
120
+ // Fail-closed on every path: malformed, wrong alg (alg:none forgery), bad signature,
121
+ // or no public key available at all.
122
+ export function verifyApprovalJwt(token: string): JwtVerifyResult {
123
+ const parts = token.split('.');
124
+ if (parts.length !== 3) {
125
+ return {
126
+ ok: false,
127
+ reason: parts.length === 1
128
+ ? 'unsigned legacy token rejected — approval tokens are AEGIS-signed JWTs (KGT-T1.1)'
129
+ : 'malformed token: expected 3 JWT segments',
130
+ };
131
+ }
132
+ const [header, body, sig] = parts;
133
+
134
+ let alg: unknown;
135
+ try {
136
+ alg = (JSON.parse(Buffer.from(header, 'base64url').toString('utf8')) as { alg?: unknown }).alg;
137
+ } catch {
138
+ return { ok: false, reason: 'malformed token: header is not base64url JSON' };
139
+ }
140
+ if (alg !== APPROVAL_JWT_ALG) {
141
+ return { ok: false, reason: `alg '${String(alg)}' rejected — only ${APPROVAL_JWT_ALG} approvals are trusted` };
142
+ }
143
+
144
+ const { pub } = loadKeys();
145
+ if (!pub) {
146
+ return {
147
+ ok: false,
148
+ reason: 'KGT-002: no AEGIS public key available — verification fails closed. ' +
149
+ 'Provision ~/.aegis/approval-signing.pub or set AEGIS_APPROVAL_PUBKEY_PEM ' +
150
+ '(GET :4850/api/v2/enforcement/signing-key).',
151
+ };
152
+ }
153
+
154
+ let valid = false;
155
+ try {
156
+ valid = edVerify(null, Buffer.from(`${header}.${body}`), pub, Buffer.from(sig, 'base64url'));
157
+ } catch {
158
+ valid = false;
159
+ }
160
+ if (!valid) return { ok: false, reason: 'signature invalid — token was not minted by AEGIS' };
161
+
162
+ try {
163
+ return { ok: true, payload: JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) };
164
+ } catch {
165
+ return { ok: false, reason: 'malformed token: payload is not base64url JSON' };
166
+ }
167
+ }
168
+
169
+ // test-only: drop the key cache so a test can swap AEGIS_DIR mid-suite
170
+ export function __resetSigningCache(): void {
171
+ cache = null;
172
+ }