@aiacta-org/ai-citation-sdk 1.0.7 → 1.0.8
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/package.json +1 -1
- package/src/node/signature.js +19 -3
- package/src/python/setup.py +1 -1
- package/tests/signature.test.js +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiacta-org/ai-citation-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Webhook receiver SDK for AIACTA citation events. HMAC-SHA256 signature verification, idempotency, and Express middleware (AIACTA Proposal 2, §3.4)",
|
|
5
5
|
"author": "Eric Michel",
|
|
6
6
|
"license": "Apache-2.0",
|
package/src/node/signature.js
CHANGED
|
@@ -16,13 +16,29 @@ const TIMESTAMP_TOLERANCE_SECONDS = 300; // 5 minutes (§3.4)
|
|
|
16
16
|
* @throws {Error} if timestamp is outside tolerance window
|
|
17
17
|
*/
|
|
18
18
|
function verifyWebhookSignature(payload, timestamp, sigHeader, secret) {
|
|
19
|
+
if (!secret || typeof sigHeader !== 'string' || !sigHeader.startsWith('sha256=')) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const parsedTimestamp = parseInt(timestamp, 10);
|
|
24
|
+
if (!Number.isFinite(parsedTimestamp)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
const now = Math.floor(Date.now() / 1000);
|
|
20
|
-
if (Math.abs(now -
|
|
29
|
+
if (Math.abs(now - parsedTimestamp) > TIMESTAMP_TOLERANCE_SECONDS) {
|
|
21
30
|
throw new Error('Timestamp outside tolerance window — possible replay attack');
|
|
22
31
|
}
|
|
23
|
-
|
|
32
|
+
|
|
33
|
+
const received = sigHeader.slice('sha256='.length);
|
|
34
|
+
if (!/^[0-9a-f]{64}$/i.test(received)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const body = Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload));
|
|
39
|
+
const signedPayload = Buffer.concat([Buffer.from(`${timestamp}.`), body]);
|
|
24
40
|
const expected = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');
|
|
25
|
-
|
|
41
|
+
|
|
26
42
|
// Constant-time comparison to prevent timing attacks
|
|
27
43
|
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(received, 'hex'));
|
|
28
44
|
}
|
package/src/python/setup.py
CHANGED
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name='ai-citation-sdk',
|
|
5
|
-
version='1.0.
|
|
5
|
+
version='1.0.8',
|
|
6
6
|
description='AIACTA Citation Webhook SDK for Python — signature verification, idempotency, and retry handling (Proposal 2, §3.4)',
|
|
7
7
|
long_description=open('../../README.md', encoding='utf-8').read() if __import__('os').path.exists('../../README.md') else '',
|
|
8
8
|
long_description_content_type='text/markdown',
|
package/tests/signature.test.js
CHANGED
|
@@ -24,3 +24,17 @@ test('old timestamp throws', () => {
|
|
|
24
24
|
const oldTs = String(Math.floor(Date.now() / 1000) - 400);
|
|
25
25
|
expect(() => verifyWebhookSignature('{}', oldTs, 'sha256=abc', 'secret')).toThrow();
|
|
26
26
|
});
|
|
27
|
+
|
|
28
|
+
test('missing signature header returns false', () => {
|
|
29
|
+
const ts = String(Math.floor(Date.now() / 1000));
|
|
30
|
+
expect(verifyWebhookSignature('{}', ts, undefined, 'secret')).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('malformed signature header returns false', () => {
|
|
34
|
+
const ts = String(Math.floor(Date.now() / 1000));
|
|
35
|
+
expect(verifyWebhookSignature('{}', ts, 'sha256=not-hex', 'secret')).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('non-numeric timestamp returns false', () => {
|
|
39
|
+
expect(verifyWebhookSignature('{}', 'not-a-timestamp', 'sha256=abc', 'secret')).toBe(false);
|
|
40
|
+
});
|