@datacules/agent-identity-audit 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,109 @@
1
+ Datacules Agent Identity License — Version 1.0
2
+ Copyright (c) 2026 Datacules LLC. All rights reserved.
3
+
4
+ ─────────────────────────────────────────────────────────────────────────────
5
+ PREAMBLE
6
+ ─────────────────────────────────────────────────────────────────────────────
7
+
8
+ This software — Agent Identity & Auth Patterns — is developed and owned by
9
+ Datacules LLC. It is made available to the public as open-source software
10
+ under the permissive terms below.
11
+
12
+ Datacules LLC retains ownership and authorship of this software while
13
+ granting broad, royalty-free rights for anyone to use, copy, modify, and
14
+ distribute it — in commercial or non-commercial contexts — without requiring
15
+ that derivative works also become open source.
16
+
17
+ ─────────────────────────────────────────────────────────────────────────────
18
+ TERMS AND CONDITIONS
19
+ ─────────────────────────────────────────────────────────────────────────────
20
+
21
+ 1. PERMISSION TO USE
22
+
23
+ Permission is hereby granted, free of charge, to any person or
24
+ organization obtaining a copy of this software and associated
25
+ documentation files (the "Software"), to use, copy, modify, merge,
26
+ publish, distribute, sublicense, and/or sell copies of the Software,
27
+ and to permit persons to whom the Software is furnished to do so,
28
+ subject to the conditions below.
29
+
30
+ 2. ATTRIBUTION
31
+
32
+ a. Redistributions of source code must retain this copyright notice,
33
+ this list of conditions, and the disclaimer below.
34
+
35
+ b. Redistributions in binary form or as a product must reproduce this
36
+ copyright notice, this list of conditions, and the disclaimer in the
37
+ documentation and/or other materials provided with the distribution.
38
+
39
+ c. Neither the name "Datacules LLC" nor the names of its contributors
40
+ may be used to endorse or promote products derived from this Software
41
+ without prior written permission from Datacules LLC.
42
+
43
+ 3. COMMERCIAL USE
44
+
45
+ Use of this Software in commercial products, SaaS platforms, internal
46
+ enterprise tools, or any revenue-generating context is explicitly
47
+ permitted without royalty, fee, or additional licensing agreement,
48
+ provided that the conditions in Section 2 (Attribution) are met.
49
+
50
+ 4. NO COPYLEFT / NO VIRAL REQUIREMENT
51
+
52
+ This license does NOT require that derivative works, modifications,
53
+ or software that uses or embeds this Software be made open source.
54
+ You may incorporate this Software into proprietary or closed-source
55
+ products under your own license terms.
56
+
57
+ 5. MODIFICATIONS
58
+
59
+ Modified versions of the Software may be distributed under the same
60
+ terms as this license or under any other permissive open-source
61
+ license (e.g. MIT, Apache 2.0, BSD), provided that:
62
+
63
+ a. The original copyright notice of Datacules LLC is preserved.
64
+ b. Modifications are clearly documented and distinguished from the
65
+ original work.
66
+
67
+ 6. COMPATIBILITY
68
+
69
+ This license is compatible with other permissive open-source licenses
70
+ such as MIT, BSD 2-Clause, BSD 3-Clause, and Apache License 2.0. It
71
+ is also GPL-compatible — this Software may coexist with GPL-licensed
72
+ code, though this Software itself is not distributed under the GPL.
73
+
74
+ ─────────────────────────────────────────────────────────────────────────────
75
+ DISCLAIMER
76
+ ─────────────────────────────────────────────────────────────────────────────
77
+
78
+ THIS SOFTWARE IS PROVIDED BY DATACULES LLC AND CONTRIBUTORS "AS IS" AND
79
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
80
+ IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
81
+ AND NON-INFRINGEMENT ARE DISCLAIMED.
82
+
83
+ IN NO EVENT SHALL DATACULES LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
84
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
85
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
86
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
87
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
88
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
89
+ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
90
+
91
+ ─────────────────────────────────────────────────────────────────────────────
92
+ SUMMARY (non-binding)
93
+ ─────────────────────────────────────────────────────────────────────────────
94
+
95
+ ✔ Use freely — commercial, proprietary, or open-source projects
96
+ ✔ Modify and distribute with or without changes
97
+ ✔ Sell products built on this Software
98
+ ✔ No royalties or fees
99
+ ✔ No requirement to open-source your own code
100
+ ✔ Attribution to Datacules LLC required in source and binary distributions
101
+ ✗ Do not use "Datacules LLC" to endorse derived products without permission
102
+
103
+ ─────────────────────────────────────────────────────────────────────────────
104
+ CONTACT
105
+ ─────────────────────────────────────────────────────────────────────────────
106
+
107
+ Datacules LLC
108
+ For licensing enquiries: legal@datacules.com
109
+ Product: https://github.com/hvrcharon1/agent-identity
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.HashChainAuditLogger = exports.S3ChainAnchor = exports.StdoutChainAnchor = exports.CompositeAuditLogger = exports.SplunkAuditLogger = exports.DatadogAuditLogger = exports.WebhookAuditLogger = exports.ConsoleAuditLogger = void 0;
37
+ // ──────────────────────────────────────────────────────────────────────────
38
+ // Existing sinks
39
+ // ──────────────────────────────────────────────────────────────────────────
40
+ class ConsoleAuditLogger {
41
+ async log(entry) {
42
+ console.log('[agent-identity audit]', JSON.stringify(entry, null, 2));
43
+ }
44
+ }
45
+ exports.ConsoleAuditLogger = ConsoleAuditLogger;
46
+ class WebhookAuditLogger {
47
+ constructor(options) {
48
+ this.options = { secret: '', timeoutMs: 5000, silent: true, ...options };
49
+ }
50
+ async log(entry) {
51
+ const { url, secret, timeoutMs, silent } = this.options;
52
+ const headers = { 'Content-Type': 'application/json' };
53
+ if (secret)
54
+ headers['X-Webhook-Secret'] = secret;
55
+ const controller = new AbortController();
56
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
57
+ try {
58
+ await fetch(url, { method: 'POST', headers, body: JSON.stringify(entry), signal: controller.signal });
59
+ }
60
+ catch (err) {
61
+ if (!silent)
62
+ throw err;
63
+ console.warn('[agent-identity] WebhookAuditLogger failed:', err);
64
+ }
65
+ finally {
66
+ clearTimeout(timer);
67
+ }
68
+ }
69
+ }
70
+ exports.WebhookAuditLogger = WebhookAuditLogger;
71
+ class DatadogAuditLogger {
72
+ constructor(options) {
73
+ this.options = { service: 'agent-identity', site: 'datadoghq.com', silent: true, ...options };
74
+ }
75
+ async log(entry) {
76
+ const { apiKey, service, site, silent } = this.options;
77
+ try {
78
+ await fetch(`https://http-intake.logs.${site}/api/v2/logs`, {
79
+ method: 'POST',
80
+ headers: { 'DD-API-KEY': apiKey, 'Content-Type': 'application/json' },
81
+ body: JSON.stringify({ ddsource: 'agent-identity', service, message: JSON.stringify(entry) }),
82
+ });
83
+ }
84
+ catch (err) {
85
+ if (!silent)
86
+ throw err;
87
+ console.warn('[agent-identity] DatadogAuditLogger failed:', err);
88
+ }
89
+ }
90
+ }
91
+ exports.DatadogAuditLogger = DatadogAuditLogger;
92
+ class SplunkAuditLogger {
93
+ constructor(options) {
94
+ this.options = { sourcetype: 'agent_identity', silent: true, ...options };
95
+ }
96
+ async log(entry) {
97
+ const { hecUrl, token, sourcetype, silent } = this.options;
98
+ try {
99
+ await fetch(hecUrl, {
100
+ method: 'POST',
101
+ headers: { Authorization: `Splunk ${token}`, 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({ event: entry, sourcetype }),
103
+ });
104
+ }
105
+ catch (err) {
106
+ if (!silent)
107
+ throw err;
108
+ console.warn('[agent-identity] SplunkAuditLogger failed:', err);
109
+ }
110
+ }
111
+ }
112
+ exports.SplunkAuditLogger = SplunkAuditLogger;
113
+ class CompositeAuditLogger {
114
+ constructor(loggers) {
115
+ this.loggers = loggers;
116
+ }
117
+ async log(entry) {
118
+ await Promise.allSettled(this.loggers.map((l) => l.log(entry)));
119
+ }
120
+ }
121
+ exports.CompositeAuditLogger = CompositeAuditLogger;
122
+ /** Prints the chain root to stdout — suitable for piping to a CI artifact */
123
+ class StdoutChainAnchor {
124
+ async publish(rootHash, sequence, timestamp) {
125
+ console.log(`[agent-identity chain-anchor] seq=${sequence} root=${rootHash} ts=${timestamp}`);
126
+ }
127
+ }
128
+ exports.StdoutChainAnchor = StdoutChainAnchor;
129
+ class S3ChainAnchor {
130
+ constructor(opts) {
131
+ this.opts = opts;
132
+ }
133
+ async publish(rootHash, sequence, timestamp) {
134
+ const key = `agent-identity/chain-roots/${sequence}-${timestamp}.json`;
135
+ const body = JSON.stringify({ rootHash, sequence, timestamp, publishedAt: new Date().toISOString() });
136
+ // S3 PutObject — in production use @aws-sdk/client-s3
137
+ const url = `https://${this.opts.bucketName}.s3.${this.opts.region}.amazonaws.com/${key}`;
138
+ await fetch(url, {
139
+ method: 'PUT',
140
+ headers: { 'Content-Type': 'application/json', 'Content-Length': String(body.length) },
141
+ body,
142
+ }).catch((err) => console.warn('[S3ChainAnchor] publish failed:', err));
143
+ }
144
+ }
145
+ exports.S3ChainAnchor = S3ChainAnchor;
146
+ /**
147
+ * Wraps any AuditLogger with tamper-evident SHA-256 hash chaining.
148
+ *
149
+ * Each entry is hashed together with the hash of the previous entry.
150
+ * Any modification to a historical entry breaks the chain from that
151
+ * point forward — detectable by recomputing and comparing hashes.
152
+ *
153
+ * @example
154
+ * const logger = new HashChainAuditLogger({
155
+ * sink: new DatadogAuditLogger({ apiKey: process.env.DD_API_KEY! }),
156
+ * anchor: new S3ChainAnchor({ bucketName: 'my-audit-anchors', region: 'us-east-1' }),
157
+ * });
158
+ */
159
+ class HashChainAuditLogger {
160
+ constructor(opts) {
161
+ this.opts = opts;
162
+ this.sequence = 0;
163
+ this.previousHash = opts.seedHash ?? '0';
164
+ this.anchorEveryN = opts.anchorEveryN ?? 1000;
165
+ }
166
+ async log(entry) {
167
+ this.sequence += 1;
168
+ const entryHash = await this.sha256(JSON.stringify(entry));
169
+ const chainHash = await this.sha256(`${this.previousHash}:${entryHash}`);
170
+ const chained = {
171
+ ...entry,
172
+ entryHash,
173
+ previousHash: this.previousHash,
174
+ sequence: this.sequence,
175
+ };
176
+ this.previousHash = chainHash;
177
+ await this.opts.sink.log(chained);
178
+ if (this.opts.anchor && this.sequence % this.anchorEveryN === 0) {
179
+ await this.opts.anchor
180
+ .publish(chainHash, this.sequence, new Date().toISOString())
181
+ .catch(console.error);
182
+ }
183
+ }
184
+ /** Verify an ordered array of chained entries; returns result with first broken sequence if any */
185
+ async verify(entries) {
186
+ let prevHash = this.opts.seedHash ?? '0';
187
+ for (const entry of entries) {
188
+ const { entryHash, previousHash, sequence, ...data } = entry;
189
+ const expectedEntryHash = await this.sha256(JSON.stringify(data));
190
+ if (expectedEntryHash !== entryHash) {
191
+ return { valid: false, entriesChecked: sequence, brokenAt: sequence, error: `entry hash mismatch at sequence ${sequence}` };
192
+ }
193
+ if (previousHash !== prevHash) {
194
+ return { valid: false, entriesChecked: sequence, brokenAt: sequence, error: `chain break at sequence ${sequence}` };
195
+ }
196
+ prevHash = await this.sha256(`${previousHash}:${entryHash}`);
197
+ }
198
+ return {
199
+ valid: true,
200
+ entriesChecked: entries.length,
201
+ firstEntry: entries[0],
202
+ lastEntry: entries[entries.length - 1],
203
+ };
204
+ }
205
+ async sha256(data) {
206
+ if (typeof crypto !== 'undefined' && crypto.subtle) {
207
+ const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data));
208
+ return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, '0')).join('');
209
+ }
210
+ const { createHash } = await Promise.resolve().then(() => __importStar(require('crypto')));
211
+ return createHash('sha256').update(data).digest('hex');
212
+ }
213
+ }
214
+ exports.HashChainAuditLogger = HashChainAuditLogger;
215
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAUA,6EAA6E;AAC7E,iBAAiB;AACjB,6EAA6E;AAE7E,MAAa,kBAAkB;IAC7B,KAAK,CAAC,GAAG,CAAC,KAAoB;QAC5B,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACxE,CAAC;CACF;AAJD,gDAIC;AASD,MAAa,kBAAkB;IAE7B,YAAY,OAAkC;QAC5C,IAAI,CAAC,OAAO,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC;IAC3E,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,KAAoB;QAC5B,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC;QACxD,MAAM,OAAO,GAA2B,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;QAC/E,IAAI,MAAM;YAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG,MAAM,CAAC;QACjD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;QAC9D,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;QACxG,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM;gBAAE,MAAM,GAAG,CAAC;YACvB,OAAO,CAAC,IAAI,CAAC,6CAA6C,EAAE,GAAG,CAAC,CAAC;QACnE,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;CACF;AApBD,gDAoBC;AASD,MAAa,kBAAkB;IAE7B,YAAY,OAAkC;QAC5C,IAAI,CAAC,OAAO,GAAG,EAAE,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC;IAChG,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,KAAoB;QAC5B,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC;QACvD,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,4BAA4B,IAAI,cAAc,EAAE;gBAC1D,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBACrE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;aAC9F,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM;gBAAE,MAAM,GAAG,CAAC;YACvB,OAAO,CAAC,IAAI,CAAC,6CAA6C,EAAE,GAAG,CAAC,CAAC;QACnE,CAAC;IACH,CAAC;CACF;AAlBD,gDAkBC;AASD,MAAa,iBAAiB;IAE5B,YAAY,OAAiC;QAC3C,IAAI,CAAC,OAAO,GAAG,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC;IAC5E,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,KAAoB;QAC5B,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC;QAC3D,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,MAAM,EAAE;gBAClB,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBACjF,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;aACnD,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM;gBAAE,MAAM,GAAG,CAAC;YACvB,OAAO,CAAC,IAAI,CAAC,4CAA4C,EAAE,GAAG,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;CACF;AAlBD,8CAkBC;AAED,MAAa,oBAAoB;IAC/B,YAA6B,OAAsB;QAAtB,YAAO,GAAP,OAAO,CAAe;IAAG,CAAC;IACvD,KAAK,CAAC,GAAG,CAAC,KAAoB;QAC5B,MAAM,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAClE,CAAC;CACF;AALD,oDAKC;AA6BD,6EAA6E;AAC7E,MAAa,iBAAiB;IAC5B,KAAK,CAAC,OAAO,CAAC,QAAgB,EAAE,QAAgB,EAAE,SAAiB;QACjE,OAAO,CAAC,GAAG,CAAC,qCAAqC,QAAQ,SAAS,QAAQ,OAAO,SAAS,EAAE,CAAC,CAAC;IAChG,CAAC;CACF;AAJD,8CAIC;AAUD,MAAa,aAAa;IACxB,YAA6B,IAA0B;QAA1B,SAAI,GAAJ,IAAI,CAAsB;IAAG,CAAC;IAE3D,KAAK,CAAC,OAAO,CAAC,QAAgB,EAAE,QAAgB,EAAE,SAAiB;QACjE,MAAM,GAAG,GAAG,8BAA8B,QAAQ,IAAI,SAAS,OAAO,CAAC;QACvE,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QACtG,sDAAsD;QACtD,MAAM,GAAG,GAAG,WAAW,IAAI,CAAC,IAAI,CAAC,UAAU,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;QAC1F,MAAM,KAAK,CAAC,GAAG,EAAE;YACf,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;YACtF,IAAI;SACL,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,iCAAiC,EAAE,GAAG,CAAC,CAAC,CAAC;IAC1E,CAAC;CACF;AAdD,sCAcC;AAeD;;;;;;;;;;;;GAYG;AACH,MAAa,oBAAoB;IAK/B,YAA6B,IAAsB;QAAtB,SAAI,GAAJ,IAAI,CAAkB;QAH3C,aAAQ,GAAG,CAAC,CAAC;QAInB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,QAAQ,IAAI,GAAG,CAAC;QACzC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC;IAChD,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,KAAoB;QAC5B,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;QACnB,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3D,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,YAAY,IAAI,SAAS,EAAE,CAAC,CAAC;QAEzE,MAAM,OAAO,GAAyB;YACpC,GAAG,KAAK;YACR,SAAS;YACT,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,QAAQ,EAAE,IAAI,CAAC,QAAQ;SACxB,CAAC;QAEF,IAAI,CAAC,YAAY,GAAG,SAAS,CAAC;QAE9B,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAmC,CAAC,CAAC;QAE9D,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,YAAY,KAAK,CAAC,EAAE,CAAC;YAChE,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM;iBACnB,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;iBAC3D,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,mGAAmG;IACnG,KAAK,CAAC,MAAM,CAAC,OAA+B;QAC1C,IAAI,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,GAAG,CAAC;QACzC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC;YAC7D,MAAM,iBAAiB,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;YAClE,IAAI,iBAAiB,KAAK,SAAS,EAAE,CAAC;gBACpC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,mCAAmC,QAAQ,EAAE,EAAE,CAAC;YAC9H,CAAC;YACD,IAAI,YAAY,KAAK,QAAQ,EAAE,CAAC;gBAC9B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,2BAA2B,QAAQ,EAAE,EAAE,CAAC;YACtH,CAAC;YACD,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,YAAY,IAAI,SAAS,EAAE,CAAC,CAAC;QAC/D,CAAC;QACD,OAAO;YACL,KAAK,EAAE,IAAI;YACX,cAAc,EAAE,OAAO,CAAC,MAAM;YAC9B,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;YACtB,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;SACvC,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,MAAM,CAAC,IAAY;QAC/B,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YACnD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;YAClF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC9F,CAAC;QACD,MAAM,EAAE,UAAU,EAAE,GAAG,wDAAa,QAAQ,GAAC,CAAC;QAC9C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACzD,CAAC;CACF;AA/DD,oDA+DC"}
@@ -0,0 +1,171 @@
1
+ // ──────────────────────────────────────────────────────────────────────────
2
+ // Existing sinks
3
+ // ──────────────────────────────────────────────────────────────────────────
4
+ export class ConsoleAuditLogger {
5
+ async log(entry) {
6
+ console.log('[agent-identity audit]', JSON.stringify(entry, null, 2));
7
+ }
8
+ }
9
+ export class WebhookAuditLogger {
10
+ constructor(options) {
11
+ this.options = { secret: '', timeoutMs: 5000, silent: true, ...options };
12
+ }
13
+ async log(entry) {
14
+ const { url, secret, timeoutMs, silent } = this.options;
15
+ const headers = { 'Content-Type': 'application/json' };
16
+ if (secret)
17
+ headers['X-Webhook-Secret'] = secret;
18
+ const controller = new AbortController();
19
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
20
+ try {
21
+ await fetch(url, { method: 'POST', headers, body: JSON.stringify(entry), signal: controller.signal });
22
+ }
23
+ catch (err) {
24
+ if (!silent)
25
+ throw err;
26
+ console.warn('[agent-identity] WebhookAuditLogger failed:', err);
27
+ }
28
+ finally {
29
+ clearTimeout(timer);
30
+ }
31
+ }
32
+ }
33
+ export class DatadogAuditLogger {
34
+ constructor(options) {
35
+ this.options = { service: 'agent-identity', site: 'datadoghq.com', silent: true, ...options };
36
+ }
37
+ async log(entry) {
38
+ const { apiKey, service, site, silent } = this.options;
39
+ try {
40
+ await fetch(`https://http-intake.logs.${site}/api/v2/logs`, {
41
+ method: 'POST',
42
+ headers: { 'DD-API-KEY': apiKey, 'Content-Type': 'application/json' },
43
+ body: JSON.stringify({ ddsource: 'agent-identity', service, message: JSON.stringify(entry) }),
44
+ });
45
+ }
46
+ catch (err) {
47
+ if (!silent)
48
+ throw err;
49
+ console.warn('[agent-identity] DatadogAuditLogger failed:', err);
50
+ }
51
+ }
52
+ }
53
+ export class SplunkAuditLogger {
54
+ constructor(options) {
55
+ this.options = { sourcetype: 'agent_identity', silent: true, ...options };
56
+ }
57
+ async log(entry) {
58
+ const { hecUrl, token, sourcetype, silent } = this.options;
59
+ try {
60
+ await fetch(hecUrl, {
61
+ method: 'POST',
62
+ headers: { Authorization: `Splunk ${token}`, 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ event: entry, sourcetype }),
64
+ });
65
+ }
66
+ catch (err) {
67
+ if (!silent)
68
+ throw err;
69
+ console.warn('[agent-identity] SplunkAuditLogger failed:', err);
70
+ }
71
+ }
72
+ }
73
+ export class CompositeAuditLogger {
74
+ constructor(loggers) {
75
+ this.loggers = loggers;
76
+ }
77
+ async log(entry) {
78
+ await Promise.allSettled(this.loggers.map((l) => l.log(entry)));
79
+ }
80
+ }
81
+ /** Prints the chain root to stdout — suitable for piping to a CI artifact */
82
+ export class StdoutChainAnchor {
83
+ async publish(rootHash, sequence, timestamp) {
84
+ console.log(`[agent-identity chain-anchor] seq=${sequence} root=${rootHash} ts=${timestamp}`);
85
+ }
86
+ }
87
+ export class S3ChainAnchor {
88
+ constructor(opts) {
89
+ this.opts = opts;
90
+ }
91
+ async publish(rootHash, sequence, timestamp) {
92
+ const key = `agent-identity/chain-roots/${sequence}-${timestamp}.json`;
93
+ const body = JSON.stringify({ rootHash, sequence, timestamp, publishedAt: new Date().toISOString() });
94
+ // S3 PutObject — in production use @aws-sdk/client-s3
95
+ const url = `https://${this.opts.bucketName}.s3.${this.opts.region}.amazonaws.com/${key}`;
96
+ await fetch(url, {
97
+ method: 'PUT',
98
+ headers: { 'Content-Type': 'application/json', 'Content-Length': String(body.length) },
99
+ body,
100
+ }).catch((err) => console.warn('[S3ChainAnchor] publish failed:', err));
101
+ }
102
+ }
103
+ /**
104
+ * Wraps any AuditLogger with tamper-evident SHA-256 hash chaining.
105
+ *
106
+ * Each entry is hashed together with the hash of the previous entry.
107
+ * Any modification to a historical entry breaks the chain from that
108
+ * point forward — detectable by recomputing and comparing hashes.
109
+ *
110
+ * @example
111
+ * const logger = new HashChainAuditLogger({
112
+ * sink: new DatadogAuditLogger({ apiKey: process.env.DD_API_KEY! }),
113
+ * anchor: new S3ChainAnchor({ bucketName: 'my-audit-anchors', region: 'us-east-1' }),
114
+ * });
115
+ */
116
+ export class HashChainAuditLogger {
117
+ constructor(opts) {
118
+ this.opts = opts;
119
+ this.sequence = 0;
120
+ this.previousHash = opts.seedHash ?? '0';
121
+ this.anchorEveryN = opts.anchorEveryN ?? 1000;
122
+ }
123
+ async log(entry) {
124
+ this.sequence += 1;
125
+ const entryHash = await this.sha256(JSON.stringify(entry));
126
+ const chainHash = await this.sha256(`${this.previousHash}:${entryHash}`);
127
+ const chained = {
128
+ ...entry,
129
+ entryHash,
130
+ previousHash: this.previousHash,
131
+ sequence: this.sequence,
132
+ };
133
+ this.previousHash = chainHash;
134
+ await this.opts.sink.log(chained);
135
+ if (this.opts.anchor && this.sequence % this.anchorEveryN === 0) {
136
+ await this.opts.anchor
137
+ .publish(chainHash, this.sequence, new Date().toISOString())
138
+ .catch(console.error);
139
+ }
140
+ }
141
+ /** Verify an ordered array of chained entries; returns result with first broken sequence if any */
142
+ async verify(entries) {
143
+ let prevHash = this.opts.seedHash ?? '0';
144
+ for (const entry of entries) {
145
+ const { entryHash, previousHash, sequence, ...data } = entry;
146
+ const expectedEntryHash = await this.sha256(JSON.stringify(data));
147
+ if (expectedEntryHash !== entryHash) {
148
+ return { valid: false, entriesChecked: sequence, brokenAt: sequence, error: `entry hash mismatch at sequence ${sequence}` };
149
+ }
150
+ if (previousHash !== prevHash) {
151
+ return { valid: false, entriesChecked: sequence, brokenAt: sequence, error: `chain break at sequence ${sequence}` };
152
+ }
153
+ prevHash = await this.sha256(`${previousHash}:${entryHash}`);
154
+ }
155
+ return {
156
+ valid: true,
157
+ entriesChecked: entries.length,
158
+ firstEntry: entries[0],
159
+ lastEntry: entries[entries.length - 1],
160
+ };
161
+ }
162
+ async sha256(data) {
163
+ if (typeof crypto !== 'undefined' && crypto.subtle) {
164
+ const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data));
165
+ return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, '0')).join('');
166
+ }
167
+ const { createHash } = await import('crypto');
168
+ return createHash('sha256').update(data).digest('hex');
169
+ }
170
+ }
171
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * @datacules/agent-identity-audit — extended with hash-chain tamper-evident logging
3
+ *
4
+ * New in this version:
5
+ * HashChainAuditLogger — wraps any existing AuditLogger and appends a SHA-256
6
+ * hash chain to every entry. Detects tampering by recomputing the chain.
7
+ * ChainAnchor interface + built-in S3 and stdout anchors.
8
+ */
9
+ import type { AuditLogEntry, AuditLogger } from '@datacules/agent-identity';
10
+ export declare class ConsoleAuditLogger implements AuditLogger {
11
+ log(entry: AuditLogEntry): Promise<void>;
12
+ }
13
+ export interface WebhookAuditLoggerOptions {
14
+ url: string;
15
+ secret?: string;
16
+ timeoutMs?: number;
17
+ silent?: boolean;
18
+ }
19
+ export declare class WebhookAuditLogger implements AuditLogger {
20
+ private readonly options;
21
+ constructor(options: WebhookAuditLoggerOptions);
22
+ log(entry: AuditLogEntry): Promise<void>;
23
+ }
24
+ export interface DatadogAuditLoggerOptions {
25
+ apiKey: string;
26
+ service?: string;
27
+ site?: string;
28
+ silent?: boolean;
29
+ }
30
+ export declare class DatadogAuditLogger implements AuditLogger {
31
+ private readonly options;
32
+ constructor(options: DatadogAuditLoggerOptions);
33
+ log(entry: AuditLogEntry): Promise<void>;
34
+ }
35
+ export interface SplunkAuditLoggerOptions {
36
+ hecUrl: string;
37
+ token: string;
38
+ sourcetype?: string;
39
+ silent?: boolean;
40
+ }
41
+ export declare class SplunkAuditLogger implements AuditLogger {
42
+ private readonly options;
43
+ constructor(options: SplunkAuditLoggerOptions);
44
+ log(entry: AuditLogEntry): Promise<void>;
45
+ }
46
+ export declare class CompositeAuditLogger implements AuditLogger {
47
+ private readonly loggers;
48
+ constructor(loggers: AuditLogger[]);
49
+ log(entry: AuditLogEntry): Promise<void>;
50
+ }
51
+ export interface ChainedAuditLogEntry extends AuditLogEntry {
52
+ /** SHA-256 hash of this entry's data fields */
53
+ entryHash: string;
54
+ /** SHA-256 hash of the previous entry (or '0' for the first entry) */
55
+ previousHash: string;
56
+ /** Sequential position in the chain */
57
+ sequence: number;
58
+ }
59
+ export interface ChainVerificationResult {
60
+ valid: boolean;
61
+ entriesChecked: number;
62
+ firstEntry?: ChainedAuditLogEntry;
63
+ lastEntry?: ChainedAuditLogEntry;
64
+ brokenAt?: number;
65
+ error?: string;
66
+ }
67
+ export interface ChainAnchor {
68
+ /** Publish the chain root hash to an immutable external location */
69
+ publish(rootHash: string, sequence: number, timestamp: string): Promise<void>;
70
+ }
71
+ /** Prints the chain root to stdout — suitable for piping to a CI artifact */
72
+ export declare class StdoutChainAnchor implements ChainAnchor {
73
+ publish(rootHash: string, sequence: number, timestamp: string): Promise<void>;
74
+ }
75
+ /** Publishes the chain root hash to an S3 object with Object Lock enabled */
76
+ export interface S3ChainAnchorOptions {
77
+ bucketName: string;
78
+ region: string;
79
+ /** AWS credentials or SDK client — omit to use default credential chain */
80
+ credentialsJson?: string;
81
+ }
82
+ export declare class S3ChainAnchor implements ChainAnchor {
83
+ private readonly opts;
84
+ constructor(opts: S3ChainAnchorOptions);
85
+ publish(rootHash: string, sequence: number, timestamp: string): Promise<void>;
86
+ }
87
+ export interface HashChainOptions {
88
+ /** Downstream sink that receives chained entries */
89
+ sink: AuditLogger;
90
+ /** Publish chain root every N entries (default: 1000) */
91
+ anchorEveryN?: number;
92
+ /** Optional anchor to publish roots externally */
93
+ anchor?: ChainAnchor;
94
+ /** Seed hash for the first entry (default: '0') */
95
+ seedHash?: string;
96
+ }
97
+ /**
98
+ * Wraps any AuditLogger with tamper-evident SHA-256 hash chaining.
99
+ *
100
+ * Each entry is hashed together with the hash of the previous entry.
101
+ * Any modification to a historical entry breaks the chain from that
102
+ * point forward — detectable by recomputing and comparing hashes.
103
+ *
104
+ * @example
105
+ * const logger = new HashChainAuditLogger({
106
+ * sink: new DatadogAuditLogger({ apiKey: process.env.DD_API_KEY! }),
107
+ * anchor: new S3ChainAnchor({ bucketName: 'my-audit-anchors', region: 'us-east-1' }),
108
+ * });
109
+ */
110
+ export declare class HashChainAuditLogger implements AuditLogger {
111
+ private readonly opts;
112
+ private previousHash;
113
+ private sequence;
114
+ private readonly anchorEveryN;
115
+ constructor(opts: HashChainOptions);
116
+ log(entry: AuditLogEntry): Promise<void>;
117
+ /** Verify an ordered array of chained entries; returns result with first broken sequence if any */
118
+ verify(entries: ChainedAuditLogEntry[]): Promise<ChainVerificationResult>;
119
+ private sha256;
120
+ }
121
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,18 +1,47 @@
1
1
  {
2
2
  "name": "@datacules/agent-identity-audit",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "private": false,
5
5
  "description": "Pre-built audit logger sinks for @datacules/agent-identity (Console, Webhook, Datadog, Splunk)",
6
+ "author": "Datacules LLC",
7
+ "license": "SEE LICENSE IN LICENSE",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/hvrcharon1/agent-identity.git",
11
+ "directory": "packages/audit"
12
+ },
13
+ "keywords": [
14
+ "agent-identity",
15
+ "audit",
16
+ "logging",
17
+ "datadog",
18
+ "splunk",
19
+ "webhook",
20
+ "ai-agents",
21
+ "datacules"
22
+ ],
6
23
  "main": "./dist/cjs/index.js",
7
24
  "module": "./dist/esm/index.js",
8
25
  "types": "./dist/types/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "import": "./dist/esm/index.js",
29
+ "require": "./dist/cjs/index.js",
30
+ "types": "./dist/types/index.d.ts"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "LICENSE",
36
+ "README.md"
37
+ ],
9
38
  "scripts": {
10
- "build": "tsc -p tsconfig.build.json",
39
+ "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.cjs.json",
11
40
  "test": "vitest run",
12
41
  "type-check": "tsc --noEmit"
13
42
  },
14
43
  "peerDependencies": {
15
- "@datacules/agent-identity": "^0.8.0"
44
+ "@datacules/agent-identity": "^0.11.1"
16
45
  },
17
46
  "devDependencies": {
18
47
  "@datacules/agent-identity": "*",
package/src/audit.test.ts DELETED
@@ -1,213 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import {
3
- ConsoleAuditLogger,
4
- WebhookAuditLogger,
5
- DatadogAuditLogger,
6
- SplunkAuditLogger,
7
- CompositeAuditLogger,
8
- } from './index';
9
- import type { AuditLogEntry } from '@datacules/agent-identity';
10
-
11
- // All HTTP calls are mocked via vi.stubGlobal('fetch', ...).
12
- // No live webhook, Datadog, or Splunk endpoint is needed.
13
- const mockFetch = vi.fn();
14
- vi.stubGlobal('fetch', mockFetch);
15
-
16
- const ENTRY: AuditLogEntry = {
17
- userId: 'user-alice',
18
- resourceId: 'knowledge-base',
19
- resourceKind: 'shared',
20
- provider: 'openai',
21
- action: 'read',
22
- credentialId: 'cred-openai-prod',
23
- credentialKind: 'fixed',
24
- resolvedFor: 'service',
25
- traceId: 'trace-abc123',
26
- sessionId: 'sess-xyz',
27
- requestedAt: '2026-05-30T12:00:00.000Z',
28
- model: 'gpt-4o',
29
- };
30
-
31
- // ── ConsoleAuditLogger ─────────────────────────────────────────────────────
32
-
33
- describe('ConsoleAuditLogger', () => {
34
- it('calls console.log with the [agent-identity audit] prefix and JSON entry', async () => {
35
- const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
36
- const logger = new ConsoleAuditLogger();
37
- await logger.log(ENTRY);
38
- expect(spy).toHaveBeenCalledOnce();
39
- const [prefix, json] = spy.mock.calls[0] as [string, string];
40
- expect(prefix).toBe('[agent-identity audit]');
41
- expect(json).toContain('cred-openai-prod');
42
- spy.mockRestore();
43
- });
44
-
45
- it('resolves without throwing on any valid AuditLogEntry', async () => {
46
- vi.spyOn(console, 'log').mockImplementation(() => {});
47
- const logger = new ConsoleAuditLogger();
48
- await expect(logger.log(ENTRY)).resolves.not.toThrow();
49
- vi.restoreAllMocks();
50
- });
51
- });
52
-
53
- // ── WebhookAuditLogger ─────────────────────────────────────────────────────
54
-
55
- describe('WebhookAuditLogger', () => {
56
- beforeEach(() => vi.clearAllMocks());
57
-
58
- it('POSTs the entry as JSON to the configured webhook URL', async () => {
59
- mockFetch.mockResolvedValueOnce({ ok: true } as Response);
60
- const logger = new WebhookAuditLogger({ url: 'https://hooks.example.com/agent-identity' });
61
- await logger.log(ENTRY);
62
- expect(mockFetch).toHaveBeenCalledWith(
63
- 'https://hooks.example.com/agent-identity',
64
- expect.objectContaining({
65
- method: 'POST',
66
- headers: expect.objectContaining({ 'Content-Type': 'application/json' }),
67
- body: expect.stringContaining('cred-openai-prod'),
68
- })
69
- );
70
- });
71
-
72
- it('adds the X-Webhook-Secret header when a secret is configured', async () => {
73
- mockFetch.mockResolvedValueOnce({ ok: true } as Response);
74
- const logger = new WebhookAuditLogger({
75
- url: 'https://hooks.example.com/ai',
76
- secret: 'hmac-secret-xyz',
77
- });
78
- await logger.log(ENTRY);
79
- expect(mockFetch).toHaveBeenCalledWith(
80
- expect.any(String),
81
- expect.objectContaining({
82
- headers: expect.objectContaining({ 'X-Webhook-Secret': 'hmac-secret-xyz' }),
83
- })
84
- );
85
- });
86
-
87
- it('resolves without throwing when fetch fails and silent=true (default)', async () => {
88
- mockFetch.mockRejectedValueOnce(new Error('Network unreachable'));
89
- vi.spyOn(console, 'warn').mockImplementation(() => {});
90
- const logger = new WebhookAuditLogger({ url: 'https://hooks.example.com/agent-identity' });
91
- await expect(logger.log(ENTRY)).resolves.not.toThrow();
92
- vi.restoreAllMocks();
93
- });
94
-
95
- it('throws when fetch fails and silent=false', async () => {
96
- mockFetch.mockRejectedValueOnce(new Error('Connection refused'));
97
- const logger = new WebhookAuditLogger({
98
- url: 'https://hooks.example.com/ai',
99
- silent: false,
100
- });
101
- await expect(logger.log(ENTRY)).rejects.toThrow('Connection refused');
102
- });
103
- });
104
-
105
- // ── DatadogAuditLogger ─────────────────────────────────────────────────────
106
-
107
- describe('DatadogAuditLogger', () => {
108
- beforeEach(() => vi.clearAllMocks());
109
-
110
- it('POSTs to the Datadog log intake URL with the DD-API-KEY header', async () => {
111
- mockFetch.mockResolvedValueOnce({ ok: true } as Response);
112
- const logger = new DatadogAuditLogger({ apiKey: 'dd-api-key-test-123' });
113
- await logger.log(ENTRY);
114
- expect(mockFetch).toHaveBeenCalledWith(
115
- expect.stringContaining('https://http-intake.logs.datadoghq.com/api/v2/logs'),
116
- expect.objectContaining({
117
- headers: expect.objectContaining({ 'DD-API-KEY': 'dd-api-key-test-123' }),
118
- })
119
- );
120
- });
121
-
122
- it('uses a custom Datadog site when the site option is specified', async () => {
123
- mockFetch.mockResolvedValueOnce({ ok: true } as Response);
124
- const logger = new DatadogAuditLogger({ apiKey: 'key', site: 'datadoghq.eu' });
125
- await logger.log(ENTRY);
126
- const [url] = mockFetch.mock.calls[0] as [string];
127
- expect(url).toContain('datadoghq.eu');
128
- });
129
-
130
- it('is silent by default when fetch fails (resolves without throwing)', async () => {
131
- mockFetch.mockRejectedValueOnce(new Error('Datadog unreachable'));
132
- vi.spyOn(console, 'warn').mockImplementation(() => {});
133
- const logger = new DatadogAuditLogger({ apiKey: 'key' });
134
- await expect(logger.log(ENTRY)).resolves.not.toThrow();
135
- vi.restoreAllMocks();
136
- });
137
- });
138
-
139
- // ── SplunkAuditLogger ─────────────────────────────────────────────────────
140
-
141
- describe('SplunkAuditLogger', () => {
142
- beforeEach(() => vi.clearAllMocks());
143
-
144
- it('POSTs to the HEC URL with a Splunk token Authorization header', async () => {
145
- mockFetch.mockResolvedValueOnce({ ok: true } as Response);
146
- const logger = new SplunkAuditLogger({
147
- hecUrl: 'https://splunk.example.com:8088/services/collector',
148
- token: 'splunk-token-abc',
149
- });
150
- await logger.log(ENTRY);
151
- expect(mockFetch).toHaveBeenCalledWith(
152
- 'https://splunk.example.com:8088/services/collector',
153
- expect.objectContaining({
154
- headers: expect.objectContaining({ Authorization: 'Splunk splunk-token-abc' }),
155
- })
156
- );
157
- });
158
-
159
- it('includes the audit entry inside the Splunk event payload', async () => {
160
- mockFetch.mockResolvedValueOnce({ ok: true } as Response);
161
- const logger = new SplunkAuditLogger({
162
- hecUrl: 'https://splunk.example.com/collector',
163
- token: 'tok',
164
- });
165
- await logger.log(ENTRY);
166
- const body = JSON.parse(
167
- (mockFetch.mock.calls[0][1] as RequestInit).body as string
168
- ) as { event: AuditLogEntry; sourcetype: string };
169
- expect(body.event).toMatchObject({ credentialId: 'cred-openai-prod' });
170
- expect(body.sourcetype).toBe('agent_identity');
171
- });
172
-
173
- it('is silent by default when fetch fails (resolves without throwing)', async () => {
174
- mockFetch.mockRejectedValueOnce(new Error('HEC unreachable'));
175
- vi.spyOn(console, 'warn').mockImplementation(() => {});
176
- const logger = new SplunkAuditLogger({
177
- hecUrl: 'https://splunk.example.com/collector',
178
- token: 'tok',
179
- });
180
- await expect(logger.log(ENTRY)).resolves.not.toThrow();
181
- vi.restoreAllMocks();
182
- });
183
- });
184
-
185
- // ── CompositeAuditLogger ───────────────────────────────────────────────────
186
-
187
- describe('CompositeAuditLogger', () => {
188
- beforeEach(() => vi.clearAllMocks());
189
-
190
- it('forwards the entry to all registered loggers', async () => {
191
- const logA = { log: vi.fn<[AuditLogEntry], Promise<void>>().mockResolvedValue(undefined) };
192
- const logB = { log: vi.fn<[AuditLogEntry], Promise<void>>().mockResolvedValue(undefined) };
193
- const composite = new CompositeAuditLogger([logA, logB]);
194
- await composite.log(ENTRY);
195
- expect(logA.log).toHaveBeenCalledWith(ENTRY);
196
- expect(logB.log).toHaveBeenCalledWith(ENTRY);
197
- });
198
-
199
- it('continues via Promise.allSettled even when one logger rejects', async () => {
200
- const logA = { log: vi.fn<[AuditLogEntry], Promise<void>>().mockRejectedValue(new Error('Sink A failed')) };
201
- const logB = { log: vi.fn<[AuditLogEntry], Promise<void>>().mockResolvedValue(undefined) };
202
- const composite = new CompositeAuditLogger([logA, logB]);
203
- await expect(composite.log(ENTRY)).resolves.not.toThrow();
204
- expect(logB.log).toHaveBeenCalledWith(ENTRY);
205
- });
206
-
207
- it('works correctly with a single logger', async () => {
208
- const logA = { log: vi.fn<[AuditLogEntry], Promise<void>>().mockResolvedValue(undefined) };
209
- const composite = new CompositeAuditLogger([logA]);
210
- await composite.log(ENTRY);
211
- expect(logA.log).toHaveBeenCalledOnce();
212
- });
213
- });
package/src/index.ts DELETED
@@ -1,258 +0,0 @@
1
- /**
2
- * @datacules/agent-identity-audit — extended with hash-chain tamper-evident logging
3
- *
4
- * New in this version:
5
- * HashChainAuditLogger — wraps any existing AuditLogger and appends a SHA-256
6
- * hash chain to every entry. Detects tampering by recomputing the chain.
7
- * ChainAnchor interface + built-in S3 and stdout anchors.
8
- */
9
- import type { AuditLogEntry, AuditLogger } from '@datacules/agent-identity';
10
-
11
- // ──────────────────────────────────────────────────────────────────────────
12
- // Existing sinks
13
- // ──────────────────────────────────────────────────────────────────────────
14
-
15
- export class ConsoleAuditLogger implements AuditLogger {
16
- async log(entry: AuditLogEntry): Promise<void> {
17
- console.log('[agent-identity audit]', JSON.stringify(entry, null, 2));
18
- }
19
- }
20
-
21
- export interface WebhookAuditLoggerOptions {
22
- url: string;
23
- secret?: string;
24
- timeoutMs?: number;
25
- silent?: boolean;
26
- }
27
-
28
- export class WebhookAuditLogger implements AuditLogger {
29
- private readonly options: Required<WebhookAuditLoggerOptions>;
30
- constructor(options: WebhookAuditLoggerOptions) {
31
- this.options = { secret: '', timeoutMs: 5000, silent: true, ...options };
32
- }
33
- async log(entry: AuditLogEntry): Promise<void> {
34
- const { url, secret, timeoutMs, silent } = this.options;
35
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
36
- if (secret) headers['X-Webhook-Secret'] = secret;
37
- const controller = new AbortController();
38
- const timer = setTimeout(() => controller.abort(), timeoutMs);
39
- try {
40
- await fetch(url, { method: 'POST', headers, body: JSON.stringify(entry), signal: controller.signal });
41
- } catch (err) {
42
- if (!silent) throw err;
43
- console.warn('[agent-identity] WebhookAuditLogger failed:', err);
44
- } finally {
45
- clearTimeout(timer);
46
- }
47
- }
48
- }
49
-
50
- export interface DatadogAuditLoggerOptions {
51
- apiKey: string;
52
- service?: string;
53
- site?: string;
54
- silent?: boolean;
55
- }
56
-
57
- export class DatadogAuditLogger implements AuditLogger {
58
- private readonly options: Required<DatadogAuditLoggerOptions>;
59
- constructor(options: DatadogAuditLoggerOptions) {
60
- this.options = { service: 'agent-identity', site: 'datadoghq.com', silent: true, ...options };
61
- }
62
- async log(entry: AuditLogEntry): Promise<void> {
63
- const { apiKey, service, site, silent } = this.options;
64
- try {
65
- await fetch(`https://http-intake.logs.${site}/api/v2/logs`, {
66
- method: 'POST',
67
- headers: { 'DD-API-KEY': apiKey, 'Content-Type': 'application/json' },
68
- body: JSON.stringify({ ddsource: 'agent-identity', service, message: JSON.stringify(entry) }),
69
- });
70
- } catch (err) {
71
- if (!silent) throw err;
72
- console.warn('[agent-identity] DatadogAuditLogger failed:', err);
73
- }
74
- }
75
- }
76
-
77
- export interface SplunkAuditLoggerOptions {
78
- hecUrl: string;
79
- token: string;
80
- sourcetype?: string;
81
- silent?: boolean;
82
- }
83
-
84
- export class SplunkAuditLogger implements AuditLogger {
85
- private readonly options: Required<SplunkAuditLoggerOptions>;
86
- constructor(options: SplunkAuditLoggerOptions) {
87
- this.options = { sourcetype: 'agent_identity', silent: true, ...options };
88
- }
89
- async log(entry: AuditLogEntry): Promise<void> {
90
- const { hecUrl, token, sourcetype, silent } = this.options;
91
- try {
92
- await fetch(hecUrl, {
93
- method: 'POST',
94
- headers: { Authorization: `Splunk ${token}`, 'Content-Type': 'application/json' },
95
- body: JSON.stringify({ event: entry, sourcetype }),
96
- });
97
- } catch (err) {
98
- if (!silent) throw err;
99
- console.warn('[agent-identity] SplunkAuditLogger failed:', err);
100
- }
101
- }
102
- }
103
-
104
- export class CompositeAuditLogger implements AuditLogger {
105
- constructor(private readonly loggers: AuditLogger[]) {}
106
- async log(entry: AuditLogEntry): Promise<void> {
107
- await Promise.allSettled(this.loggers.map((l) => l.log(entry)));
108
- }
109
- }
110
-
111
- // ─── Hash chain types ────────────────────────────────────────────────────────────
112
-
113
- export interface ChainedAuditLogEntry extends AuditLogEntry {
114
- /** SHA-256 hash of this entry's data fields */
115
- entryHash: string;
116
- /** SHA-256 hash of the previous entry (or '0' for the first entry) */
117
- previousHash: string;
118
- /** Sequential position in the chain */
119
- sequence: number;
120
- }
121
-
122
- export interface ChainVerificationResult {
123
- valid: boolean;
124
- entriesChecked: number;
125
- firstEntry?: ChainedAuditLogEntry;
126
- lastEntry?: ChainedAuditLogEntry;
127
- brokenAt?: number; // sequence number where chain breaks
128
- error?: string;
129
- }
130
-
131
- // ─── Chain Anchor ────────────────────────────────────────────────────────────────
132
-
133
- export interface ChainAnchor {
134
- /** Publish the chain root hash to an immutable external location */
135
- publish(rootHash: string, sequence: number, timestamp: string): Promise<void>;
136
- }
137
-
138
- /** Prints the chain root to stdout — suitable for piping to a CI artifact */
139
- export class StdoutChainAnchor implements ChainAnchor {
140
- async publish(rootHash: string, sequence: number, timestamp: string): Promise<void> {
141
- console.log(`[agent-identity chain-anchor] seq=${sequence} root=${rootHash} ts=${timestamp}`);
142
- }
143
- }
144
-
145
- /** Publishes the chain root hash to an S3 object with Object Lock enabled */
146
- export interface S3ChainAnchorOptions {
147
- bucketName: string;
148
- region: string;
149
- /** AWS credentials or SDK client — omit to use default credential chain */
150
- credentialsJson?: string;
151
- }
152
-
153
- export class S3ChainAnchor implements ChainAnchor {
154
- constructor(private readonly opts: S3ChainAnchorOptions) {}
155
-
156
- async publish(rootHash: string, sequence: number, timestamp: string): Promise<void> {
157
- const key = `agent-identity/chain-roots/${sequence}-${timestamp}.json`;
158
- const body = JSON.stringify({ rootHash, sequence, timestamp, publishedAt: new Date().toISOString() });
159
- // S3 PutObject — in production use @aws-sdk/client-s3
160
- const url = `https://${this.opts.bucketName}.s3.${this.opts.region}.amazonaws.com/${key}`;
161
- await fetch(url, {
162
- method: 'PUT',
163
- headers: { 'Content-Type': 'application/json', 'Content-Length': String(body.length) },
164
- body,
165
- }).catch((err) => console.warn('[S3ChainAnchor] publish failed:', err));
166
- }
167
- }
168
-
169
- // ─── HashChainAuditLogger ──────────────────────────────────────────────────────
170
-
171
- export interface HashChainOptions {
172
- /** Downstream sink that receives chained entries */
173
- sink: AuditLogger;
174
- /** Publish chain root every N entries (default: 1000) */
175
- anchorEveryN?: number;
176
- /** Optional anchor to publish roots externally */
177
- anchor?: ChainAnchor;
178
- /** Seed hash for the first entry (default: '0') */
179
- seedHash?: string;
180
- }
181
-
182
- /**
183
- * Wraps any AuditLogger with tamper-evident SHA-256 hash chaining.
184
- *
185
- * Each entry is hashed together with the hash of the previous entry.
186
- * Any modification to a historical entry breaks the chain from that
187
- * point forward — detectable by recomputing and comparing hashes.
188
- *
189
- * @example
190
- * const logger = new HashChainAuditLogger({
191
- * sink: new DatadogAuditLogger({ apiKey: process.env.DD_API_KEY! }),
192
- * anchor: new S3ChainAnchor({ bucketName: 'my-audit-anchors', region: 'us-east-1' }),
193
- * });
194
- */
195
- export class HashChainAuditLogger implements AuditLogger {
196
- private previousHash: string;
197
- private sequence = 0;
198
- private readonly anchorEveryN: number;
199
-
200
- constructor(private readonly opts: HashChainOptions) {
201
- this.previousHash = opts.seedHash ?? '0';
202
- this.anchorEveryN = opts.anchorEveryN ?? 1000;
203
- }
204
-
205
- async log(entry: AuditLogEntry): Promise<void> {
206
- this.sequence += 1;
207
- const entryHash = await this.sha256(JSON.stringify(entry));
208
- const chainHash = await this.sha256(`${this.previousHash}:${entryHash}`);
209
-
210
- const chained: ChainedAuditLogEntry = {
211
- ...entry,
212
- entryHash,
213
- previousHash: this.previousHash,
214
- sequence: this.sequence,
215
- };
216
-
217
- this.previousHash = chainHash;
218
-
219
- await this.opts.sink.log(chained as unknown as AuditLogEntry);
220
-
221
- if (this.opts.anchor && this.sequence % this.anchorEveryN === 0) {
222
- await this.opts.anchor
223
- .publish(chainHash, this.sequence, new Date().toISOString())
224
- .catch(console.error);
225
- }
226
- }
227
-
228
- /** Verify an ordered array of chained entries; returns result with first broken sequence if any */
229
- async verify(entries: ChainedAuditLogEntry[]): Promise<ChainVerificationResult> {
230
- let prevHash = this.opts.seedHash ?? '0';
231
- for (const entry of entries) {
232
- const { entryHash, previousHash, sequence, ...data } = entry;
233
- const expectedEntryHash = await this.sha256(JSON.stringify(data));
234
- if (expectedEntryHash !== entryHash) {
235
- return { valid: false, entriesChecked: sequence, brokenAt: sequence, error: `entry hash mismatch at sequence ${sequence}` };
236
- }
237
- if (previousHash !== prevHash) {
238
- return { valid: false, entriesChecked: sequence, brokenAt: sequence, error: `chain break at sequence ${sequence}` };
239
- }
240
- prevHash = await this.sha256(`${previousHash}:${entryHash}`);
241
- }
242
- return {
243
- valid: true,
244
- entriesChecked: entries.length,
245
- firstEntry: entries[0],
246
- lastEntry: entries[entries.length - 1],
247
- };
248
- }
249
-
250
- private async sha256(data: string): Promise<string> {
251
- if (typeof crypto !== 'undefined' && crypto.subtle) {
252
- const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data));
253
- return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, '0')).join('');
254
- }
255
- const { createHash } = await import('crypto');
256
- return createHash('sha256').update(data).digest('hex');
257
- }
258
- }