@nextera.one/axis-server-sdk 1.2.1 → 1.3.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/dist/bin/generate-keys.d.mts +2 -0
- package/dist/bin/generate-keys.d.ts +2 -0
- package/dist/bin/generate-keys.js +159 -0
- package/dist/bin/generate-keys.js.map +1 -0
- package/dist/bin/generate-keys.mjs +169 -0
- package/dist/bin/generate-keys.mjs.map +1 -0
- package/dist/core/index.d.mts +2 -25
- package/dist/core/index.d.ts +2 -25
- package/dist/core/index.js +13 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +12 -0
- package/dist/core/index.mjs.map +1 -1
- package/dist/index-B5xzROld.d.mts +122 -0
- package/dist/index-B5xzROld.d.ts +122 -0
- package/dist/index.d.mts +1380 -10
- package/dist/index.d.ts +1380 -10
- package/dist/index.js +2838 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2829 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -2
package/dist/index.mjs
CHANGED
|
@@ -2678,6 +2678,2826 @@ var DiskUploadFileStore = class {
|
|
|
2678
2678
|
return fs.createReadStream(tempPath);
|
|
2679
2679
|
}
|
|
2680
2680
|
};
|
|
2681
|
+
|
|
2682
|
+
// src/core/index.ts
|
|
2683
|
+
var core_exports = {};
|
|
2684
|
+
__export(core_exports, {
|
|
2685
|
+
AXIS_MAGIC: () => AXIS_MAGIC,
|
|
2686
|
+
AXIS_VERSION: () => AXIS_VERSION,
|
|
2687
|
+
AxisError: () => AxisError,
|
|
2688
|
+
AxisFrameZ: () => AxisFrameZ,
|
|
2689
|
+
BodyProfile: () => BodyProfile,
|
|
2690
|
+
ERR_BAD_SIGNATURE: () => ERR_BAD_SIGNATURE,
|
|
2691
|
+
ERR_CONTRACT_VIOLATION: () => ERR_CONTRACT_VIOLATION,
|
|
2692
|
+
ERR_INVALID_PACKET: () => ERR_INVALID_PACKET,
|
|
2693
|
+
ERR_REPLAY_DETECTED: () => ERR_REPLAY_DETECTED,
|
|
2694
|
+
FLAG_BODY_TLV: () => FLAG_BODY_TLV,
|
|
2695
|
+
FLAG_CHAIN_REQ: () => FLAG_CHAIN_REQ,
|
|
2696
|
+
FLAG_HAS_WITNESS: () => FLAG_HAS_WITNESS,
|
|
2697
|
+
MAX_BODY_LEN: () => MAX_BODY_LEN,
|
|
2698
|
+
MAX_FRAME_LEN: () => MAX_FRAME_LEN,
|
|
2699
|
+
MAX_HDR_LEN: () => MAX_HDR_LEN,
|
|
2700
|
+
MAX_SIG_LEN: () => MAX_SIG_LEN,
|
|
2701
|
+
NCERT_ALG: () => NCERT_ALG,
|
|
2702
|
+
NCERT_EXP: () => NCERT_EXP,
|
|
2703
|
+
NCERT_ISSUER_KID: () => NCERT_ISSUER_KID,
|
|
2704
|
+
NCERT_KID: () => NCERT_KID,
|
|
2705
|
+
NCERT_NBF: () => NCERT_NBF,
|
|
2706
|
+
NCERT_NODE_ID: () => NCERT_NODE_ID,
|
|
2707
|
+
NCERT_PAYLOAD: () => NCERT_PAYLOAD,
|
|
2708
|
+
NCERT_PUB: () => NCERT_PUB,
|
|
2709
|
+
NCERT_SCOPE: () => NCERT_SCOPE,
|
|
2710
|
+
NCERT_SIG: () => NCERT_SIG,
|
|
2711
|
+
PROOF_CAPSULE: () => PROOF_CAPSULE,
|
|
2712
|
+
PROOF_JWT: () => PROOF_JWT,
|
|
2713
|
+
PROOF_LOOM: () => PROOF_LOOM,
|
|
2714
|
+
PROOF_MTLS: () => PROOF_MTLS,
|
|
2715
|
+
PROOF_NONE: () => PROOF_NONE,
|
|
2716
|
+
PROOF_WITNESS: () => PROOF_WITNESS,
|
|
2717
|
+
ProofType: () => ProofType,
|
|
2718
|
+
TLV: () => TLV,
|
|
2719
|
+
TLV_ACTOR_ID: () => TLV_ACTOR_ID,
|
|
2720
|
+
TLV_AUD: () => TLV_AUD,
|
|
2721
|
+
TLV_BODY_ARR: () => TLV_BODY_ARR,
|
|
2722
|
+
TLV_BODY_OBJ: () => TLV_BODY_OBJ,
|
|
2723
|
+
TLV_CAPSULE: () => TLV_CAPSULE,
|
|
2724
|
+
TLV_EFFECT: () => TLV_EFFECT,
|
|
2725
|
+
TLV_ERROR_CODE: () => TLV_ERROR_CODE,
|
|
2726
|
+
TLV_ERROR_MSG: () => TLV_ERROR_MSG,
|
|
2727
|
+
TLV_INDEX: () => TLV_INDEX,
|
|
2728
|
+
TLV_INTENT: () => TLV_INTENT,
|
|
2729
|
+
TLV_KID: () => TLV_KID,
|
|
2730
|
+
TLV_LOOM_PRESENCE_ID: () => TLV_LOOM_PRESENCE_ID,
|
|
2731
|
+
TLV_LOOM_THREAD_HASH: () => TLV_LOOM_THREAD_HASH,
|
|
2732
|
+
TLV_LOOM_WRIT: () => TLV_LOOM_WRIT,
|
|
2733
|
+
TLV_NODE: () => TLV_NODE,
|
|
2734
|
+
TLV_NODE_CERT_HASH: () => TLV_NODE_CERT_HASH,
|
|
2735
|
+
TLV_NODE_KID: () => TLV_NODE_KID,
|
|
2736
|
+
TLV_NONCE: () => TLV_NONCE,
|
|
2737
|
+
TLV_OFFSET: () => TLV_OFFSET,
|
|
2738
|
+
TLV_OK: () => TLV_OK,
|
|
2739
|
+
TLV_PID: () => TLV_PID,
|
|
2740
|
+
TLV_PREV_HASH: () => TLV_PREV_HASH,
|
|
2741
|
+
TLV_PROOF_REF: () => TLV_PROOF_REF,
|
|
2742
|
+
TLV_PROOF_TYPE: () => TLV_PROOF_TYPE,
|
|
2743
|
+
TLV_REALM: () => TLV_REALM,
|
|
2744
|
+
TLV_RECEIPT_HASH: () => TLV_RECEIPT_HASH,
|
|
2745
|
+
TLV_RID: () => TLV_RID,
|
|
2746
|
+
TLV_SHA256_CHUNK: () => TLV_SHA256_CHUNK,
|
|
2747
|
+
TLV_TRACE_ID: () => TLV_TRACE_ID,
|
|
2748
|
+
TLV_TS: () => TLV_TS,
|
|
2749
|
+
TLV_UPLOAD_ID: () => TLV_UPLOAD_ID,
|
|
2750
|
+
computeReceiptHash: () => computeReceiptHash,
|
|
2751
|
+
computeSignaturePayload: () => computeSignaturePayload,
|
|
2752
|
+
decodeArray: () => decodeArray,
|
|
2753
|
+
decodeFrame: () => decodeFrame,
|
|
2754
|
+
decodeObject: () => decodeObject,
|
|
2755
|
+
decodeTLVs: () => decodeTLVs,
|
|
2756
|
+
decodeTLVsList: () => decodeTLVsList,
|
|
2757
|
+
decodeVarint: () => decodeVarint,
|
|
2758
|
+
encodeFrame: () => encodeFrame,
|
|
2759
|
+
encodeTLVs: () => encodeTLVs,
|
|
2760
|
+
encodeVarint: () => encodeVarint,
|
|
2761
|
+
generateEd25519KeyPair: () => generateEd25519KeyPair,
|
|
2762
|
+
getSignTarget: () => getSignTarget,
|
|
2763
|
+
sha256: () => sha256,
|
|
2764
|
+
signFrame: () => signFrame,
|
|
2765
|
+
varintLength: () => varintLength,
|
|
2766
|
+
verifyFrameSignature: () => verifyFrameSignature
|
|
2767
|
+
});
|
|
2768
|
+
|
|
2769
|
+
// src/core/axis-error.ts
|
|
2770
|
+
var AxisError = class extends Error {
|
|
2771
|
+
constructor(code, message, httpStatus = 400, details) {
|
|
2772
|
+
super(message);
|
|
2773
|
+
this.code = code;
|
|
2774
|
+
this.httpStatus = httpStatus;
|
|
2775
|
+
this.details = details;
|
|
2776
|
+
this.name = "AxisError";
|
|
2777
|
+
}
|
|
2778
|
+
};
|
|
2779
|
+
|
|
2780
|
+
// src/crypto/index.ts
|
|
2781
|
+
var crypto_exports = {};
|
|
2782
|
+
__export(crypto_exports, {
|
|
2783
|
+
ProofVerificationService: () => ProofVerificationService,
|
|
2784
|
+
b64urlDecode: () => b64urlDecode,
|
|
2785
|
+
b64urlDecodeString: () => b64urlDecodeString,
|
|
2786
|
+
b64urlEncode: () => b64urlEncode,
|
|
2787
|
+
b64urlEncodeString: () => b64urlEncodeString,
|
|
2788
|
+
canonicalJson: () => canonicalJson,
|
|
2789
|
+
canonicalJsonExcluding: () => canonicalJsonExcluding
|
|
2790
|
+
});
|
|
2791
|
+
|
|
2792
|
+
// src/crypto/proof-verification.service.ts
|
|
2793
|
+
import { Injectable as Injectable4, Logger as Logger3 } from "@nestjs/common";
|
|
2794
|
+
import * as crypto3 from "crypto";
|
|
2795
|
+
import * as nacl from "tweetnacl";
|
|
2796
|
+
var ProofVerificationService = class {
|
|
2797
|
+
constructor() {
|
|
2798
|
+
this.logger = new Logger3(ProofVerificationService.name);
|
|
2799
|
+
// Cache of registered device public keys (deviceId -> pubKey)
|
|
2800
|
+
this.deviceKeys = /* @__PURE__ */ new Map();
|
|
2801
|
+
// Cache of trusted mTLS certificate fingerprints
|
|
2802
|
+
this.trustedCerts = /* @__PURE__ */ new Map();
|
|
2803
|
+
}
|
|
2804
|
+
/**
|
|
2805
|
+
* Verifies an authentication proof based on its type.
|
|
2806
|
+
*
|
|
2807
|
+
* **Supported Types:**
|
|
2808
|
+
* - 1 (CAPSULE): Delegated to `verifyCapsuleProof`
|
|
2809
|
+
* - 2 (JWT): Verified by `verifyJWTProof`
|
|
2810
|
+
* - 3 (MTLS_ID): Verified by `verifyMTLSProof`
|
|
2811
|
+
* - 4 (DEVICE_SE): Verified by `verifyDeviceSEProof`
|
|
2812
|
+
*
|
|
2813
|
+
* @param {ProofType} proofType - The numeric AXIS proof type
|
|
2814
|
+
* @param {Uint8Array} proofRef - The binary reference or token for the proof
|
|
2815
|
+
* @param {Object} context - Additional metadata required for specific proof types
|
|
2816
|
+
* @param {Uint8Array} [context.signTarget] - The canonical bytes that were signed (for Ed25519)
|
|
2817
|
+
* @param {Uint8Array} [context.signature] - The signature to verify (for Ed25519)
|
|
2818
|
+
* @param {MTLSContext} [context.mtls] - mTLS certificate data
|
|
2819
|
+
* @param {DeviceSEContext} [context.deviceSE] - Device Secure Element information
|
|
2820
|
+
* @returns {Promise<ProofVerificationResult>} The outcome of the verification
|
|
2821
|
+
*/
|
|
2822
|
+
async verifyProof(proofType, proofRef, context) {
|
|
2823
|
+
switch (proofType) {
|
|
2824
|
+
case 1:
|
|
2825
|
+
return this.verifyCapsuleProof(proofRef);
|
|
2826
|
+
case 2:
|
|
2827
|
+
return this.verifyJWTProof(proofRef);
|
|
2828
|
+
case 3:
|
|
2829
|
+
return this.verifyMTLSProof(context.mtls);
|
|
2830
|
+
case 4:
|
|
2831
|
+
return this.verifyDeviceSEProof(
|
|
2832
|
+
context.signTarget,
|
|
2833
|
+
context.signature,
|
|
2834
|
+
context.deviceSE
|
|
2835
|
+
);
|
|
2836
|
+
default:
|
|
2837
|
+
return { valid: false, error: `Unknown proof type: ${proofType}` };
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
/**
|
|
2841
|
+
* Verify CAPSULE proof (delegated to CapsuleService)
|
|
2842
|
+
*/
|
|
2843
|
+
async verifyCapsuleProof(proofRef) {
|
|
2844
|
+
const capsuleId = new TextDecoder().decode(proofRef);
|
|
2845
|
+
return {
|
|
2846
|
+
valid: true,
|
|
2847
|
+
metadata: { capsuleId, requiresCapsuleValidation: true }
|
|
2848
|
+
};
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Verifies a JSON Web Token (JWT) proof.
|
|
2852
|
+
*
|
|
2853
|
+
* **Validation Logic:**
|
|
2854
|
+
* 1. Decodes the token string.
|
|
2855
|
+
* 2. Checks for valid 3-part JWT structure.
|
|
2856
|
+
* 3. Validates `exp` (expiration) and `nbf` (not before) claims.
|
|
2857
|
+
* 4. Extracts `actor_id` or `sub` as the identity.
|
|
2858
|
+
*
|
|
2859
|
+
* @param {Uint8Array} proofRef - Binary representation of the JWT string
|
|
2860
|
+
* @returns {Promise<ProofVerificationResult>} Result including the actor identifier
|
|
2861
|
+
*/
|
|
2862
|
+
async verifyJWTProof(proofRef) {
|
|
2863
|
+
try {
|
|
2864
|
+
const token = new TextDecoder().decode(proofRef);
|
|
2865
|
+
const parts = token.split(".");
|
|
2866
|
+
if (parts.length !== 3) {
|
|
2867
|
+
return { valid: false, error: "Invalid JWT format" };
|
|
2868
|
+
}
|
|
2869
|
+
const header = JSON.parse(Buffer.from(parts[0], "base64url").toString());
|
|
2870
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
2871
|
+
if (payload.exp && Date.now() / 1e3 > payload.exp) {
|
|
2872
|
+
return { valid: false, error: "JWT expired" };
|
|
2873
|
+
}
|
|
2874
|
+
if (payload.nbf && Date.now() / 1e3 < payload.nbf) {
|
|
2875
|
+
return { valid: false, error: "JWT not yet valid" };
|
|
2876
|
+
}
|
|
2877
|
+
return {
|
|
2878
|
+
valid: true,
|
|
2879
|
+
actorId: payload.sub || payload.actor_id,
|
|
2880
|
+
metadata: { iss: payload.iss, scope: payload.scope }
|
|
2881
|
+
};
|
|
2882
|
+
} catch (e) {
|
|
2883
|
+
const message = e instanceof Error ? e.message : "Unknown error";
|
|
2884
|
+
return { valid: false, error: `JWT parse error: ${message}` };
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* Verify mTLS client certificate proof
|
|
2889
|
+
*/
|
|
2890
|
+
async verifyMTLSProof(mtls) {
|
|
2891
|
+
if (!mtls) {
|
|
2892
|
+
return { valid: false, error: "No mTLS context provided" };
|
|
2893
|
+
}
|
|
2894
|
+
if (!mtls.verified) {
|
|
2895
|
+
return { valid: false, error: "mTLS not verified by TLS terminator" };
|
|
2896
|
+
}
|
|
2897
|
+
if (mtls.clientCertFingerprint) {
|
|
2898
|
+
const trusted = this.trustedCerts.get(mtls.clientCertFingerprint);
|
|
2899
|
+
if (trusted) {
|
|
2900
|
+
return {
|
|
2901
|
+
valid: true,
|
|
2902
|
+
actorId: trusted.actorId,
|
|
2903
|
+
metadata: {
|
|
2904
|
+
fingerprint: mtls.clientCertFingerprint,
|
|
2905
|
+
subject: mtls.clientCertSubject
|
|
2906
|
+
}
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
if (mtls.clientCertSubject) {
|
|
2911
|
+
const cnMatch = mtls.clientCertSubject.match(/CN=([^,]+)/);
|
|
2912
|
+
if (cnMatch) {
|
|
2913
|
+
return {
|
|
2914
|
+
valid: true,
|
|
2915
|
+
actorId: cnMatch[1],
|
|
2916
|
+
metadata: {
|
|
2917
|
+
subject: mtls.clientCertSubject,
|
|
2918
|
+
issuer: mtls.clientCertIssuer
|
|
2919
|
+
}
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
return { valid: false, error: "Could not extract actor from certificate" };
|
|
2924
|
+
}
|
|
2925
|
+
/**
|
|
2926
|
+
* Verify Device Secure Element signature
|
|
2927
|
+
*/
|
|
2928
|
+
async verifyDeviceSEProof(signTarget, signature, deviceSE) {
|
|
2929
|
+
if (!deviceSE || !signTarget || !signature) {
|
|
2930
|
+
return { valid: false, error: "Missing Device SE context" };
|
|
2931
|
+
}
|
|
2932
|
+
let publicKey = deviceSE.publicKey;
|
|
2933
|
+
const registeredKey = this.deviceKeys.get(deviceSE.deviceId);
|
|
2934
|
+
if (registeredKey) {
|
|
2935
|
+
publicKey = registeredKey;
|
|
2936
|
+
}
|
|
2937
|
+
if (!publicKey || publicKey.length !== 32) {
|
|
2938
|
+
return {
|
|
2939
|
+
valid: false,
|
|
2940
|
+
error: "Invalid or unregistered device public key"
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
try {
|
|
2944
|
+
const valid = nacl.sign.detached.verify(signTarget, signature, publicKey);
|
|
2945
|
+
if (!valid) {
|
|
2946
|
+
return { valid: false, error: "Device signature verification failed" };
|
|
2947
|
+
}
|
|
2948
|
+
return {
|
|
2949
|
+
valid: true,
|
|
2950
|
+
actorId: deviceSE.deviceId,
|
|
2951
|
+
metadata: { deviceId: deviceSE.deviceId, proofType: "DEVICE_SE" }
|
|
2952
|
+
};
|
|
2953
|
+
} catch (e) {
|
|
2954
|
+
const message = e instanceof Error ? e.message : "Unknown error";
|
|
2955
|
+
return {
|
|
2956
|
+
valid: false,
|
|
2957
|
+
error: `Signature verification error: ${message}`
|
|
2958
|
+
};
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
/**
|
|
2962
|
+
* Registers a public key for a trusted device.
|
|
2963
|
+
* This key will be used for future `DEVICE_SE` proof verifications.
|
|
2964
|
+
*
|
|
2965
|
+
* @param {string} deviceId - Unique identifier for the device
|
|
2966
|
+
* @param {Uint8Array} publicKey - 32-byte Ed25519 public key
|
|
2967
|
+
* @throws {Error} If the public key is not 32 bytes
|
|
2968
|
+
*/
|
|
2969
|
+
registerDeviceKey(deviceId, publicKey) {
|
|
2970
|
+
if (publicKey.length !== 32) {
|
|
2971
|
+
throw new Error("Device public key must be 32 bytes (Ed25519)");
|
|
2972
|
+
}
|
|
2973
|
+
this.deviceKeys.set(deviceId, publicKey);
|
|
2974
|
+
this.logger.log(`Registered device key for ${deviceId}`);
|
|
2975
|
+
}
|
|
2976
|
+
/**
|
|
2977
|
+
* Unregister a device
|
|
2978
|
+
*/
|
|
2979
|
+
unregisterDevice(deviceId) {
|
|
2980
|
+
return this.deviceKeys.delete(deviceId);
|
|
2981
|
+
}
|
|
2982
|
+
/**
|
|
2983
|
+
* Registers a trusted mTLS certificate fingerprint and associates it with an actor.
|
|
2984
|
+
*
|
|
2985
|
+
* @param {string} fingerprint - SHA-256 fingerprint of the client certificate
|
|
2986
|
+
* @param {string} actorId - The actor to associate with this certificate
|
|
2987
|
+
*/
|
|
2988
|
+
registerMTLSCert(fingerprint, actorId) {
|
|
2989
|
+
this.trustedCerts.set(fingerprint, { actorId, issuedAt: Date.now() });
|
|
2990
|
+
this.logger.log(`Registered mTLS cert ${fingerprint} for actor ${actorId}`);
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* Revoke an mTLS certificate
|
|
2994
|
+
*/
|
|
2995
|
+
revokeMTLSCert(fingerprint) {
|
|
2996
|
+
return this.trustedCerts.delete(fingerprint);
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* Calculate certificate fingerprint (SHA-256)
|
|
3000
|
+
*/
|
|
3001
|
+
static calculateFingerprint(certPem) {
|
|
3002
|
+
const der = Buffer.from(
|
|
3003
|
+
certPem.replace(/-----BEGIN CERTIFICATE-----/, "").replace(/-----END CERTIFICATE-----/, "").replace(/\s/g, ""),
|
|
3004
|
+
"base64"
|
|
3005
|
+
);
|
|
3006
|
+
return crypto3.createHash("sha256").update(der).digest("hex");
|
|
3007
|
+
}
|
|
3008
|
+
};
|
|
3009
|
+
ProofVerificationService = __decorateClass([
|
|
3010
|
+
Injectable4()
|
|
3011
|
+
], ProofVerificationService);
|
|
3012
|
+
|
|
3013
|
+
// src/decorators/index.ts
|
|
3014
|
+
var decorators_exports = {};
|
|
3015
|
+
__export(decorators_exports, {
|
|
3016
|
+
AxisContext: () => AxisContext,
|
|
3017
|
+
AxisDemoPubkey: () => AxisDemoPubkey,
|
|
3018
|
+
AxisFrame: () => AxisFrame3,
|
|
3019
|
+
AxisIp: () => AxisIp,
|
|
3020
|
+
AxisRaw: () => AxisRaw,
|
|
3021
|
+
HANDLER_METADATA_KEY: () => HANDLER_METADATA_KEY,
|
|
3022
|
+
Handler: () => Handler,
|
|
3023
|
+
INTENT_BODY_KEY: () => INTENT_BODY_KEY,
|
|
3024
|
+
INTENT_METADATA_KEY: () => INTENT_METADATA_KEY,
|
|
3025
|
+
INTENT_ROUTES_KEY: () => INTENT_ROUTES_KEY,
|
|
3026
|
+
INTENT_SENSORS_KEY: () => INTENT_SENSORS_KEY,
|
|
3027
|
+
Intent: () => Intent,
|
|
3028
|
+
IntentBody: () => IntentBody,
|
|
3029
|
+
IntentSensors: () => IntentSensors,
|
|
3030
|
+
SENSOR_METADATA_KEY: () => SENSOR_METADATA_KEY,
|
|
3031
|
+
Sensor: () => Sensor,
|
|
3032
|
+
TLV_FIELDS_KEY: () => TLV_FIELDS_KEY,
|
|
3033
|
+
TLV_VALIDATORS_KEY: () => TLV_VALIDATORS_KEY,
|
|
3034
|
+
TlvEnum: () => TlvEnum,
|
|
3035
|
+
TlvField: () => TlvField,
|
|
3036
|
+
TlvMinLen: () => TlvMinLen,
|
|
3037
|
+
TlvRange: () => TlvRange,
|
|
3038
|
+
TlvUtf8Pattern: () => TlvUtf8Pattern,
|
|
3039
|
+
TlvValidate: () => TlvValidate,
|
|
3040
|
+
buildDtoDecoder: () => buildDtoDecoder,
|
|
3041
|
+
extractDtoSchema: () => extractDtoSchema
|
|
3042
|
+
});
|
|
3043
|
+
|
|
3044
|
+
// src/decorators/axis-request.decorator.ts
|
|
3045
|
+
import { createParamDecorator } from "@nestjs/common";
|
|
3046
|
+
function resolveIp(req) {
|
|
3047
|
+
return req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.headers["x-real-ip"] || req.socket.remoteAddress || void 0;
|
|
3048
|
+
}
|
|
3049
|
+
var AxisRaw = createParamDecorator(
|
|
3050
|
+
(_data, ctx) => {
|
|
3051
|
+
const req = ctx.switchToHttp().getRequest();
|
|
3052
|
+
return req.body;
|
|
3053
|
+
}
|
|
3054
|
+
);
|
|
3055
|
+
var AxisIp = createParamDecorator(
|
|
3056
|
+
(_data, ctx) => {
|
|
3057
|
+
const req = ctx.switchToHttp().getRequest();
|
|
3058
|
+
return resolveIp(req);
|
|
3059
|
+
}
|
|
3060
|
+
);
|
|
3061
|
+
var AxisContext = createParamDecorator(
|
|
3062
|
+
(_data, ctx) => {
|
|
3063
|
+
const req = ctx.switchToHttp().getRequest();
|
|
3064
|
+
const axisData = req.axis || {};
|
|
3065
|
+
return {
|
|
3066
|
+
raw: req.body,
|
|
3067
|
+
ip: resolveIp(req),
|
|
3068
|
+
preDecodeInput: axisData.preDecodeInput,
|
|
3069
|
+
frameBytesCount: axisData.frameBytesCount || 0
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3072
|
+
);
|
|
3073
|
+
var AxisDemoPubkey = createParamDecorator(
|
|
3074
|
+
(_data, ctx) => {
|
|
3075
|
+
if (process.env.NODE_ENV !== "development") return void 0;
|
|
3076
|
+
const req = ctx.switchToHttp().getRequest();
|
|
3077
|
+
return req.headers["x-demo-pubkey"];
|
|
3078
|
+
}
|
|
3079
|
+
);
|
|
3080
|
+
var AxisFrame3 = createParamDecorator(
|
|
3081
|
+
(_data, ctx) => {
|
|
3082
|
+
const req = ctx.switchToHttp().getRequest();
|
|
3083
|
+
const decoded = req.axisDecoded;
|
|
3084
|
+
if (!decoded) {
|
|
3085
|
+
throw new Error(
|
|
3086
|
+
"@AxisFrame() requires AxisDecodeInterceptor on the route. Add @UseInterceptors(AxisDecodeInterceptor) to use this decorator."
|
|
3087
|
+
);
|
|
3088
|
+
}
|
|
3089
|
+
return decoded;
|
|
3090
|
+
}
|
|
3091
|
+
);
|
|
3092
|
+
|
|
3093
|
+
// src/decorators/sensor.decorator.ts
|
|
3094
|
+
import { SetMetadata as SetMetadata2 } from "@nestjs/common";
|
|
3095
|
+
var SENSOR_METADATA_KEY = "axis:sensor";
|
|
3096
|
+
function Sensor(options) {
|
|
3097
|
+
return SetMetadata2(SENSOR_METADATA_KEY, options ?? true);
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
// src/engine/index.ts
|
|
3101
|
+
var engine_exports = {};
|
|
3102
|
+
__export(engine_exports, {
|
|
3103
|
+
BAND: () => BAND,
|
|
3104
|
+
HandlerDiscoveryService: () => HandlerDiscoveryService,
|
|
3105
|
+
IntentRouter: () => IntentRouter,
|
|
3106
|
+
PRE_DECODE_BOUNDARY: () => PRE_DECODE_BOUNDARY,
|
|
3107
|
+
SensorDiscoveryService: () => SensorDiscoveryService,
|
|
3108
|
+
SensorRegistry: () => SensorRegistry,
|
|
3109
|
+
createObservation: () => createObservation,
|
|
3110
|
+
endStage: () => endStage,
|
|
3111
|
+
finalizeObservation: () => finalizeObservation,
|
|
3112
|
+
recordSensor: () => recordSensor,
|
|
3113
|
+
startStage: () => startStage
|
|
3114
|
+
});
|
|
3115
|
+
|
|
3116
|
+
// src/engine/axis-observation.ts
|
|
3117
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
3118
|
+
function createObservation(transport, ip) {
|
|
3119
|
+
return {
|
|
3120
|
+
id: randomBytes3(16).toString("hex"),
|
|
3121
|
+
startMs: Date.now(),
|
|
3122
|
+
transport,
|
|
3123
|
+
ip,
|
|
3124
|
+
stages: [],
|
|
3125
|
+
sensors: [],
|
|
3126
|
+
facts: {}
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
function startStage(obs, name) {
|
|
3130
|
+
const stage = { name, status: "ok", startMs: Date.now() };
|
|
3131
|
+
obs.stages.push(stage);
|
|
3132
|
+
return stage;
|
|
3133
|
+
}
|
|
3134
|
+
function endStage(stage, status = "ok", reason, code) {
|
|
3135
|
+
stage.endMs = Date.now();
|
|
3136
|
+
stage.durationMs = stage.endMs - stage.startMs;
|
|
3137
|
+
stage.status = status;
|
|
3138
|
+
if (reason) stage.reason = reason;
|
|
3139
|
+
if (code) stage.code = code;
|
|
3140
|
+
}
|
|
3141
|
+
function recordSensor(obs, name, allowed, riskScore, durationMs, reasons, code) {
|
|
3142
|
+
obs.sensors.push({ name, allowed, riskScore, durationMs, reasons, code });
|
|
3143
|
+
}
|
|
3144
|
+
function finalizeObservation(obs, decision, statusCode, resultCode) {
|
|
3145
|
+
obs.endMs = Date.now();
|
|
3146
|
+
obs.durationMs = obs.endMs - obs.startMs;
|
|
3147
|
+
obs.decision = decision;
|
|
3148
|
+
obs.statusCode = statusCode;
|
|
3149
|
+
if (resultCode) obs.resultCode = resultCode;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
// src/engine/handler-discovery.service.ts
|
|
3153
|
+
import { Injectable as Injectable5, Logger as Logger4 } from "@nestjs/common";
|
|
3154
|
+
var HandlerDiscoveryService = class {
|
|
3155
|
+
constructor(discovery, scanner, router) {
|
|
3156
|
+
this.discovery = discovery;
|
|
3157
|
+
this.scanner = scanner;
|
|
3158
|
+
this.router = router;
|
|
3159
|
+
this.logger = new Logger4(HandlerDiscoveryService.name);
|
|
3160
|
+
}
|
|
3161
|
+
onModuleInit() {
|
|
3162
|
+
const providers = this.discovery.getProviders();
|
|
3163
|
+
let totalIntents = 0;
|
|
3164
|
+
for (const wrapper of providers) {
|
|
3165
|
+
const { instance, metatype } = wrapper;
|
|
3166
|
+
if (!instance || !metatype) continue;
|
|
3167
|
+
const handlerMeta = Reflect.getMetadata(HANDLER_METADATA_KEY, metatype);
|
|
3168
|
+
if (!handlerMeta) continue;
|
|
3169
|
+
const handlerName = handlerMeta.intent || metatype.name;
|
|
3170
|
+
const proto = Object.getPrototypeOf(instance);
|
|
3171
|
+
const methods = this.scanner.getAllMethodNames(proto);
|
|
3172
|
+
let registered = 0;
|
|
3173
|
+
for (const methodName of methods) {
|
|
3174
|
+
const meta = Reflect.getMetadata(
|
|
3175
|
+
INTENT_METADATA_KEY,
|
|
3176
|
+
proto,
|
|
3177
|
+
methodName
|
|
3178
|
+
);
|
|
3179
|
+
if (!meta?.intent) continue;
|
|
3180
|
+
if (!this.router.has(meta.intent)) {
|
|
3181
|
+
this.router.register(
|
|
3182
|
+
meta.intent,
|
|
3183
|
+
instance[methodName].bind(instance)
|
|
3184
|
+
);
|
|
3185
|
+
registered++;
|
|
3186
|
+
totalIntents++;
|
|
3187
|
+
}
|
|
3188
|
+
this.router.registerIntentMeta(meta.intent, proto, methodName);
|
|
3189
|
+
}
|
|
3190
|
+
if (registered > 0) {
|
|
3191
|
+
this.logger.log(
|
|
3192
|
+
`Auto-registered ${registered} intents from ${handlerName}`
|
|
3193
|
+
);
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
this.logger.log(
|
|
3197
|
+
`Handler discovery complete: ${totalIntents} intents auto-registered`
|
|
3198
|
+
);
|
|
3199
|
+
}
|
|
3200
|
+
};
|
|
3201
|
+
HandlerDiscoveryService = __decorateClass([
|
|
3202
|
+
Injectable5()
|
|
3203
|
+
], HandlerDiscoveryService);
|
|
3204
|
+
|
|
3205
|
+
// src/engine/sensor-bands.ts
|
|
3206
|
+
var BAND = {
|
|
3207
|
+
/** Pre-decode: raw byte validation, geo, budget, magic */
|
|
3208
|
+
WIRE: 0,
|
|
3209
|
+
/** Post-decode: identity resolution, capsule, proof */
|
|
3210
|
+
IDENTITY: 40,
|
|
3211
|
+
/** Post-decode: authorization, signature, rate limiting */
|
|
3212
|
+
POLICY: 90,
|
|
3213
|
+
/** Post-decode: content validation, TLV, schema, files */
|
|
3214
|
+
CONTENT: 140,
|
|
3215
|
+
/** Post-decode: business logic sensors, streams, WS */
|
|
3216
|
+
BUSINESS: 200,
|
|
3217
|
+
/** Post-decode: audit, logging (always last) */
|
|
3218
|
+
AUDIT: 900
|
|
3219
|
+
};
|
|
3220
|
+
var PRE_DECODE_BOUNDARY = 40;
|
|
3221
|
+
|
|
3222
|
+
// src/engine/sensor-discovery.service.ts
|
|
3223
|
+
import { Injectable as Injectable6, Logger as Logger5 } from "@nestjs/common";
|
|
3224
|
+
var SensorDiscoveryService = class {
|
|
3225
|
+
constructor(discovery, reflector, registry) {
|
|
3226
|
+
this.discovery = discovery;
|
|
3227
|
+
this.reflector = reflector;
|
|
3228
|
+
this.registry = registry;
|
|
3229
|
+
this.logger = new Logger5(SensorDiscoveryService.name);
|
|
3230
|
+
}
|
|
3231
|
+
onApplicationBootstrap() {
|
|
3232
|
+
const providers = this.discovery.getProviders();
|
|
3233
|
+
let count = 0;
|
|
3234
|
+
for (const wrapper of providers) {
|
|
3235
|
+
const { instance } = wrapper;
|
|
3236
|
+
if (!instance || !instance.constructor) continue;
|
|
3237
|
+
const meta = this.reflector.get(
|
|
3238
|
+
SENSOR_METADATA_KEY,
|
|
3239
|
+
instance.constructor
|
|
3240
|
+
);
|
|
3241
|
+
if (!meta) continue;
|
|
3242
|
+
const sensor = instance;
|
|
3243
|
+
if (!sensor.name || sensor.order === void 0) {
|
|
3244
|
+
this.logger.warn(
|
|
3245
|
+
`@Sensor() on ${instance.constructor.name} missing name or order \u2014 skipped`
|
|
3246
|
+
);
|
|
3247
|
+
continue;
|
|
3248
|
+
}
|
|
3249
|
+
if (!sensor.phase) {
|
|
3250
|
+
const decoratorPhase = meta !== true ? meta.phase : void 0;
|
|
3251
|
+
sensor.phase = decoratorPhase ?? (sensor.order < PRE_DECODE_BOUNDARY ? "PRE_DECODE" : "POST_DECODE");
|
|
3252
|
+
}
|
|
3253
|
+
this.registry.register(sensor);
|
|
3254
|
+
count++;
|
|
3255
|
+
}
|
|
3256
|
+
this.logger.log(`Auto-registered ${count} sensors via @Sensor()`);
|
|
3257
|
+
}
|
|
3258
|
+
};
|
|
3259
|
+
SensorDiscoveryService = __decorateClass([
|
|
3260
|
+
Injectable6()
|
|
3261
|
+
], SensorDiscoveryService);
|
|
3262
|
+
|
|
3263
|
+
// src/engine/registry/sensor.registry.ts
|
|
3264
|
+
import { Injectable as Injectable7, Logger as Logger6 } from "@nestjs/common";
|
|
3265
|
+
var SensorRegistry = class {
|
|
3266
|
+
constructor(configService) {
|
|
3267
|
+
this.configService = configService;
|
|
3268
|
+
this.sensors = [];
|
|
3269
|
+
this.logger = new Logger6(SensorRegistry.name);
|
|
3270
|
+
}
|
|
3271
|
+
/**
|
|
3272
|
+
* Registers a new sensor in the registry.
|
|
3273
|
+
*
|
|
3274
|
+
* Validates that:
|
|
3275
|
+
* - AxisSensor has a unique name
|
|
3276
|
+
* - AxisSensor has an order field
|
|
3277
|
+
* - Pre-decode sensors have order < 40
|
|
3278
|
+
* - Post-decode sensors have order >= 40
|
|
3279
|
+
*
|
|
3280
|
+
* @param {AxisSensor} sensor - The sensor instance to register
|
|
3281
|
+
* @throws Error if validation fails
|
|
3282
|
+
*/
|
|
3283
|
+
register(sensor) {
|
|
3284
|
+
if (!sensor.name) {
|
|
3285
|
+
throw new Error("AxisSensor must have a name");
|
|
3286
|
+
}
|
|
3287
|
+
const enabledSensorsStr = this.configService.get("ENABLED_SENSORS");
|
|
3288
|
+
const disabledSensorsStr = this.configService.get("DISABLED_SENSORS");
|
|
3289
|
+
const enabledSensors = enabledSensorsStr ? enabledSensorsStr.split(",").map((s) => s.trim()) : null;
|
|
3290
|
+
const disabledSensors = disabledSensorsStr ? disabledSensorsStr.split(",").map((s) => s.trim()) : [];
|
|
3291
|
+
if (enabledSensors && !enabledSensors.includes(sensor.name)) {
|
|
3292
|
+
this.logger.log(`Skipping disabled sensor (not in ENABLED_SENSORS): ${sensor.name}`);
|
|
3293
|
+
return;
|
|
3294
|
+
}
|
|
3295
|
+
if (disabledSensors.includes(sensor.name)) {
|
|
3296
|
+
this.logger.log(`Skipping disabled sensor (in DISABLED_SENSORS): ${sensor.name}`);
|
|
3297
|
+
return;
|
|
3298
|
+
}
|
|
3299
|
+
if (sensor.order === void 0) {
|
|
3300
|
+
throw new Error(`AxisSensor "${sensor.name}" must have an order field`);
|
|
3301
|
+
}
|
|
3302
|
+
const isPreDecodeSensor = this.isPreDecodeSensor(sensor);
|
|
3303
|
+
const isPostDecodeSensor = this.isPostDecodeSensor(sensor);
|
|
3304
|
+
if (isPreDecodeSensor && sensor.order >= 40) {
|
|
3305
|
+
this.logger.warn(
|
|
3306
|
+
`AxisSensor "${sensor.name}" is marked as PRE_DECODE but has order ${sensor.order} (should be < 40)`
|
|
3307
|
+
);
|
|
3308
|
+
}
|
|
3309
|
+
if (isPostDecodeSensor && sensor.order < 40) {
|
|
3310
|
+
this.logger.warn(
|
|
3311
|
+
`AxisSensor "${sensor.name}" is marked as POST_DECODE but has order ${sensor.order} (should be >= 40)`
|
|
3312
|
+
);
|
|
3313
|
+
}
|
|
3314
|
+
this.sensors.push(sensor);
|
|
3315
|
+
const phaseLabel = typeof sensor.phase === "string" ? sensor.phase : sensor.phase?.phase || "UNKNOWN";
|
|
3316
|
+
this.logger.debug(
|
|
3317
|
+
`Registered sensor: ${sensor.name} (order: ${sensor.order}, phase: ${phaseLabel})`
|
|
3318
|
+
);
|
|
3319
|
+
}
|
|
3320
|
+
/**
|
|
3321
|
+
* Returns all registered sensors, sorted by their execution order.
|
|
3322
|
+
*
|
|
3323
|
+
* @returns {AxisSensor[]} A sorted array of sensors
|
|
3324
|
+
*/
|
|
3325
|
+
list() {
|
|
3326
|
+
return [...this.sensors].sort(
|
|
3327
|
+
(a, b) => (a.order ?? 999) - (b.order ?? 999)
|
|
3328
|
+
);
|
|
3329
|
+
}
|
|
3330
|
+
/**
|
|
3331
|
+
* Returns only pre-decode sensors (order < 40).
|
|
3332
|
+
* These sensors run in middleware on raw bytes before frame decoding.
|
|
3333
|
+
*
|
|
3334
|
+
* @returns {AxisPreSensor[]} Pre-decode sensors sorted by order
|
|
3335
|
+
*/
|
|
3336
|
+
getPreDecodeSensors() {
|
|
3337
|
+
return this.list().filter((s) => (s.order ?? 999) < 40);
|
|
3338
|
+
}
|
|
3339
|
+
/**
|
|
3340
|
+
* Returns only post-decode sensors (order >= 40).
|
|
3341
|
+
* These sensors run in the controller on fully decoded frames.
|
|
3342
|
+
*
|
|
3343
|
+
* @returns {AxisPostSensor[]} Post-decode sensors sorted by order
|
|
3344
|
+
*/
|
|
3345
|
+
getPostDecodeSensors() {
|
|
3346
|
+
return this.list().filter(
|
|
3347
|
+
(s) => (s.order ?? 999) >= 40
|
|
3348
|
+
);
|
|
3349
|
+
}
|
|
3350
|
+
/**
|
|
3351
|
+
* Helper: Check if a sensor is a pre-decode sensor.
|
|
3352
|
+
*
|
|
3353
|
+
* @private
|
|
3354
|
+
* @param {AxisSensor} sensor - The sensor to check
|
|
3355
|
+
* @returns {boolean} True if sensor is pre-decode
|
|
3356
|
+
*/
|
|
3357
|
+
isPreDecodeSensor(sensor) {
|
|
3358
|
+
const phase = typeof sensor.phase === "string" ? sensor.phase : sensor.phase?.phase;
|
|
3359
|
+
return phase === "PRE_DECODE" || (sensor.order ?? 999) < 40;
|
|
3360
|
+
}
|
|
3361
|
+
/**
|
|
3362
|
+
* Helper: Check if a sensor is a post-decode sensor.
|
|
3363
|
+
*
|
|
3364
|
+
* @private
|
|
3365
|
+
* @param {AxisSensor} sensor - The sensor to check
|
|
3366
|
+
* @returns {boolean} True if sensor is post-decode
|
|
3367
|
+
*/
|
|
3368
|
+
isPostDecodeSensor(sensor) {
|
|
3369
|
+
const phase = typeof sensor.phase === "string" ? sensor.phase : sensor.phase?.phase;
|
|
3370
|
+
return phase === "POST_DECODE" || (sensor.order ?? 999) >= 40;
|
|
3371
|
+
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Returns sensor count by phase.
|
|
3374
|
+
* Useful for diagnostics and monitoring.
|
|
3375
|
+
*
|
|
3376
|
+
* @returns {{preDecodeCount: number, postDecodeCount: number}}
|
|
3377
|
+
*/
|
|
3378
|
+
getSensorCountByPhase() {
|
|
3379
|
+
return {
|
|
3380
|
+
preDecodeCount: this.getPreDecodeSensors().length,
|
|
3381
|
+
postDecodeCount: this.getPostDecodeSensors().length
|
|
3382
|
+
};
|
|
3383
|
+
}
|
|
3384
|
+
/**
|
|
3385
|
+
* Clears all registered sensors.
|
|
3386
|
+
* Useful for testing.
|
|
3387
|
+
*
|
|
3388
|
+
* @internal
|
|
3389
|
+
*/
|
|
3390
|
+
clear() {
|
|
3391
|
+
this.sensors = [];
|
|
3392
|
+
}
|
|
3393
|
+
};
|
|
3394
|
+
SensorRegistry = __decorateClass([
|
|
3395
|
+
Injectable7()
|
|
3396
|
+
], SensorRegistry);
|
|
3397
|
+
|
|
3398
|
+
// src/loom/index.ts
|
|
3399
|
+
var loom_exports = {};
|
|
3400
|
+
__export(loom_exports, {
|
|
3401
|
+
PROOF_LOOM: () => PROOF_LOOM,
|
|
3402
|
+
TLV_PRESENCE_ID: () => TLV_LOOM_PRESENCE_ID,
|
|
3403
|
+
TLV_THREAD_HASH: () => TLV_LOOM_THREAD_HASH,
|
|
3404
|
+
TLV_WRIT: () => TLV_LOOM_WRIT,
|
|
3405
|
+
canonicalizeGrant: () => canonicalizeGrant,
|
|
3406
|
+
canonicalizeWrit: () => canonicalizeWrit,
|
|
3407
|
+
deriveAnchorReflection: () => deriveAnchorReflection
|
|
3408
|
+
});
|
|
3409
|
+
|
|
3410
|
+
// src/loom/loom.types.ts
|
|
3411
|
+
function deriveAnchorReflection(softid, context = "openlogs", scope = "loom") {
|
|
3412
|
+
return `ar:${context}:${scope}:${softid}`;
|
|
3413
|
+
}
|
|
3414
|
+
function canonicalizeWrit(writ) {
|
|
3415
|
+
const ordered = {
|
|
3416
|
+
head: { tid: writ.head.tid, seq: writ.head.seq },
|
|
3417
|
+
body: {
|
|
3418
|
+
who: writ.body.who,
|
|
3419
|
+
act: writ.body.act,
|
|
3420
|
+
res: writ.body.res,
|
|
3421
|
+
law: writ.body.law
|
|
3422
|
+
},
|
|
3423
|
+
meta: { iat: writ.meta.iat, exp: writ.meta.exp, prev: writ.meta.prev }
|
|
3424
|
+
};
|
|
3425
|
+
return JSON.stringify(ordered);
|
|
3426
|
+
}
|
|
3427
|
+
function canonicalizeGrant(grant) {
|
|
3428
|
+
const ordered = {
|
|
3429
|
+
grant_id: grant.grant_id,
|
|
3430
|
+
issuer: grant.issuer,
|
|
3431
|
+
subject: grant.subject,
|
|
3432
|
+
grant_type: grant.grant_type,
|
|
3433
|
+
caps: grant.caps,
|
|
3434
|
+
meta: grant.meta
|
|
3435
|
+
};
|
|
3436
|
+
return JSON.stringify(ordered);
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
// src/schemas/index.ts
|
|
3440
|
+
var schemas_exports = {};
|
|
3441
|
+
__export(schemas_exports, {
|
|
3442
|
+
AccessProfileZ: () => AccessProfileZ,
|
|
3443
|
+
AxisContextZ: () => AxisContextZ,
|
|
3444
|
+
AxisErrorZ: () => AxisErrorZ,
|
|
3445
|
+
BodyBudgetInputZ: () => BodyBudgetInputZ,
|
|
3446
|
+
BodyBudgetPolicyZ: () => BodyBudgetPolicyZ,
|
|
3447
|
+
BodyProfile: () => BodyProfile2,
|
|
3448
|
+
BodyProfileValidator: () => BodyProfileValidator,
|
|
3449
|
+
BodyProfileZ: () => BodyProfileZ,
|
|
3450
|
+
CapsuleClaimsZ: () => CapsuleClaimsZ,
|
|
3451
|
+
CapsuleValidationResultZ: () => CapsuleValidationResultZ,
|
|
3452
|
+
CapsuleVerifyResultZ: () => CapsuleVerifyResultZ,
|
|
3453
|
+
CapsuleVerifySensorInputZ: () => CapsuleVerifySensorInputZ,
|
|
3454
|
+
CapsuleZ: () => CapsuleZ,
|
|
3455
|
+
ChunkHashInputZ: () => ChunkHashInputZ,
|
|
3456
|
+
CountryBlockDecisionZ: () => CountryBlockDecisionZ,
|
|
3457
|
+
CountryBlockSensorInputZ: () => CountryBlockSensorInputZ,
|
|
3458
|
+
EntropySensorInputZ: () => EntropySensorInputZ,
|
|
3459
|
+
ExecutionMetricsZ: () => ExecutionMetricsZ,
|
|
3460
|
+
IPReputationInputZ: () => IPReputationInputZ,
|
|
3461
|
+
IPReputationZ: () => IPReputationZ,
|
|
3462
|
+
IntentPolicyDecisionZ: () => IntentPolicyDecisionZ,
|
|
3463
|
+
IntentPolicySensorInputZ: () => IntentPolicySensorInputZ,
|
|
3464
|
+
IntentPolicyZ: () => IntentPolicyZ,
|
|
3465
|
+
IntentSchemaZ: () => IntentSchemaZ,
|
|
3466
|
+
PassportZ: () => PassportZ,
|
|
3467
|
+
ProofKindZ: () => ProofKindZ,
|
|
3468
|
+
ProofPresenceInputZ: () => ProofPresenceInputZ,
|
|
3469
|
+
ProofType: () => ProofType2,
|
|
3470
|
+
ProtocolStrictInputZ: () => ProtocolStrictInputZ,
|
|
3471
|
+
RateLimitConfigZ: () => RateLimitConfigZ,
|
|
3472
|
+
RateLimitInputZ: () => RateLimitInputZ,
|
|
3473
|
+
RateLimitProfileZ: () => RateLimitProfileZ,
|
|
3474
|
+
ScanBurstDecisionZ: () => ScanBurstDecisionZ,
|
|
3475
|
+
ScanBurstSensorInputZ: () => ScanBurstSensorInputZ,
|
|
3476
|
+
SchemaFieldKindZ: () => SchemaFieldKindZ,
|
|
3477
|
+
SchemaFieldZ: () => SchemaFieldZ,
|
|
3478
|
+
ScopeZ: () => ScopeZ,
|
|
3479
|
+
SensitivityLevelZ: () => SensitivityLevelZ,
|
|
3480
|
+
SensorChainInputZ: () => SensorChainInputZ,
|
|
3481
|
+
SensorDecisionWithMetadataZ: () => SensorDecisionWithMetadataZ,
|
|
3482
|
+
SensorDecisionZ: () => SensorDecisionZ,
|
|
3483
|
+
SensorResultZ: () => SensorResultZ,
|
|
3484
|
+
UploadSessionZ: () => UploadSessionZ,
|
|
3485
|
+
UploadStatusZ: () => UploadStatusZ,
|
|
3486
|
+
WsHandshakeDecisionZ: () => WsHandshakeDecisionZ,
|
|
3487
|
+
WsHandshakeInputZ: () => WsHandshakeInputZ
|
|
3488
|
+
});
|
|
3489
|
+
|
|
3490
|
+
// src/schemas/axis-schemas.ts
|
|
3491
|
+
import * as z2 from "zod";
|
|
3492
|
+
var SensorDecisionZ = z2.union([
|
|
3493
|
+
z2.object({ action: z2.literal("ALLOW"), meta: z2.any().optional() }),
|
|
3494
|
+
z2.object({
|
|
3495
|
+
action: z2.literal("DENY"),
|
|
3496
|
+
code: z2.string(),
|
|
3497
|
+
reason: z2.string().optional(),
|
|
3498
|
+
meta: z2.any().optional()
|
|
3499
|
+
})
|
|
3500
|
+
]);
|
|
3501
|
+
var SensorDecisionWithMetadataZ = z2.union([
|
|
3502
|
+
z2.object({ action: z2.literal("ALLOW"), meta: z2.any().optional() }),
|
|
3503
|
+
z2.object({
|
|
3504
|
+
action: z2.literal("DENY"),
|
|
3505
|
+
code: z2.string(),
|
|
3506
|
+
reason: z2.string().optional(),
|
|
3507
|
+
retryAfterMs: z2.number().int().positive().optional(),
|
|
3508
|
+
meta: z2.any().optional()
|
|
3509
|
+
})
|
|
3510
|
+
]);
|
|
3511
|
+
var CountryBlockSensorInputZ = z2.object({
|
|
3512
|
+
ip: z2.string().min(1),
|
|
3513
|
+
country: z2.string().length(2).toUpperCase().optional()
|
|
3514
|
+
});
|
|
3515
|
+
var CountryBlockDecisionZ = SensorDecisionZ;
|
|
3516
|
+
var ScanBurstSensorInputZ = z2.object({
|
|
3517
|
+
ip: z2.string().min(1),
|
|
3518
|
+
isFailure: z2.boolean().optional()
|
|
3519
|
+
});
|
|
3520
|
+
var ScanBurstDecisionZ = SensorDecisionWithMetadataZ;
|
|
3521
|
+
var ProofKindZ = z2.enum([
|
|
3522
|
+
"NONE",
|
|
3523
|
+
"CAPSULE",
|
|
3524
|
+
"PASSPORT",
|
|
3525
|
+
"MTLS",
|
|
3526
|
+
"JWT"
|
|
3527
|
+
]);
|
|
3528
|
+
var AccessProfileZ = z2.enum(["PUBLIC", "PARTNER", "INTERNAL", "NODE"]);
|
|
3529
|
+
var ProofPresenceInputZ = z2.object({
|
|
3530
|
+
profile: AccessProfileZ,
|
|
3531
|
+
visibility: z2.enum(["PUBLIC", "GUARDED"]),
|
|
3532
|
+
requiredProof: z2.array(ProofKindZ).min(1),
|
|
3533
|
+
hasCapsule: z2.boolean(),
|
|
3534
|
+
hasPassportSignature: z2.boolean(),
|
|
3535
|
+
intent: z2.string().min(1)
|
|
3536
|
+
});
|
|
3537
|
+
var SensitivityLevelZ = z2.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]);
|
|
3538
|
+
var IntentPolicyZ = z2.object({
|
|
3539
|
+
intent: z2.string().min(1),
|
|
3540
|
+
sensitivity: SensitivityLevelZ,
|
|
3541
|
+
maxFrameBytes: z2.number().int().positive(),
|
|
3542
|
+
maxHeaderBytes: z2.number().int().positive(),
|
|
3543
|
+
maxBodyBytes: z2.number().int().positive(),
|
|
3544
|
+
maxSigBytes: z2.number().int().positive().optional(),
|
|
3545
|
+
rateLimitPerMinute: z2.number().int().positive().optional(),
|
|
3546
|
+
rateLimitPerHour: z2.number().int().positive().optional(),
|
|
3547
|
+
requiresSignature: z2.boolean(),
|
|
3548
|
+
requiresCapsule: z2.boolean(),
|
|
3549
|
+
timeoutMs: z2.number().int().positive()
|
|
3550
|
+
});
|
|
3551
|
+
var IntentPolicySensorInputZ = z2.object({
|
|
3552
|
+
frame: AxisFrameZ,
|
|
3553
|
+
intent: z2.string().min(1),
|
|
3554
|
+
rawFrameSize: z2.number().int().positive()
|
|
3555
|
+
});
|
|
3556
|
+
var IntentPolicyDecisionZ = z2.union([
|
|
3557
|
+
z2.object({
|
|
3558
|
+
action: z2.literal("ALLOW"),
|
|
3559
|
+
policy: IntentPolicyZ
|
|
3560
|
+
}),
|
|
3561
|
+
z2.object({
|
|
3562
|
+
action: z2.literal("DENY"),
|
|
3563
|
+
reason: z2.string()
|
|
3564
|
+
})
|
|
3565
|
+
]);
|
|
3566
|
+
var CapsuleClaimsZ = z2.object({
|
|
3567
|
+
capsuleId: z2.string().min(8),
|
|
3568
|
+
allowIntents: z2.array(z2.string()).min(1),
|
|
3569
|
+
limits: z2.object({
|
|
3570
|
+
maxBodyBytes: z2.number().int().positive().optional()
|
|
3571
|
+
}).optional(),
|
|
3572
|
+
scopes: z2.record(z2.string(), z2.any()).optional()
|
|
3573
|
+
});
|
|
3574
|
+
var CapsuleZ = z2.object({
|
|
3575
|
+
id: z2.string(),
|
|
3576
|
+
claims: CapsuleClaimsZ,
|
|
3577
|
+
issuedAt: z2.number().int(),
|
|
3578
|
+
expiresAt: z2.number().int(),
|
|
3579
|
+
tier: z2.enum(["FREE", "STANDARD", "PREMIUM"])
|
|
3580
|
+
});
|
|
3581
|
+
var CapsuleValidationResultZ = z2.object({
|
|
3582
|
+
valid: z2.boolean(),
|
|
3583
|
+
capsule: CapsuleZ.optional(),
|
|
3584
|
+
reason: z2.string().optional(),
|
|
3585
|
+
requiresStepUp: z2.boolean().optional()
|
|
3586
|
+
});
|
|
3587
|
+
var CapsuleVerifySensorInputZ = z2.object({
|
|
3588
|
+
headers: z2.map(
|
|
3589
|
+
z2.number(),
|
|
3590
|
+
z2.custom((v) => v instanceof Uint8Array)
|
|
3591
|
+
),
|
|
3592
|
+
intent: z2.string().min(1),
|
|
3593
|
+
ctx: z2.any()
|
|
3594
|
+
// AxisContext - avoid circular dependency
|
|
3595
|
+
});
|
|
3596
|
+
var CapsuleVerifyResultZ = z2.object({
|
|
3597
|
+
ok: z2.literal(true),
|
|
3598
|
+
capsule: CapsuleZ
|
|
3599
|
+
});
|
|
3600
|
+
var RateLimitProfileZ = z2.enum([
|
|
3601
|
+
"PUBLIC",
|
|
3602
|
+
"PARTNER",
|
|
3603
|
+
"INTERNAL",
|
|
3604
|
+
"NODE"
|
|
3605
|
+
]);
|
|
3606
|
+
var RateLimitInputZ = z2.object({
|
|
3607
|
+
ip: z2.string().min(1),
|
|
3608
|
+
userAgent: z2.string().optional(),
|
|
3609
|
+
actorId: z2.string().optional(),
|
|
3610
|
+
capsuleId: z2.string().optional(),
|
|
3611
|
+
intent: z2.string().min(1),
|
|
3612
|
+
profile: RateLimitProfileZ
|
|
3613
|
+
});
|
|
3614
|
+
var RateLimitConfigZ = z2.object({
|
|
3615
|
+
windowSec: z2.number().int().positive(),
|
|
3616
|
+
max: z2.number().int().positive(),
|
|
3617
|
+
key: z2.enum(["ip_fingerprint", "actor_capsule"])
|
|
3618
|
+
});
|
|
3619
|
+
var SensorResultZ = z2.object({
|
|
3620
|
+
ok: z2.literal(true)
|
|
3621
|
+
});
|
|
3622
|
+
var PassportZ = z2.object({
|
|
3623
|
+
id: z2.string(),
|
|
3624
|
+
public_key: z2.custom((v) => Buffer.isBuffer(v)),
|
|
3625
|
+
status: z2.enum(["ACTIVE", "REVOKED", "EXPIRED", "PENDING"]),
|
|
3626
|
+
issuedAt: z2.number().int(),
|
|
3627
|
+
expiresAt: z2.number().int().optional()
|
|
3628
|
+
});
|
|
3629
|
+
var ExecutionMetricsZ = z2.object({
|
|
3630
|
+
dbWrites: z2.number().int(),
|
|
3631
|
+
dbReads: z2.number().int(),
|
|
3632
|
+
externalCalls: z2.number().int(),
|
|
3633
|
+
elapsedMs: z2.number().int().optional()
|
|
3634
|
+
});
|
|
3635
|
+
var SensorChainInputZ = z2.object({
|
|
3636
|
+
ip: z2.string().min(1),
|
|
3637
|
+
path: z2.string().min(1),
|
|
3638
|
+
contentLength: z2.number().int().nonnegative(),
|
|
3639
|
+
peek: z2.instanceof(Uint8Array),
|
|
3640
|
+
country: z2.string().optional()
|
|
3641
|
+
});
|
|
3642
|
+
var EntropySensorInputZ = z2.object({
|
|
3643
|
+
pid: z2.custom((v) => Buffer.isBuffer(v)).optional(),
|
|
3644
|
+
nonce: z2.custom((v) => Buffer.isBuffer(v)).optional(),
|
|
3645
|
+
ip: z2.string().min(1)
|
|
3646
|
+
});
|
|
3647
|
+
var ProtocolStrictInputZ = z2.object({
|
|
3648
|
+
rawBytes: z2.union([z2.custom((v) => Buffer.isBuffer(v)), z2.instanceof(Uint8Array)]).optional(),
|
|
3649
|
+
ip: z2.string().min(1),
|
|
3650
|
+
path: z2.string().min(1),
|
|
3651
|
+
contentLength: z2.number().int().nonnegative(),
|
|
3652
|
+
peek: z2.instanceof(Uint8Array),
|
|
3653
|
+
country: z2.string().optional(),
|
|
3654
|
+
contentType: z2.string().optional()
|
|
3655
|
+
});
|
|
3656
|
+
var SchemaFieldKindZ = z2.enum([
|
|
3657
|
+
"utf8",
|
|
3658
|
+
"u64",
|
|
3659
|
+
"bytes",
|
|
3660
|
+
"bytes16",
|
|
3661
|
+
"bool",
|
|
3662
|
+
"obj",
|
|
3663
|
+
"arr"
|
|
3664
|
+
]);
|
|
3665
|
+
var ScopeZ = z2.enum(["header", "body"]);
|
|
3666
|
+
var SchemaFieldZ = z2.object({
|
|
3667
|
+
name: z2.string().min(1),
|
|
3668
|
+
tlv: z2.number().int().positive(),
|
|
3669
|
+
kind: SchemaFieldKindZ,
|
|
3670
|
+
required: z2.boolean().optional(),
|
|
3671
|
+
maxLen: z2.number().int().positive().optional(),
|
|
3672
|
+
max: z2.string().optional(),
|
|
3673
|
+
scope: ScopeZ.optional()
|
|
3674
|
+
});
|
|
3675
|
+
var BodyProfileZ = z2.enum(["TLV_MAP", "RAW", "TLV_OBJ", "TLV_ARR"]);
|
|
3676
|
+
var IntentSchemaZ = z2.object({
|
|
3677
|
+
intent: z2.string().min(1),
|
|
3678
|
+
version: z2.number().int().positive(),
|
|
3679
|
+
bodyProfile: BodyProfileZ,
|
|
3680
|
+
fields: z2.array(SchemaFieldZ).min(1)
|
|
3681
|
+
});
|
|
3682
|
+
var WsHandshakeInputZ = z2.object({
|
|
3683
|
+
clientId: z2.string().min(1),
|
|
3684
|
+
isWs: z2.boolean(),
|
|
3685
|
+
ip: z2.string().min(1)
|
|
3686
|
+
});
|
|
3687
|
+
var WsHandshakeDecisionZ = z2.union([
|
|
3688
|
+
z2.object({ action: z2.literal("ALLOW") }),
|
|
3689
|
+
z2.object({ action: z2.literal("DENY"), code: z2.string() })
|
|
3690
|
+
]);
|
|
3691
|
+
var IPReputationInputZ = z2.object({
|
|
3692
|
+
ip: z2.string().min(1)
|
|
3693
|
+
});
|
|
3694
|
+
var IPReputationZ = z2.object({
|
|
3695
|
+
score: z2.number().min(-100).max(100),
|
|
3696
|
+
lastUpdated: z2.number().int(),
|
|
3697
|
+
totalRequests: z2.number().int().nonnegative(),
|
|
3698
|
+
failedRequests: z2.number().int().nonnegative(),
|
|
3699
|
+
blockedRequests: z2.number().int().nonnegative(),
|
|
3700
|
+
tags: z2.array(z2.string())
|
|
3701
|
+
});
|
|
3702
|
+
var UploadStatusZ = z2.enum([
|
|
3703
|
+
"INIT",
|
|
3704
|
+
"UPLOADING",
|
|
3705
|
+
"FINALIZING",
|
|
3706
|
+
"DONE",
|
|
3707
|
+
"ABORTED"
|
|
3708
|
+
]);
|
|
3709
|
+
var UploadSessionZ = z2.object({
|
|
3710
|
+
uploadIdHex: z2.string().min(1),
|
|
3711
|
+
fileName: z2.string().min(1),
|
|
3712
|
+
totalSize: z2.number().int().positive(),
|
|
3713
|
+
chunkSize: z2.number().int().positive(),
|
|
3714
|
+
totalChunks: z2.number().int().positive(),
|
|
3715
|
+
receivedCount: z2.number().int().nonnegative(),
|
|
3716
|
+
status: UploadStatusZ
|
|
3717
|
+
});
|
|
3718
|
+
var BodyBudgetInputZ = z2.object({
|
|
3719
|
+
intent: z2.string().min(1),
|
|
3720
|
+
headerLen: z2.number().int().nonnegative(),
|
|
3721
|
+
bodyLen: z2.number().int().nonnegative()
|
|
3722
|
+
});
|
|
3723
|
+
var BodyBudgetPolicyZ = z2.object({
|
|
3724
|
+
maxHeaderBytes: z2.number().int().positive(),
|
|
3725
|
+
maxBodyBytes: z2.number().int().positive()
|
|
3726
|
+
});
|
|
3727
|
+
var ChunkHashInputZ = z2.object({
|
|
3728
|
+
headerTLVs: z2.any(),
|
|
3729
|
+
// Map<number, Uint8Array> - flexible validation for compatibility
|
|
3730
|
+
bodyBytes: z2.any(),
|
|
3731
|
+
// Uint8Array - flexible validation for compatibility
|
|
3732
|
+
intent: z2.string().min(1)
|
|
3733
|
+
});
|
|
3734
|
+
var ProofType2 = /* @__PURE__ */ ((ProofType3) => {
|
|
3735
|
+
ProofType3[ProofType3["CAPSULE"] = 1] = "CAPSULE";
|
|
3736
|
+
ProofType3[ProofType3["JWT"] = 2] = "JWT";
|
|
3737
|
+
ProofType3[ProofType3["MTLS_ID"] = 3] = "MTLS_ID";
|
|
3738
|
+
ProofType3[ProofType3["DEVICE_SE"] = 4] = "DEVICE_SE";
|
|
3739
|
+
ProofType3[ProofType3["WITNESS_SIG"] = 5] = "WITNESS_SIG";
|
|
3740
|
+
return ProofType3;
|
|
3741
|
+
})(ProofType2 || {});
|
|
3742
|
+
var AxisContextZ = z2.object({
|
|
3743
|
+
pid: z2.custom((v) => Buffer.isBuffer(v)),
|
|
3744
|
+
// Process ID
|
|
3745
|
+
ts: z2.bigint(),
|
|
3746
|
+
// Timestamp
|
|
3747
|
+
intent: z2.string().min(1),
|
|
3748
|
+
actorId: z2.custom((v) => Buffer.isBuffer(v)),
|
|
3749
|
+
proofType: z2.enum(ProofType2),
|
|
3750
|
+
proofRef: z2.custom((v) => Buffer.isBuffer(v)),
|
|
3751
|
+
nonce: z2.custom((v) => Buffer.isBuffer(v)),
|
|
3752
|
+
ip: z2.string().min(1),
|
|
3753
|
+
nodeCertHash: z2.string().optional(),
|
|
3754
|
+
capsule: CapsuleZ.optional(),
|
|
3755
|
+
passport: PassportZ.optional(),
|
|
3756
|
+
meter: z2.any().optional()
|
|
3757
|
+
// ExecutionMeter instance - any to avoid circular dependency and allow class instance
|
|
3758
|
+
});
|
|
3759
|
+
var AxisErrorZ = z2.object({
|
|
3760
|
+
code: z2.string(),
|
|
3761
|
+
message: z2.string(),
|
|
3762
|
+
httpStatus: z2.number().int()
|
|
3763
|
+
});
|
|
3764
|
+
|
|
3765
|
+
// src/schemas/body-profile.validator.ts
|
|
3766
|
+
import { Injectable as Injectable8, Logger as Logger7 } from "@nestjs/common";
|
|
3767
|
+
var BodyProfile2 = /* @__PURE__ */ ((BodyProfile3) => {
|
|
3768
|
+
BodyProfile3[BodyProfile3["RAW"] = 0] = "RAW";
|
|
3769
|
+
BodyProfile3[BodyProfile3["TLV_MAP"] = 1] = "TLV_MAP";
|
|
3770
|
+
BodyProfile3[BodyProfile3["OBJ"] = 2] = "OBJ";
|
|
3771
|
+
BodyProfile3[BodyProfile3["ARR"] = 3] = "ARR";
|
|
3772
|
+
return BodyProfile3;
|
|
3773
|
+
})(BodyProfile2 || {});
|
|
3774
|
+
var BodyProfileValidator = class {
|
|
3775
|
+
constructor() {
|
|
3776
|
+
this.logger = new Logger7(BodyProfileValidator.name);
|
|
3777
|
+
}
|
|
3778
|
+
/**
|
|
3779
|
+
* Validate body matches declared profile
|
|
3780
|
+
*/
|
|
3781
|
+
validate(body, profile) {
|
|
3782
|
+
switch (profile) {
|
|
3783
|
+
case 0 /* RAW */:
|
|
3784
|
+
return this.validateRaw(body);
|
|
3785
|
+
case 1 /* TLV_MAP */:
|
|
3786
|
+
return this.validateTlvMap(body);
|
|
3787
|
+
case 2 /* OBJ */:
|
|
3788
|
+
return this.validateObj(body);
|
|
3789
|
+
case 3 /* ARR */:
|
|
3790
|
+
return this.validateArr(body);
|
|
3791
|
+
default:
|
|
3792
|
+
return {
|
|
3793
|
+
valid: false,
|
|
3794
|
+
error: `Unknown body profile: ${profile}`,
|
|
3795
|
+
profile
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
/**
|
|
3800
|
+
* RAW profile - no validation, any bytes accepted
|
|
3801
|
+
*/
|
|
3802
|
+
validateRaw(body) {
|
|
3803
|
+
return {
|
|
3804
|
+
valid: true,
|
|
3805
|
+
profile: 0 /* RAW */
|
|
3806
|
+
};
|
|
3807
|
+
}
|
|
3808
|
+
/**
|
|
3809
|
+
* TLV_MAP profile - flat TLV list (no nested structures)
|
|
3810
|
+
*/
|
|
3811
|
+
validateTlvMap(body) {
|
|
3812
|
+
try {
|
|
3813
|
+
const tlvs = decodeTLVsList(body);
|
|
3814
|
+
for (const tlv2 of tlvs) {
|
|
3815
|
+
if (tlv2.type === 254 || tlv2.type === 255) {
|
|
3816
|
+
return {
|
|
3817
|
+
valid: false,
|
|
3818
|
+
error: "TLV_MAP profile cannot contain nested OBJ/ARR types",
|
|
3819
|
+
profile: 1 /* TLV_MAP */
|
|
3820
|
+
};
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
return {
|
|
3824
|
+
valid: true,
|
|
3825
|
+
profile: 1 /* TLV_MAP */
|
|
3826
|
+
};
|
|
3827
|
+
} catch (error) {
|
|
3828
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
3829
|
+
return {
|
|
3830
|
+
valid: false,
|
|
3831
|
+
error: `TLV_MAP decode failed: ${message}`,
|
|
3832
|
+
profile: 1 /* TLV_MAP */
|
|
3833
|
+
};
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
/**
|
|
3837
|
+
* OBJ profile - must be valid nested object
|
|
3838
|
+
*/
|
|
3839
|
+
validateObj(body) {
|
|
3840
|
+
try {
|
|
3841
|
+
const tlvs = decodeTLVsList(body);
|
|
3842
|
+
const hasObj = tlvs.some((t) => t.type === 254);
|
|
3843
|
+
if (!hasObj && tlvs.length > 0) {
|
|
3844
|
+
return {
|
|
3845
|
+
valid: false,
|
|
3846
|
+
error: "OBJ profile must contain OBJ type (254)",
|
|
3847
|
+
profile: 2 /* OBJ */
|
|
3848
|
+
};
|
|
3849
|
+
}
|
|
3850
|
+
return {
|
|
3851
|
+
valid: true,
|
|
3852
|
+
profile: 2 /* OBJ */
|
|
3853
|
+
};
|
|
3854
|
+
} catch (error) {
|
|
3855
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
3856
|
+
return {
|
|
3857
|
+
valid: false,
|
|
3858
|
+
error: `OBJ decode failed: ${message}`,
|
|
3859
|
+
profile: 2 /* OBJ */
|
|
3860
|
+
};
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
/**
|
|
3864
|
+
* ARR profile - must be valid array
|
|
3865
|
+
*/
|
|
3866
|
+
validateArr(body) {
|
|
3867
|
+
try {
|
|
3868
|
+
const tlvs = decodeTLVsList(body);
|
|
3869
|
+
const hasArr = tlvs.some((t) => t.type === 255);
|
|
3870
|
+
if (!hasArr && tlvs.length > 0) {
|
|
3871
|
+
return {
|
|
3872
|
+
valid: false,
|
|
3873
|
+
error: "ARR profile must contain ARR type (255)",
|
|
3874
|
+
profile: 3 /* ARR */
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
return {
|
|
3878
|
+
valid: true,
|
|
3879
|
+
profile: 3 /* ARR */
|
|
3880
|
+
};
|
|
3881
|
+
} catch (error) {
|
|
3882
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
3883
|
+
return {
|
|
3884
|
+
valid: false,
|
|
3885
|
+
error: `ARR decode failed: ${message}`,
|
|
3886
|
+
profile: 3 /* ARR */
|
|
3887
|
+
};
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
};
|
|
3891
|
+
BodyProfileValidator = __decorateClass([
|
|
3892
|
+
Injectable8()
|
|
3893
|
+
], BodyProfileValidator);
|
|
3894
|
+
|
|
3895
|
+
// src/security/index.ts
|
|
3896
|
+
var security_exports = {};
|
|
3897
|
+
__export(security_exports, {
|
|
3898
|
+
CAPABILITIES: () => CAPABILITIES,
|
|
3899
|
+
INTENT_REQUIREMENTS: () => INTENT_REQUIREMENTS,
|
|
3900
|
+
PROOF_CAPABILITIES: () => PROOF_CAPABILITIES,
|
|
3901
|
+
canAccessResource: () => canAccessResource,
|
|
3902
|
+
hasScope: () => hasScope,
|
|
3903
|
+
parseScope: () => parseScope
|
|
3904
|
+
});
|
|
3905
|
+
|
|
3906
|
+
// src/sensors/index.ts
|
|
3907
|
+
var sensors_exports = {};
|
|
3908
|
+
__export(sensors_exports, {
|
|
3909
|
+
AccessProfileResolverSensor: () => AccessProfileResolverSensor,
|
|
3910
|
+
BodyBudgetSensor: () => BodyBudgetSensor,
|
|
3911
|
+
CapabilityEnforcementSensor: () => CapabilityEnforcementSensor,
|
|
3912
|
+
ChunkHashSensor: () => ChunkHashSensor,
|
|
3913
|
+
EntropySensor: () => EntropySensor,
|
|
3914
|
+
ExecutionTimeoutSensor: () => ExecutionTimeoutSensor,
|
|
3915
|
+
FrameBudgetSensor: () => FrameBudgetSensor,
|
|
3916
|
+
FrameHeaderSanitySensor: () => FrameHeaderSanitySensor,
|
|
3917
|
+
HeaderTLVLimitSensor: () => HeaderTLVLimitSensor,
|
|
3918
|
+
IntentAllowlistSensor: () => IntentAllowlistSensor,
|
|
3919
|
+
IntentRegistrySensor: () => IntentRegistrySensor,
|
|
3920
|
+
ProofPresenceSensor: () => ProofPresenceSensor,
|
|
3921
|
+
ProtocolStrictSensor: () => ProtocolStrictSensor,
|
|
3922
|
+
ReceiptPolicySensor: () => ReceiptPolicySensor,
|
|
3923
|
+
SchemaValidationSensor: () => SchemaValidationSensor,
|
|
3924
|
+
StreamScopeSensor: () => StreamScopeSensor,
|
|
3925
|
+
TLVParseSensor: () => TLVParseSensor,
|
|
3926
|
+
VarintHardeningSensor: () => VarintHardeningSensor
|
|
3927
|
+
});
|
|
3928
|
+
|
|
3929
|
+
// src/sensors/access-profile-resolver.sensor.ts
|
|
3930
|
+
import { Injectable as Injectable9 } from "@nestjs/common";
|
|
3931
|
+
var AccessProfileResolverSensor = class {
|
|
3932
|
+
constructor() {
|
|
3933
|
+
/** AxisSensor identifier */
|
|
3934
|
+
this.name = "AccessProfileResolverSensor";
|
|
3935
|
+
/**
|
|
3936
|
+
* Execution order - runs early to establish the access profile
|
|
3937
|
+
* for downstream sensors.
|
|
3938
|
+
*/
|
|
3939
|
+
this.order = BAND.IDENTITY + 10;
|
|
3940
|
+
}
|
|
3941
|
+
supports() {
|
|
3942
|
+
return true;
|
|
3943
|
+
}
|
|
3944
|
+
async run(input) {
|
|
3945
|
+
const hasCapsule = !!input.metadata?.capsuleId;
|
|
3946
|
+
const hasPassport = !!input.metadata?.passportSig;
|
|
3947
|
+
const hasMTLS = !!input.metadata?.mtlsId;
|
|
3948
|
+
const profile = hasCapsule || hasPassport || hasMTLS ? "GUARDED" : "PUBLIC";
|
|
3949
|
+
if (!input.metadata) input.metadata = {};
|
|
3950
|
+
input.metadata.profile = profile;
|
|
3951
|
+
return { action: "ALLOW" };
|
|
3952
|
+
}
|
|
3953
|
+
};
|
|
3954
|
+
AccessProfileResolverSensor = __decorateClass([
|
|
3955
|
+
Sensor(),
|
|
3956
|
+
Injectable9()
|
|
3957
|
+
], AccessProfileResolverSensor);
|
|
3958
|
+
|
|
3959
|
+
// src/sensors/body-budget.sensor.ts
|
|
3960
|
+
import { Injectable as Injectable10 } from "@nestjs/common";
|
|
3961
|
+
var BodyBudgetSensor = class {
|
|
3962
|
+
constructor() {
|
|
3963
|
+
/** AxisSensor identifier */
|
|
3964
|
+
this.name = "BodyBudgetSensor";
|
|
3965
|
+
/**
|
|
3966
|
+
* Execution order - after authentication
|
|
3967
|
+
*
|
|
3968
|
+
* Order 150 ensures:
|
|
3969
|
+
* - Authentication complete
|
|
3970
|
+
* - Runs before full body read
|
|
3971
|
+
* - Before schema validation (170)
|
|
3972
|
+
*/
|
|
3973
|
+
this.order = BAND.CONTENT + 10;
|
|
3974
|
+
}
|
|
3975
|
+
/**
|
|
3976
|
+
* Determines if this sensor should process the given input.
|
|
3977
|
+
*
|
|
3978
|
+
* Requires at least 8 bytes of peeked data to read headers.
|
|
3979
|
+
*
|
|
3980
|
+
* @param {SensorInput} input - Incoming request
|
|
3981
|
+
* @returns {boolean} True if sufficient peek data available
|
|
3982
|
+
*/
|
|
3983
|
+
supports(input) {
|
|
3984
|
+
return !!input.peek && input.peek.length >= 8;
|
|
3985
|
+
}
|
|
3986
|
+
/**
|
|
3987
|
+
* Validates header and body lengths against configured limits.
|
|
3988
|
+
*
|
|
3989
|
+
* **Frame Parsing:**
|
|
3990
|
+
* - Skip magic (5 bytes)
|
|
3991
|
+
* - Skip version (1 byte)
|
|
3992
|
+
* - Skip flags (1 byte)
|
|
3993
|
+
* - Read HDR_LEN varint
|
|
3994
|
+
* - Read BODY_LEN varint
|
|
3995
|
+
* - Compare against MAX_HDR_LEN and MAX_BODY_LEN
|
|
3996
|
+
*
|
|
3997
|
+
* @param {SensorInput} input - Request with peek data
|
|
3998
|
+
* @returns {Promise<SensorDecision>} ALLOW or DENY based on size limits
|
|
3999
|
+
*/
|
|
4000
|
+
async run(input) {
|
|
4001
|
+
const { peek } = input;
|
|
4002
|
+
if (!peek || peek.length < 8) {
|
|
4003
|
+
return { action: "ALLOW" };
|
|
4004
|
+
}
|
|
4005
|
+
try {
|
|
4006
|
+
let offset = 5;
|
|
4007
|
+
offset += 1;
|
|
4008
|
+
offset += 1;
|
|
4009
|
+
const { value: hdrLen, length: hdrBytes } = decodeVarint(peek, offset);
|
|
4010
|
+
offset += hdrBytes;
|
|
4011
|
+
const { value: bodyLen } = decodeVarint(peek, offset);
|
|
4012
|
+
if (hdrLen > MAX_HDR_LEN) {
|
|
4013
|
+
return {
|
|
4014
|
+
action: "DENY",
|
|
4015
|
+
code: "HEADER_TOO_LARGE",
|
|
4016
|
+
reason: `Header size ${hdrLen} exceeds limit ${MAX_HDR_LEN}`
|
|
4017
|
+
};
|
|
4018
|
+
}
|
|
4019
|
+
if (bodyLen > MAX_BODY_LEN) {
|
|
4020
|
+
return {
|
|
4021
|
+
action: "DENY",
|
|
4022
|
+
code: "BODY_TOO_LARGE",
|
|
4023
|
+
reason: `Body size ${bodyLen} exceeds limit ${MAX_BODY_LEN}`
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
4026
|
+
return { action: "ALLOW" };
|
|
4027
|
+
} catch (e) {
|
|
4028
|
+
return { action: "ALLOW" };
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
};
|
|
4032
|
+
BodyBudgetSensor = __decorateClass([
|
|
4033
|
+
Sensor(),
|
|
4034
|
+
Injectable10()
|
|
4035
|
+
], BodyBudgetSensor);
|
|
4036
|
+
|
|
4037
|
+
// src/sensors/capability-enforcement.sensor.ts
|
|
4038
|
+
import { Injectable as Injectable11, Logger as Logger8 } from "@nestjs/common";
|
|
4039
|
+
var CapabilityEnforcementSensor = class {
|
|
4040
|
+
constructor() {
|
|
4041
|
+
this.logger = new Logger8(CapabilityEnforcementSensor.name);
|
|
4042
|
+
/** AxisSensor identifier for logging and registry */
|
|
4043
|
+
this.name = "CapabilityEnforcementSensor";
|
|
4044
|
+
/**
|
|
4045
|
+
* Execution order - runs after authentication
|
|
4046
|
+
*
|
|
4047
|
+
* Order 100 ensures:
|
|
4048
|
+
* - Capsule is verified (CapsuleVerifySensor @ 80)
|
|
4049
|
+
* - Signature is verified (SigVerifySensor @ 90)
|
|
4050
|
+
* - We know the proof type for capability lookup
|
|
4051
|
+
*/
|
|
4052
|
+
this.order = BAND.POLICY + 10;
|
|
4053
|
+
}
|
|
4054
|
+
/**
|
|
4055
|
+
* Determines if this sensor should process the given input.
|
|
4056
|
+
*
|
|
4057
|
+
* Only activates when an intent is present.
|
|
4058
|
+
*
|
|
4059
|
+
* @param {SensorInput} input - Incoming AXIS request
|
|
4060
|
+
* @returns {boolean} True if intent is present
|
|
4061
|
+
*/
|
|
4062
|
+
supports(input) {
|
|
4063
|
+
return !!input.intent;
|
|
4064
|
+
}
|
|
4065
|
+
/**
|
|
4066
|
+
* Enforces capability requirements for the requested intent.
|
|
4067
|
+
*
|
|
4068
|
+
* **Processing Flow:**
|
|
4069
|
+
* 1. Extract proof type from packet (default: 0/NONE)
|
|
4070
|
+
* 2. Look up capabilities granted by this proof type
|
|
4071
|
+
* 3. Look up capabilities required by the intent
|
|
4072
|
+
* 4. If no requirements, ALLOW
|
|
4073
|
+
* 5. Check if all required capabilities are granted
|
|
4074
|
+
* 6. If missing capabilities, DENY with details
|
|
4075
|
+
* 7. Otherwise, ALLOW
|
|
4076
|
+
*
|
|
4077
|
+
* @param {SensorInput} input - Request with intent and packet
|
|
4078
|
+
* @returns {Promise<SensorDecision>} ALLOW or DENY based on capabilities
|
|
4079
|
+
*/
|
|
4080
|
+
async run(input) {
|
|
4081
|
+
const { intent, packet } = input;
|
|
4082
|
+
if (!intent) {
|
|
4083
|
+
return { action: "ALLOW" };
|
|
4084
|
+
}
|
|
4085
|
+
const proofType = packet?.proofType ?? 0;
|
|
4086
|
+
const grantedCapabilities = PROOF_CAPABILITIES[proofType] || [];
|
|
4087
|
+
const requiredCapabilities = this.getRequiredCapabilities(intent);
|
|
4088
|
+
if (requiredCapabilities.length === 0) {
|
|
4089
|
+
return { action: "ALLOW" };
|
|
4090
|
+
}
|
|
4091
|
+
const missingCapabilities = requiredCapabilities.filter(
|
|
4092
|
+
(cap) => !grantedCapabilities.includes(cap)
|
|
4093
|
+
);
|
|
4094
|
+
if (missingCapabilities.length > 0) {
|
|
4095
|
+
this.logger.warn(
|
|
4096
|
+
`Capability denied for ${intent}: missing ${missingCapabilities.join(", ")} (has: ${grantedCapabilities.join(", ")})`
|
|
4097
|
+
);
|
|
4098
|
+
return {
|
|
4099
|
+
action: "DENY",
|
|
4100
|
+
code: "CAPABILITY_DENIED",
|
|
4101
|
+
reason: `Missing capabilities: ${missingCapabilities.join(", ")}`
|
|
4102
|
+
};
|
|
4103
|
+
}
|
|
4104
|
+
return { action: "ALLOW" };
|
|
4105
|
+
}
|
|
4106
|
+
/**
|
|
4107
|
+
* Gets required capabilities for an intent.
|
|
4108
|
+
*
|
|
4109
|
+
* **Lookup Strategy:**
|
|
4110
|
+
* 1. Check for exact intent match
|
|
4111
|
+
* 2. Check for prefix pattern match (*.suffix)
|
|
4112
|
+
* 3. Default to 'execute' for unknown intents
|
|
4113
|
+
*
|
|
4114
|
+
* @private
|
|
4115
|
+
* @param {string} intent - Intent name to look up
|
|
4116
|
+
* @returns {Capability[]} Array of required capabilities
|
|
4117
|
+
*/
|
|
4118
|
+
getRequiredCapabilities(intent) {
|
|
4119
|
+
if (INTENT_REQUIREMENTS[intent]) {
|
|
4120
|
+
return INTENT_REQUIREMENTS[intent];
|
|
4121
|
+
}
|
|
4122
|
+
for (const [pattern, caps] of Object.entries(INTENT_REQUIREMENTS)) {
|
|
4123
|
+
if (pattern.endsWith(".*")) {
|
|
4124
|
+
const prefix = pattern.slice(0, -1);
|
|
4125
|
+
if (intent.startsWith(prefix)) {
|
|
4126
|
+
return caps;
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
return ["execute"];
|
|
4131
|
+
}
|
|
4132
|
+
};
|
|
4133
|
+
CapabilityEnforcementSensor = __decorateClass([
|
|
4134
|
+
Sensor(),
|
|
4135
|
+
Injectable11()
|
|
4136
|
+
], CapabilityEnforcementSensor);
|
|
4137
|
+
|
|
4138
|
+
// src/sensors/chunk-hash.sensor.ts
|
|
4139
|
+
import { Injectable as Injectable12 } from "@nestjs/common";
|
|
4140
|
+
import { createHash as createHash6 } from "crypto";
|
|
4141
|
+
var ChunkHashSensor = class {
|
|
4142
|
+
constructor() {
|
|
4143
|
+
/** Sensor identifier */
|
|
4144
|
+
this.name = "ChunkHashSensor";
|
|
4145
|
+
/**
|
|
4146
|
+
* Execution order - after session validation
|
|
4147
|
+
*
|
|
4148
|
+
* Order 190 ensures:
|
|
4149
|
+
* - Session validated (180)
|
|
4150
|
+
* - Chunk parameters verified
|
|
4151
|
+
* - Hash check before storage
|
|
4152
|
+
*/
|
|
4153
|
+
this.order = BAND.CONTENT + 50;
|
|
4154
|
+
}
|
|
4155
|
+
/**
|
|
4156
|
+
* Determines if this sensor should process the given input.
|
|
4157
|
+
*
|
|
4158
|
+
* Only processes file.chunk intents.
|
|
4159
|
+
*
|
|
4160
|
+
* @param {SensorInput} input - Incoming request
|
|
4161
|
+
* @returns {boolean} True if intent is 'file.chunk'
|
|
4162
|
+
*/
|
|
4163
|
+
supports(input) {
|
|
4164
|
+
return input.intent === "file.chunk";
|
|
4165
|
+
}
|
|
4166
|
+
/**
|
|
4167
|
+
* Validates chunk data against declared SHA-256 hash.
|
|
4168
|
+
*
|
|
4169
|
+
* **Processing Flow:**
|
|
4170
|
+
* 1. Check for required headerTLVs and body
|
|
4171
|
+
* 2. Extract expected hash from TLV 73
|
|
4172
|
+
* 3. Verify hash is exactly 32 bytes
|
|
4173
|
+
* 4. Compute SHA-256 of body
|
|
4174
|
+
* 5. Compare bytes (timing-safe)
|
|
4175
|
+
* 6. DENY on mismatch
|
|
4176
|
+
*
|
|
4177
|
+
* @param {SensorInput} input - Request with chunk body
|
|
4178
|
+
* @returns {Promise<SensorDecision>} ALLOW if hash matches, DENY otherwise
|
|
4179
|
+
*/
|
|
4180
|
+
async run(input) {
|
|
4181
|
+
const headerTLVs = input.headerTLVs;
|
|
4182
|
+
const bodyBytes = input.body;
|
|
4183
|
+
if (!headerTLVs || !bodyBytes) {
|
|
4184
|
+
return {
|
|
4185
|
+
action: "DENY",
|
|
4186
|
+
code: "SENSOR_INVALID_INPUT",
|
|
4187
|
+
reason: "Missing headerTLVs or body"
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
4190
|
+
const TLV_SHA256_CHUNK2 = 73;
|
|
4191
|
+
const expected = headerTLVs.get(TLV_SHA256_CHUNK2);
|
|
4192
|
+
if (!expected || expected.length !== 32) {
|
|
4193
|
+
return {
|
|
4194
|
+
action: "DENY",
|
|
4195
|
+
code: "FILE_CHUNK_HASH_MISSING",
|
|
4196
|
+
reason: "Missing sha256Chunk TLV in header"
|
|
4197
|
+
};
|
|
4198
|
+
}
|
|
4199
|
+
const actual = createHash6("sha256").update(bodyBytes).digest();
|
|
4200
|
+
if (!Buffer.from(actual).equals(Buffer.from(expected))) {
|
|
4201
|
+
return {
|
|
4202
|
+
action: "DENY",
|
|
4203
|
+
code: "FILE_CHUNK_HASH_MISMATCH",
|
|
4204
|
+
reason: "Chunk hash mismatch - data corrupted"
|
|
4205
|
+
};
|
|
4206
|
+
}
|
|
4207
|
+
return { action: "ALLOW" };
|
|
4208
|
+
}
|
|
4209
|
+
};
|
|
4210
|
+
ChunkHashSensor = __decorateClass([
|
|
4211
|
+
Sensor(),
|
|
4212
|
+
Injectable12()
|
|
4213
|
+
], ChunkHashSensor);
|
|
4214
|
+
|
|
4215
|
+
// src/sensors/entropy.sensor.ts
|
|
4216
|
+
import { Injectable as Injectable13, Logger as Logger9 } from "@nestjs/common";
|
|
4217
|
+
import * as crypto4 from "crypto";
|
|
4218
|
+
var EntropySensor = class {
|
|
4219
|
+
constructor() {
|
|
4220
|
+
this.logger = new Logger9(EntropySensor.name);
|
|
4221
|
+
/**
|
|
4222
|
+
* Minimum acceptable entropy in bits per byte.
|
|
4223
|
+
*
|
|
4224
|
+
* 3.0 bits/byte is a conservative threshold:
|
|
4225
|
+
* - Random data: ~7.9 bits/byte
|
|
4226
|
+
* - English text: ~4.5 bits/byte
|
|
4227
|
+
* - Sequential data: ~0-2 bits/byte
|
|
4228
|
+
*/
|
|
4229
|
+
this.MIN_ENTROPY_THRESHOLD = 3;
|
|
4230
|
+
/** AxisSensor identifier */
|
|
4231
|
+
this.name = "EntropySensor";
|
|
4232
|
+
/**
|
|
4233
|
+
* Execution order - anomaly detection phase
|
|
4234
|
+
*
|
|
4235
|
+
* Order 130 ensures:
|
|
4236
|
+
* - Replay protection done (120)
|
|
4237
|
+
* - Runs before expensive policy lookups
|
|
4238
|
+
*/
|
|
4239
|
+
this.order = BAND.POLICY + 35;
|
|
4240
|
+
}
|
|
4241
|
+
/**
|
|
4242
|
+
* Calculates Shannon entropy of a byte array.
|
|
4243
|
+
*
|
|
4244
|
+
* **Algorithm:**
|
|
4245
|
+
* 1. Count frequency of each byte value (0-255)
|
|
4246
|
+
* 2. Calculate probability p = count / total
|
|
4247
|
+
* 3. Sum: -Σ(p * log2(p))
|
|
4248
|
+
*
|
|
4249
|
+
* @private
|
|
4250
|
+
* @param {Uint8Array} data - Bytes to analyze
|
|
4251
|
+
* @returns {number} Entropy in bits per byte (0-8 scale)
|
|
4252
|
+
*/
|
|
4253
|
+
calculateEntropy(data) {
|
|
4254
|
+
if (data.length === 0) return 0;
|
|
4255
|
+
const freq = /* @__PURE__ */ new Map();
|
|
4256
|
+
for (const byte of data) {
|
|
4257
|
+
freq.set(byte, (freq.get(byte) || 0) + 1);
|
|
4258
|
+
}
|
|
4259
|
+
let entropy = 0;
|
|
4260
|
+
const len = data.length;
|
|
4261
|
+
for (const count of freq.values()) {
|
|
4262
|
+
const p = count / len;
|
|
4263
|
+
entropy -= p * Math.log2(p);
|
|
4264
|
+
}
|
|
4265
|
+
return entropy;
|
|
4266
|
+
}
|
|
4267
|
+
/**
|
|
4268
|
+
* Checks for sequential patterns in data.
|
|
4269
|
+
*
|
|
4270
|
+
* Detects sequences like [1,2,3,4...] or [10,9,8,7...].
|
|
4271
|
+
* More than 50% sequential is considered suspicious.
|
|
4272
|
+
*
|
|
4273
|
+
* @private
|
|
4274
|
+
* @param {Uint8Array} data - Bytes to analyze
|
|
4275
|
+
* @returns {boolean} True if sequential pattern detected
|
|
4276
|
+
*/
|
|
4277
|
+
hasSequentialPattern(data) {
|
|
4278
|
+
if (data.length < 4) return false;
|
|
4279
|
+
let ascending = 0;
|
|
4280
|
+
let descending = 0;
|
|
4281
|
+
for (let i = 1; i < data.length; i++) {
|
|
4282
|
+
if (data[i] === data[i - 1] + 1) ascending++;
|
|
4283
|
+
if (data[i] === data[i - 1] - 1) descending++;
|
|
4284
|
+
}
|
|
4285
|
+
return ascending > data.length / 2 || descending > data.length / 2;
|
|
4286
|
+
}
|
|
4287
|
+
/**
|
|
4288
|
+
* Checks for repeated patterns in data.
|
|
4289
|
+
*
|
|
4290
|
+
* Detects patterns like [0xAB, 0xCD, 0xAB, 0xCD...].
|
|
4291
|
+
* Checks for 2, 4, and 8 byte repeating patterns.
|
|
4292
|
+
*
|
|
4293
|
+
* @private
|
|
4294
|
+
* @param {Uint8Array} data - Bytes to analyze
|
|
4295
|
+
* @returns {boolean} True if repeated pattern detected
|
|
4296
|
+
*/
|
|
4297
|
+
hasRepeatedPattern(data) {
|
|
4298
|
+
if (data.length < 8) return false;
|
|
4299
|
+
for (const patternLen of [2, 4, 8]) {
|
|
4300
|
+
if (data.length % patternLen !== 0) continue;
|
|
4301
|
+
let matches = 0;
|
|
4302
|
+
for (let i = patternLen; i < data.length; i++) {
|
|
4303
|
+
if (data[i] === data[i % patternLen]) matches++;
|
|
4304
|
+
}
|
|
4305
|
+
if (matches > (data.length - patternLen) * 0.9) {
|
|
4306
|
+
return true;
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
return false;
|
|
4310
|
+
}
|
|
4311
|
+
/**
|
|
4312
|
+
* Analyzes entropy of PID and nonce in request headers.
|
|
4313
|
+
*
|
|
4314
|
+
* **Processing Flow:**
|
|
4315
|
+
* 1. Extract PID and nonce from header TLVs
|
|
4316
|
+
* 2. Calculate entropy for each
|
|
4317
|
+
* 3. Check for sequential patterns
|
|
4318
|
+
* 4. Check for repeated patterns
|
|
4319
|
+
* 5. Accumulate issues and score delta
|
|
4320
|
+
* 6. Return FLAG if issues found, ALLOW otherwise
|
|
4321
|
+
*
|
|
4322
|
+
* @param {SensorInput} input - Request with header TLVs
|
|
4323
|
+
* @returns {Promise<SensorDecision>} ALLOW or FLAG based on entropy analysis
|
|
4324
|
+
*/
|
|
4325
|
+
async run(input) {
|
|
4326
|
+
const headers = input.headerTLVs;
|
|
4327
|
+
if (!headers) {
|
|
4328
|
+
return { action: "ALLOW" };
|
|
4329
|
+
}
|
|
4330
|
+
const pid = headers.get(TLV_PID);
|
|
4331
|
+
const nonce = headers.get(TLV_NONCE);
|
|
4332
|
+
const issues = [];
|
|
4333
|
+
let totalDelta = 0;
|
|
4334
|
+
if (pid && pid.length > 0) {
|
|
4335
|
+
const pidEntropy = this.calculateEntropy(pid);
|
|
4336
|
+
if (pidEntropy < this.MIN_ENTROPY_THRESHOLD) {
|
|
4337
|
+
issues.push(`pid_low_entropy:${pidEntropy.toFixed(2)}`);
|
|
4338
|
+
totalDelta -= 3;
|
|
4339
|
+
}
|
|
4340
|
+
if (this.hasSequentialPattern(pid)) {
|
|
4341
|
+
issues.push("pid_sequential");
|
|
4342
|
+
totalDelta -= 5;
|
|
4343
|
+
}
|
|
4344
|
+
if (this.hasRepeatedPattern(pid)) {
|
|
4345
|
+
issues.push("pid_repeated");
|
|
4346
|
+
totalDelta -= 5;
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
if (nonce && nonce.length > 0) {
|
|
4350
|
+
const nonceEntropy = this.calculateEntropy(nonce);
|
|
4351
|
+
if (nonceEntropy < this.MIN_ENTROPY_THRESHOLD) {
|
|
4352
|
+
issues.push(`nonce_low_entropy:${nonceEntropy.toFixed(2)}`);
|
|
4353
|
+
totalDelta -= 3;
|
|
4354
|
+
}
|
|
4355
|
+
if (this.hasSequentialPattern(nonce)) {
|
|
4356
|
+
issues.push("nonce_sequential");
|
|
4357
|
+
totalDelta -= 5;
|
|
4358
|
+
}
|
|
4359
|
+
if (this.hasRepeatedPattern(nonce)) {
|
|
4360
|
+
issues.push("nonce_repeated");
|
|
4361
|
+
totalDelta -= 5;
|
|
4362
|
+
}
|
|
4363
|
+
}
|
|
4364
|
+
if (issues.length > 0) {
|
|
4365
|
+
this.logger.warn(`Entropy issues from ${input.ip}: ${issues.join(", ")}`);
|
|
4366
|
+
return {
|
|
4367
|
+
action: "FLAG",
|
|
4368
|
+
scoreDelta: totalDelta,
|
|
4369
|
+
reasons: issues
|
|
4370
|
+
};
|
|
4371
|
+
}
|
|
4372
|
+
return { action: "ALLOW" };
|
|
4373
|
+
}
|
|
4374
|
+
/**
|
|
4375
|
+
* Generates cryptographically secure random bytes.
|
|
4376
|
+
*
|
|
4377
|
+
* Utility method for SDK/client code to ensure proper entropy.
|
|
4378
|
+
* Uses Node.js crypto.randomBytes for secure PRNG.
|
|
4379
|
+
*
|
|
4380
|
+
* @static
|
|
4381
|
+
* @param {number} length - Number of random bytes
|
|
4382
|
+
* @returns {Uint8Array} Cryptographically secure random bytes
|
|
4383
|
+
*/
|
|
4384
|
+
static generateSecureRandom(length) {
|
|
4385
|
+
return new Uint8Array(crypto4.randomBytes(length));
|
|
4386
|
+
}
|
|
4387
|
+
};
|
|
4388
|
+
EntropySensor = __decorateClass([
|
|
4389
|
+
Sensor(),
|
|
4390
|
+
Injectable13()
|
|
4391
|
+
], EntropySensor);
|
|
4392
|
+
|
|
4393
|
+
// src/sensors/execution-timeout.sensor.ts
|
|
4394
|
+
import { Injectable as Injectable14, Logger as Logger10 } from "@nestjs/common";
|
|
4395
|
+
var ExecutionTimeoutSensor = class {
|
|
4396
|
+
constructor() {
|
|
4397
|
+
this.logger = new Logger10(ExecutionTimeoutSensor.name);
|
|
4398
|
+
/** AxisSensor identifier */
|
|
4399
|
+
this.name = "ExecutionTimeoutSensor";
|
|
4400
|
+
/**
|
|
4401
|
+
* Execution order - late, near handler execution
|
|
4402
|
+
*
|
|
4403
|
+
* Order 210 ensures:
|
|
4404
|
+
* - All validation complete
|
|
4405
|
+
* - Deadline set just before handler
|
|
4406
|
+
*/
|
|
4407
|
+
this.order = BAND.BUSINESS + 10;
|
|
4408
|
+
}
|
|
4409
|
+
/**
|
|
4410
|
+
* Determines if this sensor should process the given input.
|
|
4411
|
+
*
|
|
4412
|
+
* @param {SensorInput} input - Incoming request
|
|
4413
|
+
* @returns {boolean} True if intent is present
|
|
4414
|
+
*/
|
|
4415
|
+
supports(input) {
|
|
4416
|
+
return !!input.intent;
|
|
4417
|
+
}
|
|
4418
|
+
/**
|
|
4419
|
+
* Sets execution deadline in the request context.
|
|
4420
|
+
*
|
|
4421
|
+
* **Processing Flow:**
|
|
4422
|
+
* 1. Look up timeout for intent
|
|
4423
|
+
* 2. Calculate absolute deadline
|
|
4424
|
+
* 3. Store in context for handler use
|
|
4425
|
+
* 4. Return ALLOW
|
|
4426
|
+
*
|
|
4427
|
+
* @param {SensorInput} input - Request with intent
|
|
4428
|
+
* @returns {Promise<SensorDecision>} Always ALLOW
|
|
4429
|
+
*/
|
|
4430
|
+
async run(input) {
|
|
4431
|
+
const { intent, context } = input;
|
|
4432
|
+
if (!intent) {
|
|
4433
|
+
return { action: "ALLOW" };
|
|
4434
|
+
}
|
|
4435
|
+
const timeout = resolveTimeout(intent);
|
|
4436
|
+
const deadline = Date.now() + timeout;
|
|
4437
|
+
if (context) {
|
|
4438
|
+
context.deadline = deadline;
|
|
4439
|
+
context.timeoutMs = timeout;
|
|
4440
|
+
}
|
|
4441
|
+
this.logger.debug(
|
|
4442
|
+
`Set ${timeout}ms timeout for ${intent} (deadline: ${new Date(deadline).toISOString()})`
|
|
4443
|
+
);
|
|
4444
|
+
return { action: "ALLOW" };
|
|
4445
|
+
}
|
|
4446
|
+
/**
|
|
4447
|
+
* Checks if a deadline has been exceeded.
|
|
4448
|
+
*
|
|
4449
|
+
* Utility method for handler code.
|
|
4450
|
+
*
|
|
4451
|
+
* @static
|
|
4452
|
+
* @param {object} ctx - Context with deadline
|
|
4453
|
+
* @returns {boolean} True if deadline passed
|
|
4454
|
+
*/
|
|
4455
|
+
static isExpired(ctx) {
|
|
4456
|
+
if (!ctx.deadline) return false;
|
|
4457
|
+
return Date.now() > ctx.deadline;
|
|
4458
|
+
}
|
|
4459
|
+
/**
|
|
4460
|
+
* Gets remaining time until deadline.
|
|
4461
|
+
*
|
|
4462
|
+
* Utility method for handler code.
|
|
4463
|
+
*
|
|
4464
|
+
* @static
|
|
4465
|
+
* @param {object} ctx - Context with deadline
|
|
4466
|
+
* @returns {number} Remaining milliseconds (0 if expired, Infinity if no deadline)
|
|
4467
|
+
*/
|
|
4468
|
+
static getRemainingMs(ctx) {
|
|
4469
|
+
if (!ctx.deadline) return Infinity;
|
|
4470
|
+
return Math.max(0, ctx.deadline - Date.now());
|
|
4471
|
+
}
|
|
4472
|
+
};
|
|
4473
|
+
ExecutionTimeoutSensor = __decorateClass([
|
|
4474
|
+
Sensor(),
|
|
4475
|
+
Injectable14()
|
|
4476
|
+
], ExecutionTimeoutSensor);
|
|
4477
|
+
|
|
4478
|
+
// src/sensors/frame-budget.sensor.ts
|
|
4479
|
+
import { Injectable as Injectable15 } from "@nestjs/common";
|
|
4480
|
+
var FrameBudgetSensor = class {
|
|
4481
|
+
constructor(config) {
|
|
4482
|
+
this.config = config;
|
|
4483
|
+
/** AxisSensor identifier for logging and registry */
|
|
4484
|
+
this.name = "FrameBudgetSensor";
|
|
4485
|
+
/**
|
|
4486
|
+
* Execution order - runs after protocol validation
|
|
4487
|
+
*
|
|
4488
|
+
* Order 20 ensures:
|
|
4489
|
+
* - Protocol is valid (ProtocolStrictSensor @ 10)
|
|
4490
|
+
* - Size checked before expensive processing
|
|
4491
|
+
*/
|
|
4492
|
+
this.order = BAND.WIRE + 20;
|
|
4493
|
+
}
|
|
4494
|
+
/**
|
|
4495
|
+
* Determines if this sensor should process the given input.
|
|
4496
|
+
*
|
|
4497
|
+
* Only activates when Content-Length header is available.
|
|
4498
|
+
* WebSocket frames may not have Content-Length; they use different size tracking.
|
|
4499
|
+
*
|
|
4500
|
+
* @param {SensorInput} input - Incoming AXIS request
|
|
4501
|
+
* @returns {boolean} True if Content-Length is present
|
|
4502
|
+
*/
|
|
4503
|
+
supports(input) {
|
|
4504
|
+
return typeof input.contentLength === "number";
|
|
4505
|
+
}
|
|
4506
|
+
/**
|
|
4507
|
+
* Validates frame size against configured limits.
|
|
4508
|
+
*
|
|
4509
|
+
* **Current Implementation:** Stub that always allows.
|
|
4510
|
+
*
|
|
4511
|
+
* **TODO:** Full implementation should:
|
|
4512
|
+
* 1. Load intent policy for the request
|
|
4513
|
+
* 2. Get maxFrameBytes from policy
|
|
4514
|
+
* 3. Compare against contentLength
|
|
4515
|
+
* 4. DENY if exceeded
|
|
4516
|
+
*
|
|
4517
|
+
* @param {SensorInput} input - Request with contentLength
|
|
4518
|
+
* @returns {Promise<SensorDecision>} ALLOW or DENY based on size
|
|
4519
|
+
*/
|
|
4520
|
+
async run(input) {
|
|
4521
|
+
const maxBytes = this.config.get("AXIS_MAX_FRAME_SIZE") || 50 * 1024 * 1024;
|
|
4522
|
+
const contentLength = input.contentLength;
|
|
4523
|
+
if (typeof contentLength !== "number") {
|
|
4524
|
+
return { action: "ALLOW" };
|
|
4525
|
+
}
|
|
4526
|
+
if (contentLength > maxBytes) {
|
|
4527
|
+
return {
|
|
4528
|
+
action: "DENY",
|
|
4529
|
+
code: "FRAME_TOO_LARGE",
|
|
4530
|
+
reason: `Frame size ${contentLength} exceeds limit ${maxBytes}`
|
|
4531
|
+
};
|
|
4532
|
+
}
|
|
4533
|
+
return { action: "ALLOW" };
|
|
4534
|
+
}
|
|
4535
|
+
};
|
|
4536
|
+
FrameBudgetSensor = __decorateClass([
|
|
4537
|
+
Sensor({ phase: "PRE_DECODE" }),
|
|
4538
|
+
Injectable15()
|
|
4539
|
+
], FrameBudgetSensor);
|
|
4540
|
+
|
|
4541
|
+
// src/sensors/frame-header-sanity.sensor.ts
|
|
4542
|
+
import { Injectable as Injectable16 } from "@nestjs/common";
|
|
4543
|
+
var FrameHeaderSanitySensor = class {
|
|
4544
|
+
constructor() {
|
|
4545
|
+
this.name = "FrameHeaderSanitySensor";
|
|
4546
|
+
this.order = BAND.WIRE + 30;
|
|
4547
|
+
}
|
|
4548
|
+
supports(input) {
|
|
4549
|
+
return !!input.peek && input.peek.length >= 7;
|
|
4550
|
+
}
|
|
4551
|
+
async run(input) {
|
|
4552
|
+
const peek = input.peek;
|
|
4553
|
+
const contentLen = input.contentLength || 0;
|
|
4554
|
+
if (peek.length < 5 || !this.bufferEqual(peek.slice(0, 5), AXIS_MAGIC)) {
|
|
4555
|
+
return {
|
|
4556
|
+
action: "DENY",
|
|
4557
|
+
code: "INVALID_MAGIC",
|
|
4558
|
+
reason: "Frame magic is not AXIS1"
|
|
4559
|
+
};
|
|
4560
|
+
}
|
|
4561
|
+
if (peek[5] !== AXIS_VERSION) {
|
|
4562
|
+
return {
|
|
4563
|
+
action: "DENY",
|
|
4564
|
+
code: "UNSUPPORTED_VERSION",
|
|
4565
|
+
reason: `Unsupported version: ${peek[5]}`
|
|
4566
|
+
};
|
|
4567
|
+
}
|
|
4568
|
+
if (contentLen > MAX_FRAME_LEN) {
|
|
4569
|
+
return {
|
|
4570
|
+
action: "DENY",
|
|
4571
|
+
code: "FRAME_TOO_LARGE",
|
|
4572
|
+
reason: `Frame size ${contentLen} exceeds max ${MAX_FRAME_LEN}`
|
|
4573
|
+
};
|
|
4574
|
+
}
|
|
4575
|
+
return { action: "ALLOW" };
|
|
4576
|
+
}
|
|
4577
|
+
bufferEqual(a, b) {
|
|
4578
|
+
if (a.length !== b.length) return false;
|
|
4579
|
+
for (let i = 0; i < a.length; i++) {
|
|
4580
|
+
if (a[i] !== b[i]) return false;
|
|
4581
|
+
}
|
|
4582
|
+
return true;
|
|
4583
|
+
}
|
|
4584
|
+
};
|
|
4585
|
+
FrameHeaderSanitySensor = __decorateClass([
|
|
4586
|
+
Injectable16(),
|
|
4587
|
+
Sensor({ phase: "PRE_DECODE" })
|
|
4588
|
+
], FrameHeaderSanitySensor);
|
|
4589
|
+
|
|
4590
|
+
// src/sensors/header-tlv-limit.sensor.ts
|
|
4591
|
+
import { Injectable as Injectable17 } from "@nestjs/common";
|
|
4592
|
+
var HeaderTLVLimitSensor = class {
|
|
4593
|
+
constructor() {
|
|
4594
|
+
this.name = "HeaderTLVLimitSensor";
|
|
4595
|
+
this.order = BAND.CONTENT + 0;
|
|
4596
|
+
this.MAX_TLVS = 64;
|
|
4597
|
+
}
|
|
4598
|
+
supports(input) {
|
|
4599
|
+
return !!input.headerTLVs || !!input.packet;
|
|
4600
|
+
}
|
|
4601
|
+
async run(input) {
|
|
4602
|
+
if (input.headerTLVs && input.headerTLVs.size > this.MAX_TLVS) {
|
|
4603
|
+
return {
|
|
4604
|
+
action: "DENY",
|
|
4605
|
+
code: "TOO_MANY_TLVS",
|
|
4606
|
+
reason: `Header TLVs (${input.headerTLVs.size}) exceed max (${this.MAX_TLVS})`
|
|
4607
|
+
};
|
|
4608
|
+
}
|
|
4609
|
+
if (input.packet && input.packet.headerBytes) {
|
|
4610
|
+
const hdrLen = input.packet.headerBytes.length;
|
|
4611
|
+
if (hdrLen > MAX_HDR_LEN) {
|
|
4612
|
+
return {
|
|
4613
|
+
action: "DENY",
|
|
4614
|
+
code: "HEADER_TOO_LARGE",
|
|
4615
|
+
reason: `Header size ${hdrLen} exceeds max ${MAX_HDR_LEN}`
|
|
4616
|
+
};
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
return { action: "ALLOW" };
|
|
4620
|
+
}
|
|
4621
|
+
};
|
|
4622
|
+
HeaderTLVLimitSensor = __decorateClass([
|
|
4623
|
+
Injectable17(),
|
|
4624
|
+
Sensor()
|
|
4625
|
+
], HeaderTLVLimitSensor);
|
|
4626
|
+
|
|
4627
|
+
// src/sensors/intent-allowlist.sensor.ts
|
|
4628
|
+
import { Injectable as Injectable18 } from "@nestjs/common";
|
|
4629
|
+
var PUBLIC_INTENT_ALLOWLIST = [
|
|
4630
|
+
"public.",
|
|
4631
|
+
"schema.",
|
|
4632
|
+
"catalog.",
|
|
4633
|
+
"health.",
|
|
4634
|
+
"system."
|
|
4635
|
+
];
|
|
4636
|
+
var IntentAllowlistSensor = class {
|
|
4637
|
+
constructor() {
|
|
4638
|
+
this.name = "IntentAllowlistSensor";
|
|
4639
|
+
this.order = BAND.IDENTITY + 20;
|
|
4640
|
+
}
|
|
4641
|
+
supports(input) {
|
|
4642
|
+
return !!input.intent;
|
|
4643
|
+
}
|
|
4644
|
+
async run(input) {
|
|
4645
|
+
const profile = input.metadata?.profile || "PUBLIC";
|
|
4646
|
+
const intent = input.intent || "";
|
|
4647
|
+
if (profile === "PUBLIC") {
|
|
4648
|
+
const isAllowed = PUBLIC_INTENT_ALLOWLIST.some(
|
|
4649
|
+
(prefix) => intent.startsWith(prefix)
|
|
4650
|
+
);
|
|
4651
|
+
if (!isAllowed) {
|
|
4652
|
+
return {
|
|
4653
|
+
action: "DENY",
|
|
4654
|
+
code: "INTENT_NOT_ALLOWED",
|
|
4655
|
+
reason: `Intent '${intent}' not in public allowlist`
|
|
4656
|
+
};
|
|
4657
|
+
}
|
|
4658
|
+
}
|
|
4659
|
+
return { action: "ALLOW" };
|
|
4660
|
+
}
|
|
4661
|
+
};
|
|
4662
|
+
IntentAllowlistSensor = __decorateClass([
|
|
4663
|
+
Injectable18(),
|
|
4664
|
+
Sensor()
|
|
4665
|
+
], IntentAllowlistSensor);
|
|
4666
|
+
|
|
4667
|
+
// src/sensors/intent-registry.sensor.ts
|
|
4668
|
+
import { Injectable as Injectable19 } from "@nestjs/common";
|
|
4669
|
+
var IntentRegistrySensor = class {
|
|
4670
|
+
constructor(router) {
|
|
4671
|
+
this.router = router;
|
|
4672
|
+
this.name = "IntentRegistrySensor";
|
|
4673
|
+
this.order = BAND.IDENTITY + 25;
|
|
4674
|
+
}
|
|
4675
|
+
supports(input) {
|
|
4676
|
+
return !!input.intent;
|
|
4677
|
+
}
|
|
4678
|
+
async run(input) {
|
|
4679
|
+
const intent = input.intent;
|
|
4680
|
+
if (this.router.has(intent)) {
|
|
4681
|
+
return { action: "ALLOW" };
|
|
4682
|
+
}
|
|
4683
|
+
return {
|
|
4684
|
+
action: "DENY",
|
|
4685
|
+
code: "INTENT_NOT_REGISTERED",
|
|
4686
|
+
reason: `Intent '${intent}' is not registered`
|
|
4687
|
+
};
|
|
4688
|
+
}
|
|
4689
|
+
};
|
|
4690
|
+
IntentRegistrySensor = __decorateClass([
|
|
4691
|
+
Injectable19(),
|
|
4692
|
+
Sensor({ phase: "POST_DECODE" })
|
|
4693
|
+
], IntentRegistrySensor);
|
|
4694
|
+
|
|
4695
|
+
// src/sensors/proof-presence.sensor.ts
|
|
4696
|
+
import { Injectable as Injectable20 } from "@nestjs/common";
|
|
4697
|
+
var ProofPresenceSensor = class {
|
|
4698
|
+
constructor() {
|
|
4699
|
+
this.name = "ProofPresenceSensor";
|
|
4700
|
+
this.order = BAND.IDENTITY + 30;
|
|
4701
|
+
}
|
|
4702
|
+
supports(input) {
|
|
4703
|
+
return !!input.profile && !!input.visibility;
|
|
4704
|
+
}
|
|
4705
|
+
async run(input) {
|
|
4706
|
+
const validatedInput = ProofPresenceInputZ.safeParse(input);
|
|
4707
|
+
if (!validatedInput.success) {
|
|
4708
|
+
throw new AxisError(
|
|
4709
|
+
"SENSOR_INVALID_INPUT",
|
|
4710
|
+
`Input validation failed: ${validatedInput.error.message}`,
|
|
4711
|
+
400
|
|
4712
|
+
);
|
|
4713
|
+
}
|
|
4714
|
+
const {
|
|
4715
|
+
visibility,
|
|
4716
|
+
requiredProof,
|
|
4717
|
+
hasCapsule,
|
|
4718
|
+
hasPassportSignature,
|
|
4719
|
+
profile,
|
|
4720
|
+
intent
|
|
4721
|
+
} = validatedInput.data;
|
|
4722
|
+
if (visibility === "PUBLIC") {
|
|
4723
|
+
return { action: "ALLOW" };
|
|
4724
|
+
}
|
|
4725
|
+
if (requiredProof.includes("NONE")) {
|
|
4726
|
+
return { action: "ALLOW" };
|
|
4727
|
+
}
|
|
4728
|
+
const hasCapsuleProof = requiredProof.includes("CAPSULE") && hasCapsule;
|
|
4729
|
+
const hasPassportProof = requiredProof.includes("PASSPORT") && hasPassportSignature;
|
|
4730
|
+
const hasNodeProof = requiredProof.includes("MTLS") && profile === "NODE";
|
|
4731
|
+
const satisfied = hasCapsuleProof || hasPassportProof || hasNodeProof;
|
|
4732
|
+
if (!satisfied) {
|
|
4733
|
+
throw new AxisError(
|
|
4734
|
+
"SENSOR_PROOF_REQUIRED",
|
|
4735
|
+
`Proof required for guarded intent: ${intent}`,
|
|
4736
|
+
403
|
|
4737
|
+
);
|
|
4738
|
+
}
|
|
4739
|
+
return { action: "ALLOW" };
|
|
4740
|
+
}
|
|
4741
|
+
};
|
|
4742
|
+
ProofPresenceSensor = __decorateClass([
|
|
4743
|
+
Sensor(),
|
|
4744
|
+
Injectable20()
|
|
4745
|
+
], ProofPresenceSensor);
|
|
4746
|
+
|
|
4747
|
+
// src/sensors/protocol-strict.sensor.ts
|
|
4748
|
+
import { Injectable as Injectable21, Logger as Logger11 } from "@nestjs/common";
|
|
4749
|
+
var VALID_FLAGS = [
|
|
4750
|
+
0,
|
|
4751
|
+
// No flags
|
|
4752
|
+
FLAG_BODY_TLV,
|
|
4753
|
+
// Body contains TLVs
|
|
4754
|
+
FLAG_CHAIN_REQ,
|
|
4755
|
+
// Requires receipt chaining
|
|
4756
|
+
FLAG_HAS_WITNESS,
|
|
4757
|
+
// Has witness signatures
|
|
4758
|
+
FLAG_BODY_TLV | FLAG_CHAIN_REQ,
|
|
4759
|
+
FLAG_BODY_TLV | FLAG_HAS_WITNESS,
|
|
4760
|
+
FLAG_CHAIN_REQ | FLAG_HAS_WITNESS,
|
|
4761
|
+
FLAG_BODY_TLV | FLAG_CHAIN_REQ | FLAG_HAS_WITNESS
|
|
4762
|
+
];
|
|
4763
|
+
var ProtocolStrictSensor = class {
|
|
4764
|
+
constructor(config) {
|
|
4765
|
+
this.config = config;
|
|
4766
|
+
this.logger = new Logger11(ProtocolStrictSensor.name);
|
|
4767
|
+
/** Sensor identifier for logging and registry */
|
|
4768
|
+
this.name = "ProtocolStrictSensor";
|
|
4769
|
+
/**
|
|
4770
|
+
* Execution order - FIRST sensor in the chain
|
|
4771
|
+
*
|
|
4772
|
+
* Order 10 ensures:
|
|
4773
|
+
* - Runs before any other processing
|
|
4774
|
+
* - Invalid frames rejected immediately
|
|
4775
|
+
* - Protects all downstream sensors from malformed input
|
|
4776
|
+
*/
|
|
4777
|
+
this.order = BAND.WIRE + 10;
|
|
4778
|
+
this.protocolMagic = AXIS_MAGIC;
|
|
4779
|
+
this.protocolVersion = AXIS_VERSION;
|
|
4780
|
+
}
|
|
4781
|
+
/**
|
|
4782
|
+
* Static validation for streaming middleware (Fast Check)
|
|
4783
|
+
*/
|
|
4784
|
+
static validateMagic(chunk, expected) {
|
|
4785
|
+
if (chunk.length < expected.length) return { valid: true };
|
|
4786
|
+
const actual = chunk.subarray(0, expected.length);
|
|
4787
|
+
const valid = Buffer.from(actual).equals(Buffer.from(expected));
|
|
4788
|
+
return {
|
|
4789
|
+
valid,
|
|
4790
|
+
actual: valid ? void 0 : new TextDecoder().decode(actual)
|
|
4791
|
+
};
|
|
4792
|
+
}
|
|
4793
|
+
static validateVersion(version, expected) {
|
|
4794
|
+
return version === expected;
|
|
4795
|
+
}
|
|
4796
|
+
/**
|
|
4797
|
+
* Lifecycle hook: Registers this sensor in the chain on module initialization.
|
|
4798
|
+
*/
|
|
4799
|
+
onModuleInit() {
|
|
4800
|
+
const magicStr = this.config.get("AXIS_PROTOCOL_MAGIC");
|
|
4801
|
+
this.protocolMagic = magicStr ? Buffer.from(magicStr, "ascii") : AXIS_MAGIC;
|
|
4802
|
+
this.protocolVersion = this.config.get("AXIS_PROTOCOL_VERSION") || AXIS_VERSION;
|
|
4803
|
+
}
|
|
4804
|
+
/**
|
|
4805
|
+
* Evaluate protocol strictness
|
|
4806
|
+
*/
|
|
4807
|
+
async run(input) {
|
|
4808
|
+
const validatedInput = ProtocolStrictInputZ.safeParse(input);
|
|
4809
|
+
if (!validatedInput.success) {
|
|
4810
|
+
this.logger.error(
|
|
4811
|
+
`Invalid input: ${validatedInput.error.message}`,
|
|
4812
|
+
validatedInput.error.issues
|
|
4813
|
+
);
|
|
4814
|
+
return {
|
|
4815
|
+
action: "DENY",
|
|
4816
|
+
code: "INVALID_INPUT",
|
|
4817
|
+
reason: "Protocol validation input failed"
|
|
4818
|
+
};
|
|
4819
|
+
}
|
|
4820
|
+
const { contentType, peek } = validatedInput.data;
|
|
4821
|
+
const issues = [];
|
|
4822
|
+
if (peek.length >= 8) {
|
|
4823
|
+
const hex = Buffer.from(peek.subarray(0, 10)).toString("hex");
|
|
4824
|
+
this.logger.debug(`Raw Frame Header (Hex): ${hex} (IP: ${input.ip})`);
|
|
4825
|
+
}
|
|
4826
|
+
if (contentType !== void 0) {
|
|
4827
|
+
if (!this.isValidContentType(contentType)) {
|
|
4828
|
+
issues.push(`invalid_content_type:${contentType}`);
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
if (peek.length < 9) {
|
|
4832
|
+
return {
|
|
4833
|
+
action: "DENY",
|
|
4834
|
+
code: "FRAME_TOO_SHORT",
|
|
4835
|
+
reason: "Frame too short for protocol header"
|
|
4836
|
+
};
|
|
4837
|
+
}
|
|
4838
|
+
const magicCheck = ProtocolStrictSensor.validateMagic(
|
|
4839
|
+
peek,
|
|
4840
|
+
this.protocolMagic
|
|
4841
|
+
);
|
|
4842
|
+
if (!magicCheck.valid) {
|
|
4843
|
+
return {
|
|
4844
|
+
action: "DENY",
|
|
4845
|
+
code: "INVALID_MAGIC",
|
|
4846
|
+
reason: `Expected ${new TextDecoder().decode(this.protocolMagic)} magic, got ${magicCheck.actual}`
|
|
4847
|
+
};
|
|
4848
|
+
}
|
|
4849
|
+
const version = peek[5];
|
|
4850
|
+
if (!ProtocolStrictSensor.validateVersion(version, this.protocolVersion)) {
|
|
4851
|
+
issues.push(`unsupported_version:${version}`);
|
|
4852
|
+
}
|
|
4853
|
+
const flags = peek[6];
|
|
4854
|
+
if (!this.isValidFlags(flags)) {
|
|
4855
|
+
issues.push(`invalid_flags:0x${flags.toString(16)}`);
|
|
4856
|
+
}
|
|
4857
|
+
if (peek.length >= 10) {
|
|
4858
|
+
const lengthCheck = this.checkVarintEncoding(peek.subarray(7));
|
|
4859
|
+
if (!lengthCheck.valid) {
|
|
4860
|
+
issues.push(`non_minimal_varint:${lengthCheck.reason}`);
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
if (peek.length >= 20) {
|
|
4864
|
+
const tlvCheck = this.checkTLVOrdering(peek);
|
|
4865
|
+
if (!tlvCheck.valid) {
|
|
4866
|
+
issues.push(`tlv_not_canonical:${tlvCheck.reason}`);
|
|
4867
|
+
}
|
|
4868
|
+
const hasClientVersion = await this.checkForClientVersion(peek);
|
|
4869
|
+
if (!hasClientVersion) {
|
|
4870
|
+
issues.push("missing_client_version");
|
|
4871
|
+
}
|
|
4872
|
+
}
|
|
4873
|
+
if (issues.length > 0) {
|
|
4874
|
+
const critical = issues.some(
|
|
4875
|
+
(i) => i.startsWith("invalid_magic") || i.startsWith("unsupported_version")
|
|
4876
|
+
);
|
|
4877
|
+
if (critical) {
|
|
4878
|
+
return {
|
|
4879
|
+
action: "DENY",
|
|
4880
|
+
code: "PROTOCOL_VIOLATION",
|
|
4881
|
+
reason: issues.join(", ")
|
|
4882
|
+
};
|
|
4883
|
+
}
|
|
4884
|
+
this.logger.warn(
|
|
4885
|
+
`Protocol issues from ${input.ip}: ${issues.join(", ")}`
|
|
4886
|
+
);
|
|
4887
|
+
return {
|
|
4888
|
+
action: "FLAG",
|
|
4889
|
+
scoreDelta: -issues.length * 2,
|
|
4890
|
+
reasons: issues
|
|
4891
|
+
};
|
|
4892
|
+
}
|
|
4893
|
+
return { action: "ALLOW" };
|
|
4894
|
+
}
|
|
4895
|
+
/**
|
|
4896
|
+
* Compare two buffers for equality
|
|
4897
|
+
*/
|
|
4898
|
+
buffersEqual(a, b) {
|
|
4899
|
+
if (a.length !== b.length) return false;
|
|
4900
|
+
for (let i = 0; i < a.length; i++) {
|
|
4901
|
+
if (a[i] !== b[i]) return false;
|
|
4902
|
+
}
|
|
4903
|
+
return true;
|
|
4904
|
+
}
|
|
4905
|
+
/**
|
|
4906
|
+
* Check if Content-Type is valid for AXIS
|
|
4907
|
+
*/
|
|
4908
|
+
isValidContentType(contentType) {
|
|
4909
|
+
const valid = [
|
|
4910
|
+
"application/axis-bin",
|
|
4911
|
+
"application/octet-stream",
|
|
4912
|
+
"application/x-axis"
|
|
4913
|
+
];
|
|
4914
|
+
return valid.some((v) => contentType.toLowerCase().includes(v));
|
|
4915
|
+
}
|
|
4916
|
+
/**
|
|
4917
|
+
* Check if flags are a valid combination
|
|
4918
|
+
*/
|
|
4919
|
+
isValidFlags(flags) {
|
|
4920
|
+
return VALID_FLAGS.includes(flags);
|
|
4921
|
+
}
|
|
4922
|
+
/**
|
|
4923
|
+
* Check varint encoding is minimal (no leading zeros)
|
|
4924
|
+
*/
|
|
4925
|
+
checkVarintEncoding(data) {
|
|
4926
|
+
try {
|
|
4927
|
+
const { value, length: bytesRead } = decodeVarint(data, 0);
|
|
4928
|
+
if (value < 128 && bytesRead > 1) {
|
|
4929
|
+
return { valid: false, reason: "non-minimal-small-value" };
|
|
4930
|
+
}
|
|
4931
|
+
if (value < 16384 && bytesRead > 2) {
|
|
4932
|
+
return { valid: false, reason: "non-minimal-medium-value" };
|
|
4933
|
+
}
|
|
4934
|
+
return { valid: true };
|
|
4935
|
+
} catch {
|
|
4936
|
+
return { valid: false, reason: "varint-decode-error" };
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
/**
|
|
4940
|
+
* Check TLV ordering is canonical (sorted by type, no duplicates)
|
|
4941
|
+
*/
|
|
4942
|
+
checkTLVOrdering(data) {
|
|
4943
|
+
try {
|
|
4944
|
+
let offset = 7;
|
|
4945
|
+
const { value: hdrLen, length: hdrBytes } = decodeVarint(data, offset);
|
|
4946
|
+
offset += hdrBytes;
|
|
4947
|
+
const { length: bodyBytes } = decodeVarint(data, offset);
|
|
4948
|
+
offset += bodyBytes;
|
|
4949
|
+
const { length: sigBytes } = decodeVarint(data, offset);
|
|
4950
|
+
offset += sigBytes;
|
|
4951
|
+
const hdrStart = offset;
|
|
4952
|
+
const hdrEnd = hdrStart + Number(hdrLen);
|
|
4953
|
+
if (hdrEnd > data.length) {
|
|
4954
|
+
return { valid: true };
|
|
4955
|
+
}
|
|
4956
|
+
let lastType = -1;
|
|
4957
|
+
let pos = hdrStart;
|
|
4958
|
+
while (pos < hdrEnd && pos < data.length - 2) {
|
|
4959
|
+
const { value: type, length: typeBytes } = decodeVarint(data, pos);
|
|
4960
|
+
pos += typeBytes;
|
|
4961
|
+
if (pos >= hdrEnd) break;
|
|
4962
|
+
const { value: len, length: lenBytes } = decodeVarint(data, pos);
|
|
4963
|
+
pos += lenBytes;
|
|
4964
|
+
if (Number(type) <= lastType) {
|
|
4965
|
+
return {
|
|
4966
|
+
valid: false,
|
|
4967
|
+
reason: `type-${type}-after-${lastType}`
|
|
4968
|
+
};
|
|
4969
|
+
}
|
|
4970
|
+
lastType = Number(type);
|
|
4971
|
+
pos += Number(len);
|
|
4972
|
+
}
|
|
4973
|
+
return { valid: true };
|
|
4974
|
+
} catch {
|
|
4975
|
+
return { valid: true };
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4978
|
+
/**
|
|
4979
|
+
* Check if TLV 100 (Client Version) exists in the headers
|
|
4980
|
+
*/
|
|
4981
|
+
async checkForClientVersion(data) {
|
|
4982
|
+
try {
|
|
4983
|
+
let offset = 7;
|
|
4984
|
+
const { value: hdrLen, length: hdrBytes } = decodeVarint(data, offset);
|
|
4985
|
+
offset += hdrBytes;
|
|
4986
|
+
const { length: bodyBytes } = decodeVarint(data, offset);
|
|
4987
|
+
offset += bodyBytes;
|
|
4988
|
+
const { length: sigBytes } = decodeVarint(data, offset);
|
|
4989
|
+
offset += sigBytes;
|
|
4990
|
+
const hdrEnd = offset + Number(hdrLen);
|
|
4991
|
+
let pos = offset;
|
|
4992
|
+
while (pos < hdrEnd && pos < data.length) {
|
|
4993
|
+
const { value: type, length: typeBytes } = decodeVarint(data, pos);
|
|
4994
|
+
pos += typeBytes;
|
|
4995
|
+
const { length: lenBytes } = decodeVarint(data, pos);
|
|
4996
|
+
pos += lenBytes;
|
|
4997
|
+
const { value: valLen, length: valLenBytes } = decodeVarint(
|
|
4998
|
+
data,
|
|
4999
|
+
pos - lenBytes
|
|
5000
|
+
);
|
|
5001
|
+
}
|
|
5002
|
+
pos = offset;
|
|
5003
|
+
while (pos < hdrEnd && pos < data.length) {
|
|
5004
|
+
const t = decodeVarint(data, pos);
|
|
5005
|
+
pos += t.length;
|
|
5006
|
+
const l = decodeVarint(data, pos);
|
|
5007
|
+
pos += l.length;
|
|
5008
|
+
if (t.value === 100) return true;
|
|
5009
|
+
pos += Number(l.value);
|
|
5010
|
+
}
|
|
5011
|
+
return false;
|
|
5012
|
+
} catch {
|
|
5013
|
+
return false;
|
|
5014
|
+
}
|
|
5015
|
+
}
|
|
5016
|
+
};
|
|
5017
|
+
ProtocolStrictSensor = __decorateClass([
|
|
5018
|
+
Sensor({ phase: "PRE_DECODE" }),
|
|
5019
|
+
Injectable21()
|
|
5020
|
+
], ProtocolStrictSensor);
|
|
5021
|
+
|
|
5022
|
+
// src/sensors/receipt-policy.sensor.ts
|
|
5023
|
+
import { Injectable as Injectable22 } from "@nestjs/common";
|
|
5024
|
+
var ReceiptPolicySensor = class {
|
|
5025
|
+
constructor() {
|
|
5026
|
+
this.name = "ReceiptPolicySensor";
|
|
5027
|
+
this.order = BAND.BUSINESS + 20;
|
|
5028
|
+
}
|
|
5029
|
+
supports() {
|
|
5030
|
+
return true;
|
|
5031
|
+
}
|
|
5032
|
+
async run() {
|
|
5033
|
+
return { action: "ALLOW" };
|
|
5034
|
+
}
|
|
5035
|
+
};
|
|
5036
|
+
ReceiptPolicySensor = __decorateClass([
|
|
5037
|
+
Injectable22(),
|
|
5038
|
+
Sensor()
|
|
5039
|
+
], ReceiptPolicySensor);
|
|
5040
|
+
|
|
5041
|
+
// src/sensors/schema-validation.sensor.ts
|
|
5042
|
+
import { Injectable as Injectable23 } from "@nestjs/common";
|
|
5043
|
+
function readU64be(b) {
|
|
5044
|
+
if (b.length !== 8)
|
|
5045
|
+
throw new AxisError("SCHEMA_TYPE_MISMATCH", "u64 must be 8 bytes", 400);
|
|
5046
|
+
let x = 0n;
|
|
5047
|
+
for (const by of b) x = x << 8n | BigInt(by);
|
|
5048
|
+
return x;
|
|
5049
|
+
}
|
|
5050
|
+
var SchemaValidationSensor = class {
|
|
5051
|
+
constructor() {
|
|
5052
|
+
/** Sensor identifier for logging and registry */
|
|
5053
|
+
this.name = "SchemaValidationSensor";
|
|
5054
|
+
/**
|
|
5055
|
+
* Execution order - runs late in the pipeline
|
|
5056
|
+
*
|
|
5057
|
+
* Order 170 ensures:
|
|
5058
|
+
* - All authentication complete
|
|
5059
|
+
* - All policy checks complete
|
|
5060
|
+
* - Data validated before handler execution
|
|
5061
|
+
*/
|
|
5062
|
+
this.order = BAND.CONTENT + 35;
|
|
5063
|
+
}
|
|
5064
|
+
/**
|
|
5065
|
+
* Determines if this sensor should process the given input.
|
|
5066
|
+
*
|
|
5067
|
+
* Only activates when a schema is provided for the intent (post-decode phase).
|
|
5068
|
+
*
|
|
5069
|
+
* @param {any} input - Sensor input
|
|
5070
|
+
* @returns {boolean} True if schema exists in metadata
|
|
5071
|
+
*/
|
|
5072
|
+
supports(input) {
|
|
5073
|
+
return !!input.metadata?.schema;
|
|
5074
|
+
}
|
|
5075
|
+
/**
|
|
5076
|
+
* Validates TLV fields against the schema definition.
|
|
5077
|
+
*
|
|
5078
|
+
* **Validation Steps:**
|
|
5079
|
+
* 1. Validate the schema itself using Zod
|
|
5080
|
+
* 2. Iterate through each field definition
|
|
5081
|
+
* 3. Check required fields are present
|
|
5082
|
+
* 4. Validate size limits (maxLen)
|
|
5083
|
+
* 5. Validate type-specific rules
|
|
5084
|
+
*
|
|
5085
|
+
* @param {any} input - Standard SensorInput
|
|
5086
|
+
* @returns {{ action: 'ALLOW' } | { action: 'DENY', code: string, reason: string }} Decision
|
|
5087
|
+
*/
|
|
5088
|
+
async run(input) {
|
|
5089
|
+
const schema = input.metadata?.schema;
|
|
5090
|
+
const headerTLVs = input.headerTLVs;
|
|
5091
|
+
const bodyTLVs = input.bodyTLVs;
|
|
5092
|
+
if (!schema) {
|
|
5093
|
+
return { action: "ALLOW" };
|
|
5094
|
+
}
|
|
5095
|
+
const validatedSchema = IntentSchemaZ.safeParse(schema);
|
|
5096
|
+
if (!validatedSchema.success) {
|
|
5097
|
+
return {
|
|
5098
|
+
action: "DENY",
|
|
5099
|
+
code: "SCHEMA_INVALID",
|
|
5100
|
+
reason: `Schema validation failed: ${validatedSchema.error.message}`
|
|
5101
|
+
};
|
|
5102
|
+
}
|
|
5103
|
+
try {
|
|
5104
|
+
for (const field of schema.fields) {
|
|
5105
|
+
const scope = field.scope ?? "body";
|
|
5106
|
+
const map3 = scope === "header" ? headerTLVs : bodyTLVs;
|
|
5107
|
+
const val = map3?.get(field.tlv);
|
|
5108
|
+
if (field.required && !val) {
|
|
5109
|
+
throw new AxisError(
|
|
5110
|
+
"SCHEMA_FIELD_MISSING",
|
|
5111
|
+
`Missing required field: ${field.name} (TLV ${field.tlv})`,
|
|
5112
|
+
400
|
|
5113
|
+
);
|
|
5114
|
+
}
|
|
5115
|
+
if (!val) continue;
|
|
5116
|
+
if (typeof field.maxLen === "number" && val.length > field.maxLen) {
|
|
5117
|
+
throw new AxisError(
|
|
5118
|
+
"SCHEMA_LIMIT_EXCEEDED",
|
|
5119
|
+
`Field ${field.name} too large (${val.length} > ${field.maxLen})`,
|
|
5120
|
+
413
|
|
5121
|
+
// Payload Too Large
|
|
5122
|
+
);
|
|
5123
|
+
}
|
|
5124
|
+
switch (field.kind) {
|
|
5125
|
+
case "utf8":
|
|
5126
|
+
try {
|
|
5127
|
+
new TextDecoder("utf-8", { fatal: true }).decode(val);
|
|
5128
|
+
} catch {
|
|
5129
|
+
throw new AxisError(
|
|
5130
|
+
"SCHEMA_TYPE_MISMATCH",
|
|
5131
|
+
`Invalid UTF-8 in ${field.name}`,
|
|
5132
|
+
400
|
|
5133
|
+
);
|
|
5134
|
+
}
|
|
5135
|
+
break;
|
|
5136
|
+
case "bool":
|
|
5137
|
+
if (val.length !== 1 || val[0] !== 0 && val[0] !== 1) {
|
|
5138
|
+
throw new AxisError(
|
|
5139
|
+
"SCHEMA_TYPE_MISMATCH",
|
|
5140
|
+
`Invalid bool: ${field.name}`,
|
|
5141
|
+
400
|
|
5142
|
+
);
|
|
5143
|
+
}
|
|
5144
|
+
break;
|
|
5145
|
+
case "u64": {
|
|
5146
|
+
const x = readU64be(val);
|
|
5147
|
+
if (field.max) {
|
|
5148
|
+
const mx = BigInt(field.max);
|
|
5149
|
+
if (x > mx) {
|
|
5150
|
+
throw new AxisError(
|
|
5151
|
+
"SCHEMA_LIMIT_EXCEEDED",
|
|
5152
|
+
`u64 ${field.name} exceeds max (${x} > ${mx})`,
|
|
5153
|
+
400
|
|
5154
|
+
);
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
break;
|
|
5158
|
+
}
|
|
5159
|
+
case "bytes16":
|
|
5160
|
+
if (val.length !== 16) {
|
|
5161
|
+
throw new AxisError(
|
|
5162
|
+
"SCHEMA_TYPE_MISMATCH",
|
|
5163
|
+
`bytes16 required for ${field.name}`,
|
|
5164
|
+
400
|
|
5165
|
+
);
|
|
5166
|
+
}
|
|
5167
|
+
break;
|
|
5168
|
+
case "bytes":
|
|
5169
|
+
break;
|
|
5170
|
+
case "obj":
|
|
5171
|
+
case "arr":
|
|
5172
|
+
break;
|
|
5173
|
+
default:
|
|
5174
|
+
throw new AxisError(
|
|
5175
|
+
"SCHEMA_TYPE_MISMATCH",
|
|
5176
|
+
`Unknown schema kind: ${field.kind}`,
|
|
5177
|
+
500
|
|
5178
|
+
);
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
const validators = input.metadata?.validators;
|
|
5182
|
+
if (validators && validators.size > 0) {
|
|
5183
|
+
for (const field of schema.fields) {
|
|
5184
|
+
const fns = validators.get(field.tlv);
|
|
5185
|
+
if (!fns || fns.length === 0) continue;
|
|
5186
|
+
const scope = field.scope ?? "body";
|
|
5187
|
+
const map3 = scope === "header" ? headerTLVs : bodyTLVs;
|
|
5188
|
+
const val = map3?.get(field.tlv);
|
|
5189
|
+
if (!val) continue;
|
|
5190
|
+
for (const fn of fns) {
|
|
5191
|
+
const error = fn(val, field.name);
|
|
5192
|
+
if (error) {
|
|
5193
|
+
throw new AxisError(
|
|
5194
|
+
"SCHEMA_VALIDATION_FAILED",
|
|
5195
|
+
`${field.name} (TLV ${field.tlv}): ${error}`,
|
|
5196
|
+
400
|
|
5197
|
+
);
|
|
5198
|
+
}
|
|
5199
|
+
}
|
|
5200
|
+
}
|
|
5201
|
+
}
|
|
5202
|
+
} catch (err) {
|
|
5203
|
+
if (err instanceof AxisError) {
|
|
5204
|
+
return {
|
|
5205
|
+
action: "DENY",
|
|
5206
|
+
code: err.code,
|
|
5207
|
+
reason: err.message
|
|
5208
|
+
};
|
|
5209
|
+
}
|
|
5210
|
+
throw err;
|
|
5211
|
+
}
|
|
5212
|
+
return { action: "ALLOW" };
|
|
5213
|
+
}
|
|
5214
|
+
};
|
|
5215
|
+
SchemaValidationSensor = __decorateClass([
|
|
5216
|
+
Sensor(),
|
|
5217
|
+
Injectable23()
|
|
5218
|
+
], SchemaValidationSensor);
|
|
5219
|
+
|
|
5220
|
+
// src/sensors/stream-scope.sensor.ts
|
|
5221
|
+
import { Injectable as Injectable24 } from "@nestjs/common";
|
|
5222
|
+
var StreamScopeSensor = class {
|
|
5223
|
+
constructor() {
|
|
5224
|
+
/** Sensor identifier */
|
|
5225
|
+
this.name = "StreamScopeSensor";
|
|
5226
|
+
/**
|
|
5227
|
+
* Execution order - near handler execution
|
|
5228
|
+
*
|
|
5229
|
+
* Order 200 ensures:
|
|
5230
|
+
* - All authentication complete
|
|
5231
|
+
* - All policy checks complete
|
|
5232
|
+
* - Stream-specific check right before subscription
|
|
5233
|
+
*/
|
|
5234
|
+
this.order = BAND.BUSINESS + 0;
|
|
5235
|
+
}
|
|
5236
|
+
/**
|
|
5237
|
+
* Determines if this sensor should process the given input.
|
|
5238
|
+
*
|
|
5239
|
+
* Currently processes all inputs.
|
|
5240
|
+
*
|
|
5241
|
+
* @returns {boolean} Always true
|
|
5242
|
+
*/
|
|
5243
|
+
supports() {
|
|
5244
|
+
return true;
|
|
5245
|
+
}
|
|
5246
|
+
/**
|
|
5247
|
+
* Validates stream topic access permissions.
|
|
5248
|
+
*
|
|
5249
|
+
* **Current Implementation:** Stub that always allows.
|
|
5250
|
+
*
|
|
5251
|
+
* **TODO:** Full implementation should:
|
|
5252
|
+
* 1. Check if intent is stream.subscribe or stream.publish
|
|
5253
|
+
* 2. Extract topic from body TLVs
|
|
5254
|
+
* 3. Parse topic into owner/resource pattern
|
|
5255
|
+
* 4. Look up topic ACL from database/cache
|
|
5256
|
+
* 5. Check if actor has required permission (read/write)
|
|
5257
|
+
* 6. DENY if unauthorized
|
|
5258
|
+
*
|
|
5259
|
+
* @returns {Promise<SensorDecision>} ALLOW (stub implementation)
|
|
5260
|
+
*/
|
|
5261
|
+
async run() {
|
|
5262
|
+
return { action: "ALLOW" };
|
|
5263
|
+
}
|
|
5264
|
+
};
|
|
5265
|
+
StreamScopeSensor = __decorateClass([
|
|
5266
|
+
Sensor(),
|
|
5267
|
+
Injectable24()
|
|
5268
|
+
], StreamScopeSensor);
|
|
5269
|
+
|
|
5270
|
+
// src/sensors/tlv-parse.sensor.ts
|
|
5271
|
+
import { Injectable as Injectable25 } from "@nestjs/common";
|
|
5272
|
+
var TLVParseSensor = class {
|
|
5273
|
+
constructor() {
|
|
5274
|
+
this.name = "TLVParseSensor";
|
|
5275
|
+
this.order = BAND.CONTENT + 20;
|
|
5276
|
+
}
|
|
5277
|
+
supports(input) {
|
|
5278
|
+
return !!input.packet;
|
|
5279
|
+
}
|
|
5280
|
+
async run(input) {
|
|
5281
|
+
const packet = input.packet;
|
|
5282
|
+
if (!packet) return { action: "ALLOW" };
|
|
5283
|
+
const hdrBytes = packet.hdrBytes ?? packet.headerBytes;
|
|
5284
|
+
if (hdrBytes && hdrBytes.length > 0) {
|
|
5285
|
+
const result = this.validateCanonicalTLV(hdrBytes, "header");
|
|
5286
|
+
if (result) return result;
|
|
5287
|
+
}
|
|
5288
|
+
const bodyBytes = packet.bodyBytes ?? input.body;
|
|
5289
|
+
const bodyIsTlv = packet.flags !== void 0 ? (packet.flags & 1) !== 0 : false;
|
|
5290
|
+
const bodyProfile = input.metadata?.schema?.bodyProfile;
|
|
5291
|
+
const skipBody = bodyProfile === "RAW";
|
|
5292
|
+
if (!skipBody && bodyIsTlv && bodyBytes && bodyBytes.length > 0) {
|
|
5293
|
+
const result = this.validateCanonicalTLV(bodyBytes, "body");
|
|
5294
|
+
if (result) return result;
|
|
5295
|
+
}
|
|
5296
|
+
return { action: "ALLOW" };
|
|
5297
|
+
}
|
|
5298
|
+
/**
|
|
5299
|
+
* Validates a TLV buffer for canonical ordering, no duplicates,
|
|
5300
|
+
* valid bounds, and minimal varint encoding.
|
|
5301
|
+
*/
|
|
5302
|
+
validateCanonicalTLV(buf, section) {
|
|
5303
|
+
let offset = 0;
|
|
5304
|
+
let lastType = -1;
|
|
5305
|
+
let count = 0;
|
|
5306
|
+
const maxItems = 512;
|
|
5307
|
+
while (offset < buf.length) {
|
|
5308
|
+
if (count >= maxItems) {
|
|
5309
|
+
return {
|
|
5310
|
+
action: "DENY",
|
|
5311
|
+
code: "TLV_LIMIT",
|
|
5312
|
+
reason: `Too many TLVs in ${section}`
|
|
5313
|
+
};
|
|
5314
|
+
}
|
|
5315
|
+
let type;
|
|
5316
|
+
let typeLen;
|
|
5317
|
+
try {
|
|
5318
|
+
const r = decodeVarint(buf, offset);
|
|
5319
|
+
type = r.value;
|
|
5320
|
+
typeLen = r.length;
|
|
5321
|
+
} catch {
|
|
5322
|
+
return {
|
|
5323
|
+
action: "DENY",
|
|
5324
|
+
code: "TLV_PARSE_ERROR",
|
|
5325
|
+
reason: `Malformed type varint in ${section} at offset ${offset}`
|
|
5326
|
+
};
|
|
5327
|
+
}
|
|
5328
|
+
offset += typeLen;
|
|
5329
|
+
if (type <= 0) {
|
|
5330
|
+
return {
|
|
5331
|
+
action: "DENY",
|
|
5332
|
+
code: "TLV_INVALID_TAG",
|
|
5333
|
+
reason: `Invalid tag ${type} in ${section}`
|
|
5334
|
+
};
|
|
5335
|
+
}
|
|
5336
|
+
if (type <= lastType) {
|
|
5337
|
+
return {
|
|
5338
|
+
action: "DENY",
|
|
5339
|
+
code: "TLV_NOT_CANONICAL",
|
|
5340
|
+
reason: `Non-canonical tag order in ${section}: ${type} after ${lastType}`
|
|
5341
|
+
};
|
|
5342
|
+
}
|
|
5343
|
+
lastType = type;
|
|
5344
|
+
let len;
|
|
5345
|
+
let lenLen;
|
|
5346
|
+
try {
|
|
5347
|
+
const r = decodeVarint(buf, offset);
|
|
5348
|
+
len = r.value;
|
|
5349
|
+
lenLen = r.length;
|
|
5350
|
+
} catch {
|
|
5351
|
+
return {
|
|
5352
|
+
action: "DENY",
|
|
5353
|
+
code: "TLV_PARSE_ERROR",
|
|
5354
|
+
reason: `Malformed length varint in ${section}`
|
|
5355
|
+
};
|
|
5356
|
+
}
|
|
5357
|
+
offset += lenLen;
|
|
5358
|
+
if (offset + len > buf.length) {
|
|
5359
|
+
return {
|
|
5360
|
+
action: "DENY",
|
|
5361
|
+
code: "TLV_TRUNCATED",
|
|
5362
|
+
reason: `TLV value truncated in ${section}`
|
|
5363
|
+
};
|
|
5364
|
+
}
|
|
5365
|
+
offset += len;
|
|
5366
|
+
count++;
|
|
5367
|
+
}
|
|
5368
|
+
return null;
|
|
5369
|
+
}
|
|
5370
|
+
};
|
|
5371
|
+
TLVParseSensor = __decorateClass([
|
|
5372
|
+
Sensor(),
|
|
5373
|
+
Injectable25()
|
|
5374
|
+
], TLVParseSensor);
|
|
5375
|
+
|
|
5376
|
+
// src/sensors/varint-hardening.sensor.ts
|
|
5377
|
+
import { Injectable as Injectable26 } from "@nestjs/common";
|
|
5378
|
+
var VarintHardeningSensor = class {
|
|
5379
|
+
constructor() {
|
|
5380
|
+
/** Sensor identifier */
|
|
5381
|
+
this.name = "VarintHardeningSensor";
|
|
5382
|
+
/**
|
|
5383
|
+
* Execution order - early detection
|
|
5384
|
+
*
|
|
5385
|
+
* Order 40 ensures:
|
|
5386
|
+
* - After protocol magic check
|
|
5387
|
+
* - Before length-based parsing
|
|
5388
|
+
*/
|
|
5389
|
+
this.order = BAND.WIRE + 35;
|
|
5390
|
+
/** Maximum allowed bytes for a single varint */
|
|
5391
|
+
this.MAX_VARINT_BYTES = 5;
|
|
5392
|
+
}
|
|
5393
|
+
/**
|
|
5394
|
+
* Determines if this sensor should process the given input.
|
|
5395
|
+
*
|
|
5396
|
+
* Requires at least 7 bytes of peeked data.
|
|
5397
|
+
*
|
|
5398
|
+
* @param {SensorInput} input - Incoming request
|
|
5399
|
+
* @returns {boolean} True if sufficient peek data
|
|
5400
|
+
*/
|
|
5401
|
+
supports(input) {
|
|
5402
|
+
return !!input.peek && input.peek.length >= 7;
|
|
5403
|
+
}
|
|
5404
|
+
/**
|
|
5405
|
+
* Validates varint lengths in frame header.
|
|
5406
|
+
*
|
|
5407
|
+
* **Processing Flow:**
|
|
5408
|
+
* 1. Skip to varint section (offset 7)
|
|
5409
|
+
* 2. Scan for continuation bytes (MSB = 1)
|
|
5410
|
+
* 3. Count consecutive continuation bytes
|
|
5411
|
+
* 4. DENY if count exceeds MAX_VARINT_BYTES
|
|
5412
|
+
*
|
|
5413
|
+
* @param {SensorInput} input - Request with peek data
|
|
5414
|
+
* @returns {Promise<SensorDecision>} ALLOW or DENY based on varint length
|
|
5415
|
+
*/
|
|
5416
|
+
async run(input) {
|
|
5417
|
+
const peek = input.peek;
|
|
5418
|
+
const offset = 7;
|
|
5419
|
+
const maxOffset = Math.min(offset + 15, peek.length);
|
|
5420
|
+
let continuationCount = 0;
|
|
5421
|
+
for (let i = offset; i < maxOffset; i++) {
|
|
5422
|
+
if ((peek[i] & 128) !== 0) {
|
|
5423
|
+
continuationCount++;
|
|
5424
|
+
if (continuationCount > this.MAX_VARINT_BYTES) {
|
|
5425
|
+
return {
|
|
5426
|
+
action: "DENY",
|
|
5427
|
+
code: "VARINT_OVERFLOW",
|
|
5428
|
+
reason: `Varint exceeds ${this.MAX_VARINT_BYTES} bytes`
|
|
5429
|
+
};
|
|
5430
|
+
}
|
|
5431
|
+
} else {
|
|
5432
|
+
continuationCount = 0;
|
|
5433
|
+
}
|
|
5434
|
+
}
|
|
5435
|
+
return { action: "ALLOW" };
|
|
5436
|
+
}
|
|
5437
|
+
};
|
|
5438
|
+
VarintHardeningSensor = __decorateClass([
|
|
5439
|
+
Sensor({ phase: "PRE_DECODE" }),
|
|
5440
|
+
Injectable26()
|
|
5441
|
+
], VarintHardeningSensor);
|
|
5442
|
+
|
|
5443
|
+
// src/utils/index.ts
|
|
5444
|
+
var utils_exports = {};
|
|
5445
|
+
__export(utils_exports, {
|
|
5446
|
+
encodeAxisTlvDto: () => encodeAxisTlvDto
|
|
5447
|
+
});
|
|
5448
|
+
|
|
5449
|
+
// src/utils/axis-tlv-codec.ts
|
|
5450
|
+
function encodeAxisTlvDto(dtoClass, data) {
|
|
5451
|
+
const schema = extractDtoSchema(dtoClass);
|
|
5452
|
+
const items = schema.fields.flatMap((field) => {
|
|
5453
|
+
const value = data[field.name];
|
|
5454
|
+
if (value === void 0 || value === null) {
|
|
5455
|
+
if (field.required) {
|
|
5456
|
+
throw new Error(`Missing required TLV response field: ${field.name}`);
|
|
5457
|
+
}
|
|
5458
|
+
return [];
|
|
5459
|
+
}
|
|
5460
|
+
return [{ type: field.tag, value: encodeField(field, value) }];
|
|
5461
|
+
});
|
|
5462
|
+
return buildTLVs(items);
|
|
5463
|
+
}
|
|
5464
|
+
function encodeField(field, value) {
|
|
5465
|
+
switch (field.kind) {
|
|
5466
|
+
case "utf8":
|
|
5467
|
+
return Buffer.from(String(value), "utf8");
|
|
5468
|
+
case "u64":
|
|
5469
|
+
return encodeU64(value);
|
|
5470
|
+
case "bytes":
|
|
5471
|
+
case "bytes16":
|
|
5472
|
+
return toBuffer(value);
|
|
5473
|
+
case "bool":
|
|
5474
|
+
return Buffer.from([value ? 1 : 0]);
|
|
5475
|
+
case "obj":
|
|
5476
|
+
case "arr":
|
|
5477
|
+
return Buffer.from(JSON.stringify(value), "utf8");
|
|
5478
|
+
default:
|
|
5479
|
+
return toBuffer(value);
|
|
5480
|
+
}
|
|
5481
|
+
}
|
|
5482
|
+
function encodeU64(value) {
|
|
5483
|
+
const encoded = Buffer.alloc(8);
|
|
5484
|
+
encoded.writeBigUInt64BE(
|
|
5485
|
+
typeof value === "bigint" ? value : BigInt(value)
|
|
5486
|
+
);
|
|
5487
|
+
return encoded;
|
|
5488
|
+
}
|
|
5489
|
+
function toBuffer(value) {
|
|
5490
|
+
if (Buffer.isBuffer(value)) {
|
|
5491
|
+
return value;
|
|
5492
|
+
}
|
|
5493
|
+
if (value instanceof Uint8Array) {
|
|
5494
|
+
return Buffer.from(value);
|
|
5495
|
+
}
|
|
5496
|
+
if (typeof value === "string") {
|
|
5497
|
+
return Buffer.from(value, "utf8");
|
|
5498
|
+
}
|
|
5499
|
+
throw new Error(`Unsupported TLV bytes value: ${typeof value}`);
|
|
5500
|
+
}
|
|
2681
5501
|
export {
|
|
2682
5502
|
ATS1_HDR,
|
|
2683
5503
|
ATS1_SCHEMA,
|
|
@@ -2816,6 +5636,8 @@ export {
|
|
|
2816
5636
|
classifyIntent,
|
|
2817
5637
|
computeReceiptHash,
|
|
2818
5638
|
computeSignaturePayload,
|
|
5639
|
+
core_exports as core,
|
|
5640
|
+
crypto_exports as crypto,
|
|
2819
5641
|
decodeArray,
|
|
2820
5642
|
decodeAxis1Frame,
|
|
2821
5643
|
decodeFrame,
|
|
@@ -2823,11 +5645,13 @@ export {
|
|
|
2823
5645
|
decodeTLVs,
|
|
2824
5646
|
decodeTLVsList,
|
|
2825
5647
|
decodeVarint,
|
|
5648
|
+
decorators_exports as decorators,
|
|
2826
5649
|
encVarint,
|
|
2827
5650
|
encodeAxis1Frame,
|
|
2828
5651
|
encodeFrame,
|
|
2829
5652
|
encodeTLVs,
|
|
2830
5653
|
encodeVarint,
|
|
5654
|
+
engine_exports as engine,
|
|
2831
5655
|
extractDtoSchema,
|
|
2832
5656
|
generateEd25519KeyPair,
|
|
2833
5657
|
getSignTarget,
|
|
@@ -2835,6 +5659,7 @@ export {
|
|
|
2835
5659
|
isAdminOpcode,
|
|
2836
5660
|
isKnownOpcode,
|
|
2837
5661
|
isTimestampValid,
|
|
5662
|
+
loom_exports as loom,
|
|
2838
5663
|
nonce16,
|
|
2839
5664
|
normalizeSensorDecision,
|
|
2840
5665
|
packPasskeyLoginOptionsReq,
|
|
@@ -2844,7 +5669,10 @@ export {
|
|
|
2844
5669
|
packPasskeyRegisterOptionsReq,
|
|
2845
5670
|
parseScope,
|
|
2846
5671
|
resolveTimeout,
|
|
5672
|
+
schemas_exports as schemas,
|
|
5673
|
+
security_exports as security,
|
|
2847
5674
|
sensitivityName,
|
|
5675
|
+
sensors_exports as sensors,
|
|
2848
5676
|
sha256,
|
|
2849
5677
|
signFrame,
|
|
2850
5678
|
tlv,
|
|
@@ -2853,6 +5681,7 @@ export {
|
|
|
2853
5681
|
unpackPasskeyLoginVerifyReq,
|
|
2854
5682
|
unpackPasskeyRegisterOptionsReq,
|
|
2855
5683
|
utf8,
|
|
5684
|
+
utils_exports as utils,
|
|
2856
5685
|
validateFrameShape,
|
|
2857
5686
|
varintLength,
|
|
2858
5687
|
varintU,
|