TIMESDevelopers
v1.0.0

Runtime workers

Host your own HTTPS worker and receive signed requests from TIMES.

mode: "runtime" guests host their own HTTPS worker. TIMES signs outbound requests and never stores partner OAuth client secrets in the catalog; connection tokens stay in connection_secrets.

Manifest

times.aip.json
{
  "schemaVersion": "1",
  "slug": "my-worker-app",
  "kind": "app",
  "version": "0.1.0",
  "name": "My Worker App",
  "mode": "runtime",
  "ui": {
    "framework": "next",
    "runtime": "iframe",
    "surfaces": ["page"],
    "entry": "./out/index.html",
    "uses_times_tokens": true
  },
  "scopes": { "required": ["state:read", "state:write", "events:subscribe"] },
  "i18n": {
    "locales": ["en", "am", "om", "ti"],
    "catalogs": {
      "en": "./i18n/messages/en.json",
      "am": "./i18n/messages/am.json",
      "om": "./i18n/messages/om.json",
      "ti": "./i18n/messages/ti.json"
    }
  },
  "runtime": {
    "workerUrl": "https://worker.example.com",
    "webhookPath": "/webhook",
    "signing": { "algorithm": "hmac-sha256", "header": "X-Times-Signature", "skewSeconds": 300 }
  }
}

Signing

TIMES posts to {workerUrl}{path} with:

  • X-Times-Timestamp: unix seconds
  • X-Times-Signature: hex HMAC-SHA256 of {timestamp}.{body}
  • X-Times-Slug: catalog slug

Reject requests outside skewSeconds. Configure the shared secret via ERP Apps:Workers:{slug}:SigningSecret or Apps:Workers:DefaultSigningSecret.

Host endpoints

  • POST /api/v1/apps/integrations/worker/invoke { slug, payload }
  • POST /api/v1/apps/integrations/webhook/test { slug, path?, payload? }
  • POST /api/v1/apps/integrations/sync/run { slug, payload? }

Guest iframe sessions can mint a short-lived provider token via POST /api/v1/apps/runtime/v1/provider/token when a platform OAuth connection exists on the same slug.

Verifying a signature

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(body: string, timestamp: string, signature: string, secret: string) {
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (age > 300) return false;
  const expected = createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex');
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Find your signing secret on the AIP detail page under Runtime.