@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiacta-org/ai-citation-sdk",
3
- "version": "1.0.7",
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",
@@ -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 - parseInt(timestamp, 10)) > TIMESTAMP_TOLERANCE_SECONDS) {
29
+ if (Math.abs(now - parsedTimestamp) > TIMESTAMP_TOLERANCE_SECONDS) {
21
30
  throw new Error('Timestamp outside tolerance window — possible replay attack');
22
31
  }
23
- const signedPayload = `${timestamp}.${payload}`;
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
- const received = sigHeader.replace('sha256=', '');
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
  }
@@ -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.7',
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',
@@ -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
+ });