openvibers/openvibe.watch Commits

Watch: the chat apps' checks come from the SDK (A4, openvibe-sdk 0.92.0) (#215)

OP OpenVibers committed 7 days ago · cc44b936b2 · 4 files, +17 −31

* Watch: the chat apps' checks come from the SDK (A4)

Slack's v0 signing, Discord's Ed25519 interaction signature and Telegram's secret token are
checked by openvibe-sdk/chat-apps (verifySlack, verifyDiscord, verifyTelegram) instead of
Watch's own copies, so Watch and the chat bots read one implementation. The window, the exact
raw bytes, the constant-time comparison and "an unknown provider checks nothing" are unchanged.

Pins openvibe-sdk v0.92.0 (was v0.82.0); the chat-apps subpath ships in that release (SDK #124).

Tests: node test/run.js webhook — webhook-providers and webhook-sources pass.

* Watch: pin openvibe-sdk v0.92.0 after the fleet moved to 0.91.0

Merges main (the SDK 0.91.0 pin, Watch #214) and keeps this branch on v0.92.0, which carries
openvibe-sdk/chat-apps (SDK #124). No other change.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 05868d3..fad958f 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -3,6 +3,15 @@
What shipped on OpenVibe.Watch, newest first. The plan is T18 ("Watch complete"); each entry names
the step it landed.
++## 2026-11-06 — the chat apps' checks come from the SDK (agent plan A4)
++
++- Slack's v0 request signing, Discord's Ed25519 interaction signature and Telegram's secret token
++ are checked by `openvibe-sdk/chat-apps` (`verifySlack`, `verifyDiscord`, `verifyTelegram`), the
++ same code the chat bots use, instead of Watch's own copies (`server/webhook-sources.js`). The
++ behaviour is unchanged: the same five-minute window, the same exact raw bytes, the same
++ constant-time comparison, and a provider Watch does not know still checks nothing.
++- Pins openvibe-sdk v0.92.0 (was v0.82.0).
++
## 2026-11-03 — email in (agent plan A3)
- A webhook watch can have an email address of its own (`in-…@openvibe.watch`): mail sent there runs its check on the
ddiff --git a/package-lock.json b/package-lock.json
iindex 421bb98..e8e4ae8 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -13,7 +13,7 @@
"express": "^4.21.2",
"fast-xml-parser": "^4.5.3",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.289.1",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.91.0",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.92.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.115.1",
"pg": "^8.23.0"
},
@@@ -1060,9 +1060,8 @@
}
},
"node_modules/openvibe-sdk": {
-- "version": "0.91.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.91.0",
-- "integrity": "sha512-LUWT2g30FUCTgaj9hVUqSfvT4FNMG4LujctcO8tqtXK81vpOOWDbWvaln/zfqFVup9KfCme7fctd46DXrq4Zbw==",
++ "version": "0.92.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.92.0",
"license": "MIT",
"bin": {
"openvibe-download": "bin/openvibe-download.js"
ddiff --git a/package.json b/package.json
iindex fcae9e8..f97615e 100644
---- a/package.json
++++ b/package.json
@@@ -25,7 +25,7 @@
"express": "^4.21.2",
"fast-xml-parser": "^4.5.3",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.289.1",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.91.0",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.92.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.115.1",
"pg": "^8.23.0"
},
ddiff --git a/server/webhook-sources.js b/server/webhook-sources.js
iindex 8ffb91d..5c2c054 100644
---- a/server/webhook-sources.js
++++ b/server/webhook-sources.js
@@@ -48,6 +48,7 @@
* 503 with Retry-After: 1 while the watch's previous check still runs; 413 over 256 KB.
*/
const crypto = require('crypto');
++const { verifySlack, verifyDiscord, verifyTelegram } = require('openvibe-sdk/chat-apps');
const { parseMail } = require('./mail-parse');
const SECRET_RE = /^whk_[A-Za-z0-9_-]{43}$/;
@@@ -126,14 +127,6 @@ function safeEqual(a, b) {
const hmac = (secret, data) => crypto.createHmac('sha256', secret).update(data).digest('hex');
const fresh = (ts, nowMs) => /^\d{1,12}$/.test(String(ts)) && Math.abs(Math.floor(nowMs / 1000) - Number(ts)) <= TOLERANCE_SEC;
--/** Slack's request signing: v0 over "v0:<timestamp>:<body>", the timestamp within five minutes. */
--function slackVerified(secret, raw, headers, nowMs) {
-- const ts = String(headers['x-slack-request-timestamp'] || '');
-- const sig = String(headers['x-slack-signature'] || '');
-- if (!fresh(ts, nowMs) || !sig.startsWith('v0=')) return false;
-- return safeEqual(sig, `v0=${hmac(secret, Buffer.concat([Buffer.from(`v0:${ts}:`), raw]))}`);
--}
--
/** Stripe's webhook signing: v1 over "<t>.<body>" (any of several v1), t within five minutes. */
function stripeVerified(secret, raw, headers, nowMs) {
const parts = String(headers['stripe-signature'] || '').split(',').map((x) => x.trim().split('='));
@@@ -143,33 +136,18 @@ function stripeVerified(secret, raw, headers, nowMs) {
return parts.filter(([k]) => k === 'v1').some(([, v]) => safeEqual(v || '', want));
}
--// The DER prefix of an Ed25519 public key (SubjectPublicKeyInfo), so the 32 raw bytes Discord shows become a key object.
--const ED25519_SPKI = Buffer.from('302a300506032b6570032100', 'hex');
--
--/** Discord's interaction signing: Ed25519 over "<timestamp><body>" with the application's public key. */
--function discordVerified(publicKeyHex, raw, headers, nowMs) {
-- const ts = String(headers['x-signature-timestamp'] || '');
-- const sig = String(headers['x-signature-ed25519'] || '');
-- if (!fresh(ts, nowMs) || !/^[0-9a-f]{128}$/i.test(sig) || !/^[0-9a-f]{64}$/i.test(publicKeyHex)) return false;
-- try {
-- const key = crypto.createPublicKey({ key: Buffer.concat([ED25519_SPKI, Buffer.from(publicKeyHex, 'hex')]), format: 'der', type: 'spki' });
-- return crypto.verify(null, Buffer.concat([Buffer.from(ts), raw]), key, Buffer.from(sig, 'hex'));
-- } catch { return false; }
--}
--
/**
* Does the delivery carry the watch's secret? With a sender's own secret (`provider`), only that sender's scheme
* counts; with Watch's own, a matching body signature, the secret as a bearer token, or Telegram's secret token.
*/
function verified(secret, raw, headers, { provider = null, now = Date.now() } = {}) {
if (!secret) return false;
-- if (provider === 'slack') return slackVerified(secret, raw, headers, now);
++ if (provider === 'slack') return verifySlack({ secret, raw, headers, now });
if (provider === 'stripe') return stripeVerified(secret, raw, headers, now);
-- if (provider === 'discord') return discordVerified(secret, raw, headers, now);
++ if (provider === 'discord') return verifyDiscord({ publicKey: secret, raw, headers, now });
if (provider === 'youtube') return websubVerified(secret, raw, headers);
if (provider) return false;
-- const tg = headers['x-telegram-bot-api-secret-token'];
-- if (tg != null && safeEqual(String(tg), secret)) return true;
++ if (verifyTelegram({ secret, headers })) return true;
const sig = String(headers['x-hub-signature-256'] || '');
if (sig.startsWith('sha256=')) {
const want = `sha256=${crypto.createHmac('sha256', secret).update(raw).digest('hex')}`;