From 7efbc39bc08ae3a69094075a36e5811ebb4ad0a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 15:17:03 +0000 Subject: [PATCH 1/2] Name captured samples with the registry's safe stem requestReceiver.ts only replaced / : \ in the resolved topic, while the registry files every doc sample under safe_stem (runs of characters outside [A-Za-z0-9._-] become one ".", outer dots stripped). A live capture for PubNub, castle, descope, flexport, mailtrap (soft bounce) or document360 (titles with spaces) was therefore written as a second file beside the doc sample. Add safeStem to topic.ts with the registry's rule, use it in the receiver, and test it against existing sample file names. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01D7eqaQdKAUzQXRbGtym1PF --- README.md | 8 +++++++- requestReceiver.ts | 4 ++-- topic.test.ts | 41 ++++++++++++++++++++++++++++++++++++++++- topic.ts | 9 +++++++++ 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f9c4cb2b..c2dd5277 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,13 @@ Only a scalar can name a file. If a path resolves to an object or array the sample falls back to `untitled-`, which is a signal that the identifier is wrong for that payload rather than something to work around. -The resolver is covered by unit tests in `topic.test.ts`. Run them with +The resolved topic becomes the file name through `safeStem`, the same rule +the registry uses for its doc samples: each run of characters outside +`[A-Za-z0-9._-]` becomes one `.`, with leading and trailing dots dropped +(`soft bounce` → `soft.bounce.json`). A capture therefore overwrites the +doc sample for its event instead of landing beside it. + +The resolver and `safeStem` are covered by unit tests in `topic.test.ts`. Run them with `yarn test`. `provenance` is optional and records, per version, how that version's diff --git a/requestReceiver.ts b/requestReceiver.ts index 59f045ee..ee03701c 100644 --- a/requestReceiver.ts +++ b/requestReceiver.ts @@ -2,7 +2,7 @@ import express from "express"; import * as fs from "fs"; import * as path from "path"; import crypto from "crypto"; -import { resolveTopic } from "./topic"; +import { resolveTopic, safeStem } from "./topic"; const app = express(); const port = process.env.PORT || 9001; @@ -85,7 +85,7 @@ const outputToFile = (output: any, provider: string, version: string) => { "providers", provider, version, - `${topic.replace(/[/:\\]/g, ".")}.json` + `${safeStem(topic)}.json` ), JSON.stringify(output, null, 2) ); diff --git a/topic.test.ts b/topic.test.ts index ab1148e9..40e2b823 100644 --- a/topic.test.ts +++ b/topic.test.ts @@ -1,7 +1,9 @@ import { strict as assert } from "node:assert"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; import { describe, it } from "node:test"; -import { resolveTopic, scalarOrUndefined } from "./topic"; +import { resolveTopic, safeStem, scalarOrUndefined } from "./topic"; describe("resolveTopic", () => { it("resolves a plain key", () => { @@ -83,3 +85,40 @@ describe("scalarOrUndefined", () => { assert.equal(scalarOrUndefined(true), undefined); }); }); + +// Each case is a topic whose doc sample is already in providers/ under the registry's +// stem, so a capture of the same event has to be written to that same file. +describe("safeStem", () => { + const cases: [string, string, string][] = [ + ["pubnub/latest", "pubnub.com/schemas/events/channel.created?v=1.0.0", + "pubnub.com.schemas.events.channel.created.v.1.0.0"], + ["mailtrap/latest", "soft bounce", "soft.bounce"], + ["descope/latest", "Change Password", "Change.Password"], + ["document360/latest", "Extension token deleted", "Extension.token.deleted"], + ["flexport/latest", "/shipment_leg#departed", "shipment_leg.departed"], + ["castle/v1", "$list_item.created", "list_item.created"], + ]; + + for (const [dir, topic, stem] of cases) { + it(`names ${JSON.stringify(topic)} as the registry does (${dir}/${stem}.json)`, () => { + assert.equal(safeStem(topic), stem); + assert.ok(existsSync(join(__dirname, "providers", dir, `${stem}.json`))); + }); + } + + it("collapses a run of unsafe characters into one dot", () => { + assert.equal(safeStem("a :/\\ b"), "a.b"); + }); + + it("keeps dots, dashes and underscores", () => { + assert.equal(safeStem("charge.succeeded_v-2"), "charge.succeeded_v-2"); + }); + + it("strips leading and trailing dots", () => { + assert.equal(safeStem("..a.."), "a"); + }); + + it("never returns an empty stem", () => { + assert.equal(safeStem("///"), "_unnamed"); + }); +}); diff --git a/topic.ts b/topic.ts index 7b1c0b95..3b3379c0 100644 --- a/topic.ts +++ b/topic.ts @@ -50,3 +50,12 @@ export const scalarOrUndefined = (value: any): string | undefined => typeof value === "string" || typeof value === "number" ? String(value) : undefined; + +// The file stem for a topic, the same rule hookdeck/webhook-registry's generator +// uses for every sample it exports (safe_stem in generator/harvest_doc_examples.py): +// each run of characters outside [A-Za-z0-9._-] becomes one ".", and leading or +// trailing dots are dropped. A capture has to land on the doc sample's file name, +// otherwise it is written as a duplicate beside it ("soft bounce" -> soft.bounce, +// not "soft bounce"; a PubNub "...channel.created?v=1.0.0" -> "...channel.created.v.1.0.0"). +export const safeStem = (topic: string): string => + topic.replace(/[^A-Za-z0-9._-]+/g, ".").replace(/^\.+|\.+$/g, "") || "_unnamed"; From 91e1b4925e53c12b926bbcaef779bf92ac9ea702 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 16:18:28 +0000 Subject: [PATCH 2/2] Move relay-messenger samples to their 2026-08-30 version dir Every Relay envelope carries webhook_version 2026-08-30, which Relay documents as the fixed payload version, and hookdeck/webhook-registry#121 files the doc samples under that directory (as linq's are). Move the eight files unchanged and point index.json's latest_version and provenance at it, matching the registry's samples-doc copy byte for byte. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01D7eqaQdKAUzQXRbGtym1PF --- .../relay-messenger/{latest => 2026-08-30}/chat.created.json | 0 .../{latest => 2026-08-30}/chat.typing_indicator.started.json | 0 .../{latest => 2026-08-30}/chat.typing_indicator.stopped.json | 0 .../{latest => 2026-08-30}/message.delivered.json | 0 .../{latest => 2026-08-30}/message.failed.json | 0 .../relay-messenger/{latest => 2026-08-30}/message.read.json | 0 .../{latest => 2026-08-30}/message.received.json | 0 .../relay-messenger/{latest => 2026-08-30}/message.sent.json | 0 providers/relay-messenger/index.json | 4 ++-- 9 files changed, 2 insertions(+), 2 deletions(-) rename providers/relay-messenger/{latest => 2026-08-30}/chat.created.json (100%) rename providers/relay-messenger/{latest => 2026-08-30}/chat.typing_indicator.started.json (100%) rename providers/relay-messenger/{latest => 2026-08-30}/chat.typing_indicator.stopped.json (100%) rename providers/relay-messenger/{latest => 2026-08-30}/message.delivered.json (100%) rename providers/relay-messenger/{latest => 2026-08-30}/message.failed.json (100%) rename providers/relay-messenger/{latest => 2026-08-30}/message.read.json (100%) rename providers/relay-messenger/{latest => 2026-08-30}/message.received.json (100%) rename providers/relay-messenger/{latest => 2026-08-30}/message.sent.json (100%) diff --git a/providers/relay-messenger/latest/chat.created.json b/providers/relay-messenger/2026-08-30/chat.created.json similarity index 100% rename from providers/relay-messenger/latest/chat.created.json rename to providers/relay-messenger/2026-08-30/chat.created.json diff --git a/providers/relay-messenger/latest/chat.typing_indicator.started.json b/providers/relay-messenger/2026-08-30/chat.typing_indicator.started.json similarity index 100% rename from providers/relay-messenger/latest/chat.typing_indicator.started.json rename to providers/relay-messenger/2026-08-30/chat.typing_indicator.started.json diff --git a/providers/relay-messenger/latest/chat.typing_indicator.stopped.json b/providers/relay-messenger/2026-08-30/chat.typing_indicator.stopped.json similarity index 100% rename from providers/relay-messenger/latest/chat.typing_indicator.stopped.json rename to providers/relay-messenger/2026-08-30/chat.typing_indicator.stopped.json diff --git a/providers/relay-messenger/latest/message.delivered.json b/providers/relay-messenger/2026-08-30/message.delivered.json similarity index 100% rename from providers/relay-messenger/latest/message.delivered.json rename to providers/relay-messenger/2026-08-30/message.delivered.json diff --git a/providers/relay-messenger/latest/message.failed.json b/providers/relay-messenger/2026-08-30/message.failed.json similarity index 100% rename from providers/relay-messenger/latest/message.failed.json rename to providers/relay-messenger/2026-08-30/message.failed.json diff --git a/providers/relay-messenger/latest/message.read.json b/providers/relay-messenger/2026-08-30/message.read.json similarity index 100% rename from providers/relay-messenger/latest/message.read.json rename to providers/relay-messenger/2026-08-30/message.read.json diff --git a/providers/relay-messenger/latest/message.received.json b/providers/relay-messenger/2026-08-30/message.received.json similarity index 100% rename from providers/relay-messenger/latest/message.received.json rename to providers/relay-messenger/2026-08-30/message.received.json diff --git a/providers/relay-messenger/latest/message.sent.json b/providers/relay-messenger/2026-08-30/message.sent.json similarity index 100% rename from providers/relay-messenger/latest/message.sent.json rename to providers/relay-messenger/2026-08-30/message.sent.json diff --git a/providers/relay-messenger/index.json b/providers/relay-messenger/index.json index ae0fac7a..70c6dbe0 100644 --- a/providers/relay-messenger/index.json +++ b/providers/relay-messenger/index.json @@ -1,11 +1,11 @@ { "label": "Relay", "configs": { - "latest_version": "latest", + "latest_version": "2026-08-30", "topic_identifier": "event_type" }, "provenance": { - "latest": { + "2026-08-30": { "sourced_via": "docs", "sourced_on": "2026-10-26" }