@rocketlang/aegis-guard 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 ADDED
@@ -0,0 +1,31 @@
1
+ GNU AFFERO GENERAL PUBLIC LICENSE
2
+ Version 3, 19 November 2007
3
+
4
+ Copyright (C) 2026 ANKR Labs / Capt. Anil Sharma
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Affero General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Affero General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Affero General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
18
+
19
+ ---
20
+
21
+ The full text of the GNU Affero General Public License v3 is available at:
22
+ https://www.gnu.org/licenses/agpl-3.0.txt
23
+
24
+ ADDITIONAL TERMS (permitted under AGPL §7):
25
+
26
+ If you run a modified version of this software as a network service,
27
+ you must make the complete source code of the modified version available
28
+ to all users of that service under the terms of this license.
29
+
30
+ Commercial use, including SaaS deployments and enterprise integrations,
31
+ requires a separate commercial license. Contact: captain@ankr.in
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @rocketlang/aegis-guard
2
+
3
+ AEGIS Guard SDK — reusable approval-token, nonce, idempotency, SENSE, and quality-evidence primitives for AEGIS-governed services.
4
+
5
+ **Carbonx proved the locks. Batch 93 makes the locks reusable.**
6
+
7
+ The Five Locks were proven across 13 batches (62–74) of carbonx-backend. This package extracts them into a service-agnostic SDK so any AEGIS-governed service can adopt them without copy-pasting bespoke logic.
8
+
9
+ ## Five Locks
10
+
11
+ | Lock | Primitive | Rule |
12
+ |---|---|---|
13
+ | LOCK_1 — decision | `verifyApprovalToken` | AEG-E-016 |
14
+ | LOCK_2 — identity | `verifyScopedApprovalToken` | AEG-E-016 |
15
+ | LOCK_3 — observability | `emitAegisSenseEvent` | CA-003, AEG-HG-2B-003/005 |
16
+ | LOCK_4 — rollback | `checkIdempotency` | AEG-HG-2B-006 |
17
+ | LOCK_5 — idempotency | `verifyAndConsumeNonce` | AEG-HG-2B-006 |
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ bun add @rocketlang/aegis-guard
23
+ # or
24
+ npm install @rocketlang/aegis-guard
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### LOCK_1 + LOCK_2 — decision + identity
30
+
31
+ ```typescript
32
+ import { verifyApprovalToken, verifyScopedApprovalToken } from '@rocketlang/aegis-guard';
33
+
34
+ // LOCK_1 — base token verification (service_id + capability + operation)
35
+ const payload = verifyApprovalToken(token, 'my-service', 'settle', 'record_settle');
36
+
37
+ // LOCK_2 — scoped verification (add service-specific field bindings)
38
+ const payload = verifyScopedApprovalToken(
39
+ token, 'my-service', 'settle', 'record_settle',
40
+ { vessel_id: args.vesselId, amount: args.amount },
41
+ );
42
+ ```
43
+
44
+ ### LOCK_3 — observability (SENSE)
45
+
46
+ ```typescript
47
+ import { emitAegisSenseEvent, digestApprovalToken, configureSenseTransport } from '@rocketlang/aegis-guard';
48
+
49
+ // Wire your logger (default: process.stdout JSON)
50
+ configureSenseTransport((event) => logger.info(event, `SENSE:${event.event_type}`));
51
+
52
+ emitAegisSenseEvent({
53
+ event_type: 'allowance.settle',
54
+ service_id: 'my-service',
55
+ capability: 'settle',
56
+ operation: 'record_settle',
57
+ before_snapshot: { status: 'pending' },
58
+ after_snapshot: { status: 'settled' },
59
+ delta: { status: 'pending→settled' },
60
+ emitted_at: new Date().toISOString(),
61
+ irreversible: true,
62
+ correlation_id: req.headers['x-correlation-id'],
63
+ approval_token_ref: digestApprovalToken(token), // 24-hex digest, never raw token
64
+ });
65
+ ```
66
+
67
+ ### LOCK_4 — rollback guard (idempotency check)
68
+
69
+ ```typescript
70
+ import { checkIdempotency, buildIdempotencyFingerprint } from '@rocketlang/aegis-guard';
71
+
72
+ const existing = await db.findByExternalRef(args.externalRef);
73
+ const fp = buildIdempotencyFingerprint({ amount: args.amount, vessel_id: args.vesselId });
74
+ const { isDuplicate, safeNoOp } = checkIdempotency(args.externalRef, existing, fp, existing?.fingerprint);
75
+
76
+ if (isDuplicate && safeNoOp) return existing; // safe no-op
77
+ if (isDuplicate && !safeNoOp) throw new Error('payload mismatch on duplicate externalRef');
78
+ ```
79
+
80
+ ### LOCK_5 — nonce replay prevention
81
+
82
+ ```typescript
83
+ import { verifyAndConsumeNonce } from '@rocketlang/aegis-guard';
84
+
85
+ // Requires nonce in payload; throws IrrNoApprovalError on missing or replayed nonce
86
+ await verifyAndConsumeNonce(payload, redisNonceStore);
87
+ ```
88
+
89
+ ### Quality evidence
90
+
91
+ ```typescript
92
+ import { buildQualityMaskAtPromotion, meetsHgQualityRequirement } from '@rocketlang/aegis-guard';
93
+
94
+ const mask = buildQualityMaskAtPromotion({
95
+ tests_passed: true,
96
+ rollback_tested: true,
97
+ audit_artifact_produced: true,
98
+ });
99
+
100
+ const ready = meetsHgQualityRequirement('HG-2B-financial', mask);
101
+ ```
102
+
103
+ ## NonceStore — production wiring
104
+
105
+ The default `defaultNonceStore` is in-memory (single-process only). Multi-instance deployments must provide a Redis-backed store:
106
+
107
+ ```typescript
108
+ import { type NonceStore } from '@rocketlang/aegis-guard';
109
+
110
+ const redisNonceStore: NonceStore = {
111
+ async consumeNonce(nonce, ttlMs) {
112
+ const key = `aegis:nonce:${nonce}`;
113
+ const result = await redis.set(key, '1', 'NX', 'PX', ttlMs);
114
+ if (result === null) return false; // already consumed
115
+ return true;
116
+ // throws propagate to callers → fail CLOSED (AEG-HG-2B-006)
117
+ },
118
+ };
119
+ ```
120
+
121
+ ## Schema
122
+
123
+ - `quality_mask_at_promotion`: bits 0–11, `aegis-quality-16bit-v1`
124
+ - `quality_drift_score`: bits 12–15, `aegis-quality-16bit-v1`
125
+ - AEG-Q-003 invariant: bits 12–15 must **never** be set in `quality_mask_at_promotion`
126
+
127
+ ## License
128
+
129
+ AGPL-3.0 — Capt. Anil Sharma, powerpbox.org
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@rocketlang/aegis-guard",
3
+ "version": "0.1.0",
4
+ "description": "AEGIS Guard SDK — reusable approval-token, nonce, idempotency, SENSE, and quality-evidence primitives for AEGIS-governed services",
5
+ "license": "AGPL-3.0-only",
6
+ "type": "module",
7
+ "author": "Capt. Anil Sharma <capt.anil.sharma@powerpbox.org>",
8
+ "homepage": "https://github.com/rocketlang/aegis/tree/main/packages/aegis-guard",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/rocketlang/aegis.git",
12
+ "directory": "packages/aegis-guard"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/rocketlang/aegis/issues"
16
+ },
17
+ "keywords": [
18
+ "aegis",
19
+ "aegis-guard",
20
+ "approval-token",
21
+ "nonce",
22
+ "idempotency",
23
+ "sense",
24
+ "quality-mask",
25
+ "hg-2b",
26
+ "five-locks",
27
+ "agentic-ai",
28
+ "ai-governance",
29
+ "ai-agent-safety"
30
+ ],
31
+ "exports": {
32
+ ".": {
33
+ "import": "./src/index.ts",
34
+ "types": "./src/index.ts"
35
+ }
36
+ },
37
+ "main": "./src/index.ts",
38
+ "files": [
39
+ "src/",
40
+ "README.md",
41
+ "LICENSE"
42
+ ],
43
+ "scripts": {
44
+ "test": "bun test tests/aegis-guard.test.ts",
45
+ "typecheck": "tsc --noEmit"
46
+ },
47
+ "devDependencies": {
48
+ "typescript": "^5.4.0",
49
+ "@types/node": "^20.0.0"
50
+ },
51
+ "engines": {
52
+ "bun": ">=1.0.0"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "aegis": {
58
+ "batch": 93,
59
+ "five_locks_proof": "carbonx-backend batches 62-74",
60
+ "doctrine": "Carbonx proved the locks. Batch 93 makes the locks reusable."
61
+ }
62
+ }
@@ -0,0 +1,142 @@
1
+ // @rule:AEG-E-016 — approval tokens are scoped to service_id + capability + operation
2
+ // @rule:AEG-HG-2B-005 — approval token references in SENSE use digest, not raw token material
3
+ // @rule:AEG-HG-2B-006 — nonce protects the approval; idempotency protects the operation (separate locks)
4
+
5
+ import { createHash } from 'crypto';
6
+ import { IrrNoApprovalError } from './errors.js';
7
+ import { type NonceStore, defaultNonceStore } from './nonce.js';
8
+
9
+ // Token may arrive up to 60s before local clock (NTP tolerance).
10
+ const CLOCK_SKEW_MS = 60_000;
11
+
12
+ export interface ApprovalTokenPayload {
13
+ service_id: string;
14
+ capability: string;
15
+ operation: string;
16
+ issued_at: number;
17
+ expires_at: number;
18
+ issued_by?: string;
19
+ nonce?: string;
20
+ status?: 'approved' | 'revoked' | 'denied';
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ // @rule:AEG-HG-2B-005 — SENSE stores proof reference, not proof secret.
25
+ // Returns first 24 hex chars of SHA-256 (96 bits) — sufficient for correlation, not reconstruction.
26
+ export function digestApprovalToken(token: string): string {
27
+ return createHash('sha256').update(token).digest('hex').slice(0, 24);
28
+ }
29
+
30
+ // @rule:AEG-E-016 — test/dev helper: encode a payload as base64url JSON.
31
+ // Production tokens are minted by the AEGIS PROOF system (port 4850), not by services.
32
+ export function mintApprovalToken(payload: ApprovalTokenPayload): string {
33
+ return Buffer.from(JSON.stringify(payload)).toString('base64url');
34
+ }
35
+
36
+ // @rule:AEG-E-016 — token must match service_id + capability + operation exactly.
37
+ // Token format: base64url(JSON). Production replacement: JWT signed by AEGIS key at port 4850.
38
+ export function verifyApprovalToken(
39
+ token: string,
40
+ expectedServiceId: string,
41
+ expectedCapability: string,
42
+ expectedOperation: string,
43
+ ): ApprovalTokenPayload {
44
+ let payload: ApprovalTokenPayload;
45
+
46
+ try {
47
+ const decoded = Buffer.from(token, 'base64url').toString('utf8');
48
+ payload = JSON.parse(decoded) as ApprovalTokenPayload;
49
+ } catch {
50
+ throw new IrrNoApprovalError(expectedCapability, 'token could not be decoded');
51
+ }
52
+
53
+ if (payload.service_id !== expectedServiceId) {
54
+ throw new IrrNoApprovalError(
55
+ expectedCapability,
56
+ `AEG-E-016: token scoped to '${payload.service_id}', not '${expectedServiceId}'`,
57
+ );
58
+ }
59
+
60
+ if (payload.capability !== expectedCapability) {
61
+ throw new IrrNoApprovalError(
62
+ expectedCapability,
63
+ `AEG-E-016: token capability '${payload.capability}' does not match '${expectedCapability}'`,
64
+ );
65
+ }
66
+
67
+ if (payload.operation !== expectedOperation) {
68
+ throw new IrrNoApprovalError(
69
+ expectedCapability,
70
+ `AEG-E-016: token operation '${payload.operation}' does not match '${expectedOperation}'`,
71
+ );
72
+ }
73
+
74
+ if (Date.now() > payload.expires_at) {
75
+ throw new IrrNoApprovalError(expectedCapability, 'AEG-E-016: token expired');
76
+ }
77
+
78
+ if (payload.issued_at !== undefined && payload.issued_at > Date.now() + CLOCK_SKEW_MS) {
79
+ throw new IrrNoApprovalError(
80
+ expectedCapability,
81
+ 'AEG-E-016: token issued_at is in the future (clock skew > 60s or forged timestamp)',
82
+ );
83
+ }
84
+
85
+ return payload;
86
+ }
87
+
88
+ // @rule:AEG-HG-2B-006 — consume nonce before any state mutation; missing nonce = hard reject.
89
+ // Nonce TTL is bounded by token lifetime; store unavailable = fail CLOSED (throws, never open).
90
+ export async function verifyAndConsumeNonce(
91
+ payload: ApprovalTokenPayload,
92
+ store: NonceStore = defaultNonceStore,
93
+ ): Promise<void> {
94
+ if (!payload.nonce) {
95
+ throw new IrrNoApprovalError(
96
+ payload.capability,
97
+ 'AEG-E-016: irreversible operation requires nonce for replay prevention',
98
+ );
99
+ }
100
+ const ttlMs = Math.max(0, payload.expires_at - Date.now());
101
+ const consumed = await store.consumeNonce(payload.nonce, ttlMs);
102
+ if (!consumed) {
103
+ throw new IrrNoApprovalError(
104
+ payload.capability,
105
+ `AEG-E-016: nonce '${payload.nonce}' already consumed — approval replay rejected`,
106
+ );
107
+ }
108
+ }
109
+
110
+ // @rule:AEG-E-016 — HG-2B: verify scope fields declared by the caller service.
111
+ // requiredScope: Record<string, unknown> — caller declares which fields to bind; SDK enforces them.
112
+ // Service-agnostic: the caller owns the field names; the SDK never names domain concepts.
113
+ export function verifyScopedApprovalToken(
114
+ token: string,
115
+ expectedServiceId: string,
116
+ expectedCapability: string,
117
+ expectedOperation: string,
118
+ requiredScope: Record<string, unknown>,
119
+ ): ApprovalTokenPayload {
120
+ const payload = verifyApprovalToken(
121
+ token, expectedServiceId, expectedCapability, expectedOperation,
122
+ );
123
+
124
+ if (payload.status === 'revoked') {
125
+ throw new IrrNoApprovalError(expectedCapability, 'AEG-E-016: token revoked');
126
+ }
127
+ if (payload.status === 'denied') {
128
+ throw new IrrNoApprovalError(expectedCapability, 'AEG-E-016: token denied');
129
+ }
130
+
131
+ for (const [field, contextValue] of Object.entries(requiredScope)) {
132
+ const tokenValue = payload[field];
133
+ if (tokenValue !== contextValue) {
134
+ throw new IrrNoApprovalError(
135
+ expectedCapability,
136
+ `AEG-E-016: token ${field} '${String(tokenValue)}' does not match scope '${String(contextValue)}'`,
137
+ );
138
+ }
139
+ }
140
+
141
+ return payload;
142
+ }
@@ -0,0 +1,119 @@
1
+ // @ankr/aegis-guard — Agent Session Envelope helpers (ASE-T020)
2
+ //
3
+ // issueEnvelope — POST /api/v1/aegis/session — proxy-native agent frameworks use this at startup
4
+ // verifyEnvelope — GET /api/v1/aegis/sessions/:id/audit — check seal integrity + drift
5
+ //
6
+ // @rule:ASE-001 every proxy-native agent session must call issueEnvelope before its first LLM call
7
+ // @rule:ASE-002 sealed_hash is computed by Aegis at issuance and returned in the response
8
+ // @rule:INF-ASE-002 if sealed_hash_verified=false the session must be quarantined immediately
9
+
10
+ export interface IssueEnvelopeParams {
11
+ /** agent_type must be 'proxy-native' for frameworks calling this directly */
12
+ agent_type?: "proxy-native" | "hook-native";
13
+ service_key?: string;
14
+ tenant_id?: string;
15
+ trust_mask?: number;
16
+ perm_mask?: number;
17
+ class_mask?: number;
18
+ /** Capabilities declared at birth — must be a subset of trust_mask. Empty = conservative default. */
19
+ declared_caps?: string[];
20
+ budget_usd?: number;
21
+ parent_session_id?: string;
22
+ /** Override the Aegis dashboard URL (default: AEGIS_URL env or http://localhost:4850) */
23
+ aegis_url?: string;
24
+ }
25
+
26
+ export interface EnvelopeIssueResult {
27
+ session_id: string;
28
+ agent_id: string;
29
+ sealed_hash: string;
30
+ issued_at: string;
31
+ expires_at: string;
32
+ budget_usd: number;
33
+ declared_caps: string[];
34
+ perm_mask: number;
35
+ class_mask: number;
36
+ }
37
+
38
+ export interface EnvelopeVerifyResult {
39
+ session_id: string;
40
+ /** true = sealed_hash matches stored fields. false = tampered — quarantine immediately. @rule:INF-ASE-002 */
41
+ verified: boolean;
42
+ drift_detected: boolean;
43
+ drift_set: string[];
44
+ declared_caps: string[];
45
+ actual_caps_used: string[];
46
+ budget_usd: number;
47
+ budget_used_usd: number;
48
+ }
49
+
50
+ function aegisBase(override?: string): string {
51
+ return override ?? process.env.AEGIS_URL ?? "http://localhost:4850";
52
+ }
53
+
54
+ // @rule:ASE-001 issue a sealed envelope before the first action
55
+ export async function issueEnvelope(params: IssueEnvelopeParams): Promise<EnvelopeIssueResult> {
56
+ const url = `${aegisBase(params.aegis_url)}/api/v1/aegis/session`;
57
+ const body: Record<string, unknown> = {
58
+ agent_type: params.agent_type ?? "proxy-native",
59
+ };
60
+ if (params.service_key) body.service_key = params.service_key;
61
+ if (params.tenant_id) body.tenant_id = params.tenant_id;
62
+ if (params.trust_mask != null) body.trust_mask = params.trust_mask;
63
+ if (params.perm_mask != null) body.perm_mask = params.perm_mask;
64
+ if (params.class_mask != null) body.class_mask = params.class_mask;
65
+ if (params.declared_caps) body.declared_caps = params.declared_caps;
66
+ if (params.budget_usd != null) body.budget_usd = params.budget_usd;
67
+ if (params.parent_session_id) body.parent_session_id = params.parent_session_id;
68
+
69
+ const resp = await fetch(url, {
70
+ method: "POST",
71
+ headers: { "Content-Type": "application/json" },
72
+ body: JSON.stringify(body),
73
+ });
74
+
75
+ if (!resp.ok) {
76
+ const text = await resp.text().catch(() => resp.statusText);
77
+ throw new Error(`issueEnvelope failed (${resp.status}): ${text}`);
78
+ }
79
+
80
+ const data = await resp.json() as Record<string, unknown>;
81
+ return {
82
+ session_id: String(data.session_id ?? ""),
83
+ agent_id: String(data.agent_id ?? data.session_id ?? ""),
84
+ sealed_hash: String(data.sealed_hash ?? ""),
85
+ issued_at: String(data.issued_at ?? ""),
86
+ expires_at: String(data.expires_at ?? ""),
87
+ budget_usd: Number(data.budget_usd ?? 0),
88
+ declared_caps: Array.isArray(data.declared_caps) ? data.declared_caps as string[] : [],
89
+ perm_mask: Number(data.perm_mask ?? data.trust_mask ?? 0),
90
+ class_mask: Number(data.class_mask ?? 0xFFFF),
91
+ };
92
+ }
93
+
94
+ // @rule:INF-ASE-002 caller must quarantine if verified=false
95
+ export async function verifyEnvelope(
96
+ sessionId: string,
97
+ options?: { aegis_url?: string },
98
+ ): Promise<EnvelopeVerifyResult> {
99
+ const url = `${aegisBase(options?.aegis_url)}/api/v1/aegis/sessions/${encodeURIComponent(sessionId)}/audit`;
100
+
101
+ const resp = await fetch(url);
102
+ if (!resp.ok) {
103
+ const text = await resp.text().catch(() => resp.statusText);
104
+ throw new Error(`verifyEnvelope failed (${resp.status}): ${text}`);
105
+ }
106
+
107
+ const data = await resp.json() as Record<string, unknown>;
108
+ const drift_set: string[] = Array.isArray(data.drift_set) ? data.drift_set as string[] : [];
109
+ return {
110
+ session_id: sessionId,
111
+ verified: Boolean(data.sealed_hash_verified ?? false),
112
+ drift_detected: Boolean(data.drift_detected ?? drift_set.length > 0),
113
+ drift_set,
114
+ declared_caps: Array.isArray(data.declared_caps) ? data.declared_caps as string[] : [],
115
+ actual_caps_used: Array.isArray(data.actual_caps_used) ? data.actual_caps_used as string[] : [],
116
+ budget_usd: Number(data.budget_usd ?? 0),
117
+ budget_used_usd: Number(data.budget_used_usd ?? 0),
118
+ };
119
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,25 @@
1
+ // @rule:AEG-E-016 — no AI agent may perform an irreversible action without a human approval token
2
+ // @rule:IRR-NOAPPROVAL — core doctrine: irreversible means human-in-the-loop is mandatory
3
+
4
+ export class IrrNoApprovalError extends Error {
5
+ readonly code = 'IRR-NOAPPROVAL';
6
+ readonly doctrine = 'AEG-E-016';
7
+
8
+ constructor(capability: string, reason?: string) {
9
+ const detail = reason ? ` (${reason})` : '';
10
+ super(
11
+ `IRR-NOAPPROVAL: capability '${capability}' requires a human approval token before execution.` +
12
+ ` No AI agent may perform this irreversible action without one. [AEG-E-016]${detail}`,
13
+ );
14
+ this.name = 'IrrNoApprovalError';
15
+ }
16
+ }
17
+
18
+ export class AegisNonceError extends Error {
19
+ readonly code = 'AEGIS-NONCE-REPLAY';
20
+
21
+ constructor(nonce: string) {
22
+ super(`AEGIS-NONCE-REPLAY: nonce '${nonce}' already consumed — approval replay rejected`);
23
+ this.name = 'AegisNonceError';
24
+ }
25
+ }
@@ -0,0 +1,38 @@
1
+ // @rule:AEG-HG-2B-006 — idempotency protects the operation; nonce protects the approval (separate locks)
2
+ // Pattern: check DB for externalRef before mutating. Matching fingerprint = safe no-op. Mismatch = warn + reject.
3
+
4
+ export interface IdempotencyCheckResult {
5
+ isDuplicate: boolean;
6
+ payloadMismatch: boolean;
7
+ safeNoOp: boolean;
8
+ }
9
+
10
+ // Functional helper: does not touch DB. Caller provides existingRecord and fingerprints.
11
+ // When isDuplicate=true and payloadMismatch=false: safeNoOp=true — return existing record, do not re-execute.
12
+ // When isDuplicate=true and payloadMismatch=true: safeNoOp=false — log warning; reject or escalate.
13
+ export function checkIdempotency(
14
+ _externalRef: string,
15
+ existingRecord: unknown,
16
+ newFingerprint: string,
17
+ existingFingerprint?: string,
18
+ ): IdempotencyCheckResult {
19
+ const isDuplicate = existingRecord !== null && existingRecord !== undefined;
20
+ if (!isDuplicate) {
21
+ return { isDuplicate: false, payloadMismatch: false, safeNoOp: false };
22
+ }
23
+ const payloadMismatch =
24
+ existingFingerprint !== undefined && existingFingerprint !== newFingerprint;
25
+ return {
26
+ isDuplicate: true,
27
+ payloadMismatch,
28
+ safeNoOp: !payloadMismatch,
29
+ };
30
+ }
31
+
32
+ // Build a stable base64 fingerprint from an arbitrary operation payload.
33
+ export function buildIdempotencyFingerprint(payload: Record<string, unknown>): string {
34
+ const sorted = Object.keys(payload)
35
+ .sort()
36
+ .reduce<Record<string, unknown>>((acc, k) => { acc[k] = payload[k]; return acc; }, {});
37
+ return Buffer.from(JSON.stringify(sorted)).toString('base64');
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,46 @@
1
+ // @rocketlang/aegis-guard — AEGIS Guard SDK public API
2
+ // Five Locks proved in carbonx-backend (batches 62-74). Batch 93 makes them reusable.
3
+
4
+ export { IrrNoApprovalError, AegisNonceError } from './errors.js';
5
+
6
+ export { type NonceStore, defaultNonceStore } from './nonce.js';
7
+
8
+ export {
9
+ type IdempotencyCheckResult,
10
+ checkIdempotency,
11
+ buildIdempotencyFingerprint,
12
+ } from './idempotency.js';
13
+
14
+ export {
15
+ type AegisSenseEvent,
16
+ type SenseTransport,
17
+ configureSenseTransport,
18
+ emitAegisSenseEvent,
19
+ } from './sense.js';
20
+
21
+ export {
22
+ type QualityEvidenceInput,
23
+ type QualityDriftInput,
24
+ type HgGroup,
25
+ buildQualityMaskAtPromotion,
26
+ buildQualityDriftScore,
27
+ HG_REQUIRED_MASKS,
28
+ meetsHgQualityRequirement,
29
+ } from './quality.js';
30
+
31
+ export {
32
+ type ApprovalTokenPayload,
33
+ digestApprovalToken,
34
+ mintApprovalToken,
35
+ verifyApprovalToken,
36
+ verifyAndConsumeNonce,
37
+ verifyScopedApprovalToken,
38
+ } from './approval-token.js';
39
+
40
+ export {
41
+ type IssueEnvelopeParams,
42
+ type EnvelopeIssueResult,
43
+ type EnvelopeVerifyResult,
44
+ issueEnvelope,
45
+ verifyEnvelope,
46
+ } from './envelope.js';
package/src/nonce.ts ADDED
@@ -0,0 +1,26 @@
1
+ // @rule:AEG-HG-2B-006 — NonceStore.consumeNonce fail-closed semantics:
2
+ // returns true = first use (allowed)
3
+ // returns false = already consumed (replay — reject)
4
+ // throws = store unavailable (fails CLOSED, never open)
5
+ // Production path: Redis SET NX EX (AEGIS PROOF at port 4850).
6
+ // Default: in-memory (single-process only — multi-instance requires Redis).
7
+
8
+ export interface NonceStore {
9
+ consumeNonce(nonce: string, ttlMs: number): Promise<boolean>;
10
+ }
11
+
12
+ class InMemoryNonceStore implements NonceStore {
13
+ private readonly used = new Map<string, number>(); // nonce → expiry timestamp
14
+
15
+ async consumeNonce(nonce: string, ttlMs: number): Promise<boolean> {
16
+ const now = Date.now();
17
+ for (const [k, exp] of this.used) {
18
+ if (now > exp) this.used.delete(k);
19
+ }
20
+ if (this.used.has(nonce)) return false;
21
+ this.used.set(nonce, now + ttlMs);
22
+ return true;
23
+ }
24
+ }
25
+
26
+ export const defaultNonceStore: NonceStore = new InMemoryNonceStore();
package/src/quality.ts ADDED
@@ -0,0 +1,80 @@
1
+ // @rule:AEG-Q-001 — quality_mask_at_promotion: bits 0-11, point-in-time at promotion, immutable after
2
+ // @rule:AEG-Q-002 — quality_drift_score: bits 12-15, longitudinal, post-promotion only
3
+ // @rule:AEG-Q-003 — bits 12-15 must NEVER be set in quality_mask_at_promotion
4
+
5
+ export interface QualityEvidenceInput {
6
+ typecheck_passed?: boolean; // bit 0 — Q-001
7
+ tests_passed?: boolean; // bit 1 — Q-002
8
+ lint_passed?: boolean; // bit 2 — Q-003
9
+ no_unrelated_diff?: boolean; // bit 3 — Q-004
10
+ migration_verified?: boolean; // bit 4 — Q-005
11
+ rollback_tested?: boolean; // bit 5 — Q-006
12
+ dependency_checked?: boolean; // bit 6 — Q-007
13
+ codex_updated?: boolean; // bit 7 — Q-008
14
+ audit_artifact_produced?: boolean; // bit 8 — Q-009
15
+ scope_confirmed?: boolean; // bit 9 — Q-010
16
+ no_secrets_exposed?: boolean; // bit 10 — Q-011
17
+ human_reviewed?: boolean; // bit 11 — Q-012
18
+ }
19
+
20
+ const PROMOTION_BIT_MAP: Array<[keyof QualityEvidenceInput, number]> = [
21
+ ['typecheck_passed', 0],
22
+ ['tests_passed', 1],
23
+ ['lint_passed', 2],
24
+ ['no_unrelated_diff', 3],
25
+ ['migration_verified', 4],
26
+ ['rollback_tested', 5],
27
+ ['dependency_checked', 6],
28
+ ['codex_updated', 7],
29
+ ['audit_artifact_produced', 8],
30
+ ['scope_confirmed', 9],
31
+ ['no_secrets_exposed', 10],
32
+ ['human_reviewed', 11],
33
+ ];
34
+
35
+ // @rule:AEG-Q-003 — bits 12-15 are never touched by this function (point-in-time only)
36
+ export function buildQualityMaskAtPromotion(evidence: QualityEvidenceInput): number {
37
+ let mask = 0;
38
+ for (const [field, bit] of PROMOTION_BIT_MAP) {
39
+ if (evidence[field] === true) mask |= (1 << bit);
40
+ }
41
+ return mask;
42
+ }
43
+
44
+ export interface QualityDriftInput {
45
+ idempotency_evidenced?: boolean; // bit 12 — Q-013
46
+ observability_evidenced?: boolean; // bit 13 — Q-014
47
+ regression_suite_pass?: boolean; // bit 14 — Q-015
48
+ production_fire_zero?: boolean; // bit 15 — Q-016
49
+ }
50
+
51
+ const DRIFT_BIT_MAP: Array<[keyof QualityDriftInput, number]> = [
52
+ ['idempotency_evidenced', 12],
53
+ ['observability_evidenced', 13],
54
+ ['regression_suite_pass', 14],
55
+ ['production_fire_zero', 15],
56
+ ];
57
+
58
+ // @rule:AEG-Q-002 — drift bits 12-15 only; never OR'd with promotion mask into a single field
59
+ export function buildQualityDriftScore(drift: QualityDriftInput): number {
60
+ let score = 0;
61
+ for (const [field, bit] of DRIFT_BIT_MAP) {
62
+ if (drift[field] === true) score |= (1 << bit);
63
+ }
64
+ return score;
65
+ }
66
+
67
+ // HG group minimum quality mask requirements (promotion bits 0-11 only)
68
+ export const HG_REQUIRED_MASKS = {
69
+ 'HG-1': 0x0302,
70
+ 'HG-2A': 0x0B83,
71
+ 'HG-2B': 0x0FAB,
72
+ 'HG-2B-financial': 0x0FFF,
73
+ } as const;
74
+
75
+ export type HgGroup = keyof typeof HG_REQUIRED_MASKS;
76
+
77
+ export function meetsHgQualityRequirement(hgGroup: HgGroup, qualityMask: number): boolean {
78
+ const required = HG_REQUIRED_MASKS[hgGroup];
79
+ return (qualityMask & required) === required;
80
+ }
package/src/sense.ts ADDED
@@ -0,0 +1,38 @@
1
+ // @rule:CA-003 — SENSE events carry before_snapshot, after_snapshot, and delta
2
+ // @rule:AEG-HG-2B-003 — boundary-crossing irreversible events must emit to event bus
3
+ // @rule:AEG-HG-2B-004 — gate_phase tags event to soak vs live phase
4
+ // @rule:AEG-HG-2B-005 — approval_token_ref must be a digest (digestApprovalToken), never raw token
5
+
6
+ export interface AegisSenseEvent {
7
+ event_type: string;
8
+ service_id: string;
9
+ capability: string;
10
+ operation: string;
11
+ before_snapshot: Record<string, unknown>;
12
+ after_snapshot: Record<string, unknown>;
13
+ delta: Record<string, unknown>;
14
+ emitted_at: string;
15
+ irreversible: boolean;
16
+ correlation_id: string;
17
+ approval_token_ref?: string;
18
+ idempotency_key?: string;
19
+ gate_phase?: string;
20
+ }
21
+
22
+ export type SenseTransport = (event: AegisSenseEvent) => void;
23
+
24
+ function defaultJsonTransport(event: AegisSenseEvent): void {
25
+ process.stdout.write(JSON.stringify({ aegis_sense: true, ...event }) + '\n');
26
+ }
27
+
28
+ let _transport: SenseTransport = defaultJsonTransport;
29
+
30
+ export function configureSenseTransport(transport: SenseTransport): void {
31
+ _transport = transport;
32
+ }
33
+
34
+ // @rule:CA-003 — all three snapshot fields are required by the type; callers must supply them.
35
+ // approval_token_ref, if present, must already be the output of digestApprovalToken (AEG-HG-2B-005).
36
+ export function emitAegisSenseEvent(event: AegisSenseEvent): void {
37
+ _transport(event);
38
+ }