[object Object]

← back to Marketing Command Center

channels: add read-only GET /tiktok-health token-validity probe (creator_info; token never leaves server)

a89c005143416f52a284724488fd1abaf49a668a · 2026-08-12 16:47:34 -0700 · Steve

Files touched

Diff

commit a89c005143416f52a284724488fd1abaf49a668a
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 12 16:47:34 2026 -0700

    channels: add read-only GET /tiktok-health token-validity probe (creator_info; token never leaves server)
---
 modules/channels/index.js | 32 ++++++++++++++++++++++++++++++++
 package.json              |  2 +-
 2 files changed, 33 insertions(+), 1 deletion(-)

diff --git a/modules/channels/index.js b/modules/channels/index.js
index ba848d1..c9ca981 100644
--- a/modules/channels/index.js
+++ b/modules/channels/index.js
@@ -725,6 +725,38 @@ module.exports = {
       const connected = Object.values(st).filter(p => p.connected).length;
       res.json({ platforms: st, connectedCount: connected, total: Object.keys(st).length, outbox: readOutbox().length });
     });
+
+    // Read-only TikTok token VALIDITY probe. The /status 'connected' flag for TikTok
+    // is only a presence check (env var set), which can mask an EXPIRED token — so a
+    // handoff might look ready yet fail at publish time. This calls TikTok's
+    // creator_info/query (a read; it does NOT post) with the SAME token postTikTok()
+    // uses, and reports genuine validity. The token never leaves the server — only a
+    // last-4 digest is returned.
+    router.get('/tiktok-health', async (_req, res) => {
+      const token = env('TIKTOK_ACCESS_TOKEN') || (readTokens().tiktok || {}).access_token || '';
+      if (!token) return res.json({ ok: true, valid: false, reason: 'no TikTok token configured' });
+      try {
+        const r = await fetch('https://open.tiktokapis.com/v2/post/publish/creator_info/query/', {
+          method: 'POST',
+          headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json; charset=UTF-8' },
+          body: '{}',
+        });
+        const j = await r.json().catch(() => ({}));
+        const code = j.error && j.error.code;
+        const valid = r.ok && code === 'ok';
+        res.json({
+          ok: true, valid, http: r.status,
+          error_code: code || null,
+          error_message: (j.error && j.error.message) || null,
+          creator_username: (j.data && j.data.creator_username) || null,
+          privacy_options: (j.data && j.data.privacy_level_options) || null,
+          last4: token.slice(-4),
+          caveat: valid ? 'Direct posts are SELF_ONLY (private) until TikTok approves the app audit.' : null,
+        });
+      } catch (e) {
+        res.json({ ok: true, valid: false, error_message: String(e.message || e).slice(0, 140) });
+      }
+    });
     // Localize expiring IG/fbcdn media to durable local copies before serving
     // (see lib/media-cache): cached→local path synchronously, background-fetch
     // any missing so the next load self-heals. Never blocks the response.
diff --git a/package.json b/package.json
index 3b5054e..addd357 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "marketing-command-center",
-  "version": "1.10.1",
+  "version": "1.10.2",
   "description": "DW Marketing Command Center — Constant Contact, marketing calendar, suggested copy, on-demand layouts",
   "main": "server.js",
   "scripts": {

← 941504a auto-data-snapshot: 2026-08-12T16:33:05 (2 data files) — dat  ·  back to Marketing Command Center  ·  channels: consolidated /health probe (fb/ig/tiktok/linkedin/ f874f5b →