From 02c09838880e6125178c4b19e16ca83389823ee8 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Tue, 27 Oct 2026 08:48:45 +0200 Subject: [PATCH 1/4] test(channels): preserve newsletter metadata in JSON and event streams --- README.md | 18 ++ contracts/newsletter-observations.json | 13 ++ test/commands/newsletter-observations.test.ts | 168 ++++++++++++++++++ .../channel-message.json | 16 ++ .../newsletter-observations/delivery.json | 70 ++++++++ 5 files changed, 285 insertions(+) create mode 100644 contracts/newsletter-observations.json create mode 100644 test/commands/newsletter-observations.test.ts create mode 100644 test/fixtures/newsletter-observations/channel-message.json create mode 100644 test/fixtures/newsletter-observations/delivery.json diff --git a/README.md b/README.md index 4c14a508..8dfcd6ec 100644 --- a/README.md +++ b/README.md @@ -2740,3 +2740,21 @@ Campaign analytics shows `Skipped by frequency cap`; recipient output includes This implementation keeps the published SDK pin unchanged. Release waits for a matching SDK prerelease and accepted API contract; source support does not imply package publication or beta availability. + +## Channel update metadata + +Use the existing event stream to read `newsletter.update` observations as JSON lines: + +```sh +polymorfa event stream --owner project --project "$PROJECT_ID" --type newsletter.update --json +``` + +`live_update` messages contain reported counts and positions, with optional message +references. Missing counts are not zero. Preserve every typed or untyped view +value and unknown milestone type. Position-only observations do not provide +message action authority. The payload contains no message body. + +`channel message list --json` and `channel message updates --json` preserve a +reported `forwards` value, including zero. Event lists remain metadata-only; +`event view --include-payload --json` returns exact retained bytes only when the +API authorizes them. These commands do not request additional retention. diff --git a/contracts/newsletter-observations.json b/contracts/newsletter-observations.json new file mode 100644 index 00000000..0258b3f5 --- /dev/null +++ b/contracts/newsletter-observations.json @@ -0,0 +1,13 @@ +{ + "repository": "polymorfa/polymorfa", + "sourceCommit": "d60f08042a5e3f36e53a7b378be7b7e710e80e9f", + "sourcePath": "apps/api/docs/openapi.json", + "sourceSha256": "9253f7fcf7b6e802411cc2f9b3b28bbf7c1e45e90c2ebee7fc461f5627ca9a32", + "polymorfaVersion": "2026-09-22", + "sdkSourceCommit": "bc3c694d548b8550bface0f3d94a643affd06d80", + "localCandidateSdkArtifactSha256": "fc2b0ba01891c38956055e1719c726e73630d8fa8f778f6a6f1a69018d54d651", + "published": false, + "productionDependencyPinUnchanged": "0.1.0-dev.20261004024706", + "scope": "Existing channel JSON and event view/stream retain optional newsletter metadata; no new commands. Event list stays metadata-only.", + "releaseGate": "Matching accepted and published SDK required before a real CLI dependency-pin update; local artifact testing is not registry publication." +} diff --git a/test/commands/newsletter-observations.test.ts b/test/commands/newsletter-observations.test.ts new file mode 100644 index 00000000..6c1d854f --- /dev/null +++ b/test/commands/newsletter-observations.test.ts @@ -0,0 +1,168 @@ +import {readFileSync} from 'node:fs' +import {runCommand} from '@oclif/test' +import {afterEach, describe, expect, test} from 'vitest' +import { + startHttpServer, + startHttpSequenceServer, + withEnvironment, + type TestHttpServer, +} from '../helpers/http-server.js' +interface FixtureEnvelope { + readonly id: string + readonly session: string + readonly timestamp: string + readonly event: string + readonly payload: { + readonly id: string + readonly action: string + readonly muted?: boolean + readonly messages?: readonly { + readonly position: number + readonly forwards?: number + readonly reactionCounts?: Readonly> + }[] + readonly milestone?: {readonly type: string; readonly value: number} + } +} +const fixture = JSON.parse( + readFileSync( + new URL( + '../fixtures/newsletter-observations/delivery.json', + import.meta.url, + ), + 'utf8', + ), +) as {readonly events: readonly FixtureEnvelope[]} +const channel = JSON.parse( + readFileSync( + new URL( + '../fixtures/newsletter-observations/channel-message.json', + import.meta.url, + ), + 'utf8', + ), +) as Record +const PROJECT = '11111111-1111-4111-8111-111111111111' +const metadata = (index: number, includePayload: boolean) => ({ + id: fixture.events[index]!.id, + organizationId: 'org_1', + projectId: PROJECT, + type: 'newsletter.update', + source: 'runtime', + environment: 'development', + createdAt: fixture.events[index]!.timestamp, + payloadAvailability: includePayload ? 'available' : 'not_retained', + payload: includePayload + ? { + encoding: 'base64', + contentType: 'application/json', + data: Buffer.from(JSON.stringify(fixture.events[index])).toString( + 'base64', + ), + } + : null, + replayableUntil: null, + metadataExpiresAt: '2026-11-27T00:00:00.000Z', +}) +describe('newsletter observation consumers', () => { + let server: TestHttpServer | undefined + afterEach(async () => { + await server?.close() + server = undefined + }) + const environment = (run: () => Promise) => + withEnvironment( + { + POLYMORFA_API_KEY: + 'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + POLYMORFA_BASE_URL: server!.baseUrl, + POLYMORFA_CLIENT_TOKEN: undefined, + POLYMORFA_PROJECT_TOKEN: undefined, + }, + run, + ) + test.each(['list', 'updates'])( + 'retains reported forward zero in channel message %s JSON', + async (command) => { + server = await startHttpServer({body: {success: true, data: [channel]}}) + const result = await environment(() => + runCommand( + `channel message ${command} ${fixture.events[0]!.payload.id} --session number_1 --json`, + ), + ) + expect(result.error).toBeUndefined() + expect((JSON.parse(result.stdout) as {data: unknown}).data).toEqual([ + channel, + ]) + }, + ) + test('keeps event list metadata separate from explicitly requested retained payload bytes', async () => { + const listed = metadata(0, false), + detail = metadata(0, true) + server = await startHttpSequenceServer([ + {body: {data: [listed], page: {hasMore: false, nextCursor: null}}}, + {body: {data: detail}}, + ]) + const list = await environment(() => + runCommand( + `event list --owner project --project ${PROJECT} --type newsletter.update --json`, + ), + ) + expect(list.error).toBeUndefined() + expect((JSON.parse(list.stdout) as {data: unknown}).data).toEqual([listed]) + const view = await environment(() => + runCommand( + `event view ${detail.id} --owner project --project ${PROJECT} --include-payload --json`, + ), + ) + expect(view.error).toBeUndefined() + const bytes = (JSON.parse(view.stdout) as {data: {payload: {data: string}}}) + .data.payload.data + expect(JSON.parse(Buffer.from(bytes, 'base64').toString())).toEqual( + fixture.events[0], + ) + expect(server.requests[1]?.url).toContain('includePayload=true') + }) + test('preserves native live, milestone and legacy payloads through real SDK SSE into NDJSON', async () => { + const ready = { + v: 1, + type: 'ready', + streamId: 's', + sequence: 1, + heartbeatIntervalMs: 15000, + } + const frames = fixture.events + .map( + (_: unknown, index: number) => + `id: c${index}\nevent: event\ndata: ${JSON.stringify({v: 1, type: 'event', streamId: 's', sequence: index + 2, sentAt: fixture.events[index]!.timestamp, cursor: `c${index}`, event: metadata(index, true)})}\n\n`, + ) + .join('') + server = await startHttpSequenceServer([ + { + headers: {'content-type': 'text/event-stream'}, + rawBody: `event: ready\ndata: ${JSON.stringify(ready)}\n\n${frames}`, + }, + {status: 204}, + ]) + const result = await environment(() => + runCommand( + `event stream --owner project --project ${PROJECT} --type newsletter.update --max-events 3 --json`, + ), + ) + expect(result.error).toBeUndefined() + const lines = result.stdout + .trim() + .split('\n') + .map((line) => JSON.parse(line) as {webhook: FixtureEnvelope}) + expect(lines).toHaveLength(3) + expect(lines.map((line) => line.webhook)).toEqual(fixture.events) + expect(lines[0]?.webhook.payload.messages?.[0]?.forwards).toBe(0) + expect(lines[0]?.webhook.payload.messages?.[0]?.reactionCounts).toEqual({}) + expect(lines[0]?.webhook.payload.messages?.[1]).toEqual({position: 124}) + expect(lines[1]?.webhook.payload.milestone).toMatchObject({ + type: 'FUTURE_MILESTONE', + value: 0, + }) + expect(lines[2]?.webhook.payload.muted).toBe(false) + }) +}) diff --git a/test/fixtures/newsletter-observations/channel-message.json b/test/fixtures/newsletter-observations/channel-message.json new file mode 100644 index 00000000..1595b387 --- /dev/null +++ b/test/fixtures/newsletter-observations/channel-message.json @@ -0,0 +1,16 @@ +{ + "position": 123, + "id": "9007199254740994", + "whatsapp_ids": { + "linked_devices": "native-observed" + }, + "whatsapp_id": "native-observed", + "conversation": { + "id": "9007199254740993" + }, + "type": "text", + "timestamp": "2023-11-14T22:13:21Z", + "views": 11, + "forwards": 0, + "reactionCounts": {} +} diff --git a/test/fixtures/newsletter-observations/delivery.json b/test/fixtures/newsletter-observations/delivery.json new file mode 100644 index 00000000..22829e89 --- /dev/null +++ b/test/fixtures/newsletter-observations/delivery.json @@ -0,0 +1,70 @@ +{ + "secret": "synthetic-newsletter-secret", + "batchId": "0198a2cb-0000-7000-8000-000000000081", + "events": [ + { + "id": "newsletter-event-0", + "session": "channel-number", + "timestamp": "2026-10-27T00:00:00.000Z", + "event": "newsletter.update", + "payload": { + "id": "9007199254740993", + "action": "live_update", + "sourceTimestamp": "2023-11-14T22:13:20Z", + "messages": [ + { + "position": 123, + "message": { + "id": "9007199254740994", + "whatsapp_ids": { + "linked_devices": "native-observed" + } + }, + "timestamp": "2023-11-14T22:13:21Z", + "viewCounts": [ + { + "count": 0 + }, + { + "type": "views", + "count": 11 + } + ], + "forwards": 0, + "reactionCounts": {} + }, + { + "position": 124 + } + ] + } + }, + { + "id": "newsletter-event-1", + "session": "channel-number", + "timestamp": "2026-10-27T00:00:00.000Z", + "event": "newsletter.update", + "payload": { + "id": "9007199254740993", + "action": "milestone", + "milestone": { + "position": 123, + "type": "FUTURE_MILESTONE", + "value": 0, + "reactionCode": "👍" + } + } + }, + { + "id": "legacy-mute", + "session": "channel-number", + "timestamp": "2026-10-27T00:00:00.000Z", + "event": "newsletter.update", + "payload": { + "id": "9007199254740993", + "action": "mute", + "muted": false + } + } + ] +} From db9f803138110f19a26ec8a79eb0b9ee7f738883 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Tue, 27 Oct 2026 08:59:23 +0200 Subject: [PATCH 2/4] docs(contracts): record physical CLI and final SDK package proof --- contracts/newsletter-observations.json | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/contracts/newsletter-observations.json b/contracts/newsletter-observations.json index 0258b3f5..4a96972e 100644 --- a/contracts/newsletter-observations.json +++ b/contracts/newsletter-observations.json @@ -4,10 +4,23 @@ "sourcePath": "apps/api/docs/openapi.json", "sourceSha256": "9253f7fcf7b6e802411cc2f9b3b28bbf7c1e45e90c2ebee7fc461f5627ca9a32", "polymorfaVersion": "2026-09-22", - "sdkSourceCommit": "bc3c694d548b8550bface0f3d94a643affd06d80", + "sdkSourceCommit": "b39e76125dcb4816295285272b407f18f8a9962e", "localCandidateSdkArtifactSha256": "fc2b0ba01891c38956055e1719c726e73630d8fa8f778f6a6f1a69018d54d651", "published": false, "productionDependencyPinUnchanged": "0.1.0-dev.20261004024706", "scope": "Existing channel JSON and event view/stream retain optional newsletter metadata; no new commands. Event list stays metadata-only.", - "releaseGate": "Matching accepted and published SDK required before a real CLI dependency-pin update; local artifact testing is not registry publication." + "releaseGate": "Matching accepted and published SDK required before a real CLI dependency-pin update; local artifact testing is not registry publication.", + "qualification": { + "cliSource": "78c33475dae1883b9451c036df9cc6ae0630be90", + "cliArtifactSha256": "1fb43d831b475c069700ed3b3ebac5de8b49f6054805385f10d11d26c9d1b811", + "sdkArtifactSha256": "fc2b0ba01891c38956055e1719c726e73630d8fa8f778f6a6f1a69018d54d651", + "checks": [ + "channel list/update forwards zero", + "metadata-only event list", + "exact retained event bytes", + "three native SSE events to CLI NDJSON" + ], + "dependencyRebind": "Final SDK source changes only type assertions after the tested source; the physical package SHA-256 is identical, so runtime proof applies to the same bytes.", + "liveAcceptance": false + } } From d29749226218763340841eca6405c112d8b4a201 Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Tue, 27 Oct 2026 09:21:26 +0200 Subject: [PATCH 3/4] fix(deps): update Zod to clear the production audit --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b368c232..bf434af6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@oclif/core": "^4.13.5", "@polymorfa/sdk": "0.1.0-dev.20261004024706", "jq-wasm": "3.0.0-jq-1.8.2", - "zod": "^4.6.6" + "zod": "4.6.7" }, "bin": { "polymorfa": "bin/run.js" diff --git a/package.json b/package.json index d12d559b..baf09130 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@oclif/core": "^4.13.5", "@polymorfa/sdk": "0.1.0-dev.20261004024706", "jq-wasm": "3.0.0-jq-1.8.2", - "zod": "^4.6.6" + "zod": "4.6.7" }, "devDependencies": { "@eslint/js": "^9.39.5", From ec49f1948e0ff7d631aeb539593117b6a2ee566b Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Tue, 27 Oct 2026 09:28:48 +0200 Subject: [PATCH 4/4] docs(contracts): refresh the audited CLI package receipt --- contracts/newsletter-observations.json | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/contracts/newsletter-observations.json b/contracts/newsletter-observations.json index 4a96972e..c3a83355 100644 --- a/contracts/newsletter-observations.json +++ b/contracts/newsletter-observations.json @@ -4,15 +4,15 @@ "sourcePath": "apps/api/docs/openapi.json", "sourceSha256": "9253f7fcf7b6e802411cc2f9b3b28bbf7c1e45e90c2ebee7fc461f5627ca9a32", "polymorfaVersion": "2026-09-22", - "sdkSourceCommit": "b39e76125dcb4816295285272b407f18f8a9962e", + "sdkSourceCommit": "d7977377f97b9fa12b8837ba02bc30c6fb512733", "localCandidateSdkArtifactSha256": "fc2b0ba01891c38956055e1719c726e73630d8fa8f778f6a6f1a69018d54d651", "published": false, "productionDependencyPinUnchanged": "0.1.0-dev.20261004024706", "scope": "Existing channel JSON and event view/stream retain optional newsletter metadata; no new commands. Event list stays metadata-only.", "releaseGate": "Matching accepted and published SDK required before a real CLI dependency-pin update; local artifact testing is not registry publication.", "qualification": { - "cliSource": "78c33475dae1883b9451c036df9cc6ae0630be90", - "cliArtifactSha256": "1fb43d831b475c069700ed3b3ebac5de8b49f6054805385f10d11d26c9d1b811", + "cliSource": "131b02f1813e383140b26ef31bd7e8726367ebb0", + "cliArtifactSha256": "027e0af085db135ee295aa42d92d0f7e9d87cee4cad00cdf7f28c7d626c366fa", "sdkArtifactSha256": "fc2b0ba01891c38956055e1719c726e73630d8fa8f778f6a6f1a69018d54d651", "checks": [ "channel list/update forwards zero", @@ -20,7 +20,10 @@ "exact retained event bytes", "three native SSE events to CLI NDJSON" ], - "dependencyRebind": "Final SDK source changes only type assertions after the tested source; the physical package SHA-256 is identical, so runtime proof applies to the same bytes.", + "dependencyRebind": "Final SDK Rust README fix changes no TypeScript package bytes; the packed SHA-256 matches the artifact used by this physical CLI test.", + "fullCliTestsPassed": 1091, + "productionAuditVulnerabilities": 0, + "zod": "4.6.7", "liveAcceptance": false } }