@volter/twin-segment 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +154 -0
  2. package/client/segment-mirror.css +45 -0
  3. package/client/segment-mirror.tsx +154 -0
  4. package/dist/client/segment-mirror.bundle.js +342 -0
  5. package/dist/client/segment-mirror.css +45 -0
  6. package/dist/client/segment-mirror.d.ts +34 -0
  7. package/dist/client/segment-mirror.js +80 -0
  8. package/dist/client/segment-mirror.tsx +154 -0
  9. package/dist/src/cli.d.ts +2 -0
  10. package/dist/src/cli.js +34 -0
  11. package/dist/src/index.d.ts +8 -0
  12. package/dist/src/index.js +53 -0
  13. package/dist/src/segment-budget.d.ts +41 -0
  14. package/dist/src/segment-budget.js +112 -0
  15. package/dist/src/segment-capabilities.d.ts +12 -0
  16. package/dist/src/segment-capabilities.gen.d.ts +3 -0
  17. package/dist/src/segment-capabilities.gen.js +22 -0
  18. package/dist/src/segment-capabilities.js +907 -0
  19. package/dist/src/segment-conformance.d.ts +8 -0
  20. package/dist/src/segment-conformance.js +106 -0
  21. package/dist/src/segment-connector.d.ts +76 -0
  22. package/dist/src/segment-connector.js +226 -0
  23. package/dist/src/segment-mirror-ui.d.ts +42 -0
  24. package/dist/src/segment-mirror-ui.js +143 -0
  25. package/dist/src/segment-server.d.ts +25 -0
  26. package/dist/src/segment-server.js +90 -0
  27. package/dist/src/segment-surface.gen.d.ts +48 -0
  28. package/dist/src/segment-surface.gen.js +267 -0
  29. package/dist/src/segment-twin.d.ts +98 -0
  30. package/dist/src/segment-twin.js +543 -0
  31. package/package.json +59 -0
  32. package/src/cli.ts +30 -0
  33. package/src/index.ts +87 -0
  34. package/src/segment-budget.ts +138 -0
  35. package/src/segment-capabilities.gen.ts +25 -0
  36. package/src/segment-capabilities.ts +988 -0
  37. package/src/segment-conformance.ts +123 -0
  38. package/src/segment-connector.ts +233 -0
  39. package/src/segment-journey.uitest.ts +116 -0
  40. package/src/segment-mirror-ui.ts +157 -0
  41. package/src/segment-server.ts +95 -0
  42. package/src/segment-surface.gen.ts +277 -0
  43. package/src/segment-twin.ts +664 -0
@@ -0,0 +1,90 @@
1
+ import { serveHttp } from '@volter/world-core';
2
+ import { dropped, events, groups, handleSegmentTwinRequest, identities } from "./segment-twin.js";
3
+ import { statefulTwinManifest, worldNow } from '@volter/world-core';
4
+ /** The keyless discovery door's body: a frozen VALUE, built once at module load from
5
+ * constants only. Nothing here reads the clock or state, so every serve of GET /twin is
6
+ * byte-identical (serve-path determinism, R9). */
7
+ const BASE_MANIFEST = statefulTwinManifest({
8
+ vendor: 'segment',
9
+ twinOf: 'the Segment HTTP Tracking API',
10
+ stores: 'tracked events, identify calls, group calls and refused (dropped) messages',
11
+ identity: 'Authenticate as Segment does — the write key as HTTP Basic username, an OAuth bearer, or `writeKey` in the body; the twin accepts any non-sentinel credential.',
12
+ });
13
+ /** THE STORE DOOR's projections (R5c). The Segment HTTP Tracking API is WRITE-ONLY — every route
14
+ * is an ingest and none of them reads a message back — so these four named projections are the
15
+ * only way to observe what was accepted, and `dropped` is the only way to see what was
16
+ * accepted-and-refused (Segment answers 200 and silently drops; this twin records the reason).
17
+ * They are also what makes this pack's determinism checkable at the resource level rather than
18
+ * only at the manifest door. */
19
+ const STORES = { events, identities, groups, dropped };
20
+ const STORE_NAMES = Object.keys(STORES).sort();
21
+ /** The manifest EDUCATES: it names the store door the way the kernel adapter does. */
22
+ const MANIFEST = {
23
+ ...BASE_MANIFEST,
24
+ stores: STORE_NAMES,
25
+ doors: { ...BASE_MANIFEST.doors, store: 'GET /twin/store/<name>' },
26
+ };
27
+ /** The twin's HTTP face. Two things it must get right, both invisible to an in-process verify:
28
+ * 1. THE AUTHORIZATION HEADER IS PART OF THE PAYLOAD on this vendor. Two of Segment's three
29
+ * documented auth schemes live in that header (Basic with the write key as the username;
30
+ * OAuth Bearer), so a fetch that dropped it would make an analytics-node v1 client — which
31
+ * sends `auth: { username: writeKey }` and NO writeKey in the body — arrive anonymous.
32
+ * 2. An empty-body status must serve a genuinely EMPTY body: `Response.json(null)` puts the
33
+ * four bytes "null" on the wire (the A3-caught serialization class in
34
+ * var/line/mixpanel/REVIEW.md).
35
+ *
36
+ * FETCH-FIRST (runtime contract R12b): this is the pack's whole HTTP surface as a plain fetch,
37
+ * and `createSegmentTwinServer` is one line of `Bun.serve` around it. It is a CUSTOM fetch, not
38
+ * the kernel adapter (`createTwinFetchFromHandler`): the authorization threading (1) and the
39
+ * handler-throw guard (below) are this pack's own. The keyless `GET /twin` manifest door the
40
+ * adapter would have provided is mounted here by hand, answering the same
41
+ * `statefulTwinManifest` shape — discovery is a door every twin owes (serve-path determinism
42
+ * R9 replays it), and `/twin` shadows no Segment route: the Tracking API lives under `/v1/*`. */
43
+ export function createSegmentTwinFetch(options = {}) {
44
+ return async function segmentTwinFetch(request) {
45
+ try {
46
+ const url = new URL(request.url);
47
+ const cleanPath = url.pathname.replace(/\/+$/, '') || '/';
48
+ // The keyless discovery door, before any vendor dispatch: a pure function of the
49
+ // manifest VALUE — no clock, no state — so replaying it is byte-identical (R9).
50
+ if (request.method === 'GET' && cleanPath === '/twin') {
51
+ return Response.json(MANIFEST);
52
+ }
53
+ // THE STORE DOOR (R5c) — see STORES above. Read-only, deterministic, served by the same
54
+ // fetch the vendor paths come out of; `/twin/*` shadows no Segment route (`/v1/*`).
55
+ if (request.method === 'GET' && cleanPath.startsWith('/twin/store/')) {
56
+ const name = cleanPath.slice('/twin/store/'.length);
57
+ const store = STORES[name];
58
+ if (!store)
59
+ return Response.json({ error: 'unknown store', store: name, stores: STORE_NAMES }, { status: 404 });
60
+ return Response.json(store(options.root));
61
+ }
62
+ const res = await handleSegmentTwinRequest({
63
+ method: request.method,
64
+ path: url.pathname + url.search,
65
+ body: request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.text(),
66
+ contentType: request.headers.get('content-type') ?? undefined,
67
+ authorization: request.headers.get('authorization') ?? undefined,
68
+ // The WORLD instant (R9): `receivedAt` and every minted messageId come from the world
69
+ // clock, never wall time.
70
+ occurredAt: worldNow(),
71
+ root: options.root,
72
+ });
73
+ if (res.body === null || res.body === undefined)
74
+ return new Response(null, { status: res.status, headers: res.headers });
75
+ return Response.json(res.body, { status: res.status, headers: res.headers });
76
+ }
77
+ catch (err) {
78
+ // Never let a handler throw escape the fetch callback: under `bun test` that is reported as
79
+ // "unhandled between tests" and can stop later test registration, hiding the real defect
80
+ // (ADDING_A_TWIN §8). Answer the vendor's own error SHAPE.
81
+ return Response.json({ code: 'twin_internal_error', message: String(err) }, { status: 500 });
82
+ }
83
+ };
84
+ }
85
+ export async function createSegmentTwinServer(options = {}) {
86
+ return await serveHttp({
87
+ port: options.port ?? 0,
88
+ fetch: createSegmentTwinFetch(options),
89
+ });
90
+ }
@@ -0,0 +1,48 @@
1
+ export type TwinOp = {
2
+ id: string;
3
+ area: string;
4
+ method: string;
5
+ path: string;
6
+ summary: string;
7
+ pathParams: string[];
8
+ requiredQuery: string[];
9
+ requiredBody: string[];
10
+ successStatus: number;
11
+ responseSkeleton: unknown;
12
+ specFile: string;
13
+ evidence: string;
14
+ tier: string;
15
+ };
16
+ export declare const OPS: TwinOp[];
17
+ export type TwinRequest = {
18
+ method: string;
19
+ path: string;
20
+ body?: string;
21
+ };
22
+ export type TwinResponse = {
23
+ status: number;
24
+ body: unknown;
25
+ headers?: Record<string, string>;
26
+ };
27
+ export type OpHandler = (ctx: {
28
+ op: TwinOp;
29
+ params: Record<string, string>;
30
+ query: URLSearchParams;
31
+ body: unknown;
32
+ root?: string;
33
+ }) => TwinResponse | Promise<TwinResponse>;
34
+ /** Match one request against the ratified surface. PURE LOOKUP — dispatch belongs to the pack. */
35
+ export declare function matchOp(req: TwinRequest): {
36
+ op: TwinOp;
37
+ params: Record<string, string>;
38
+ query: URLSearchParams;
39
+ } | undefined;
40
+ /** First required query/body field missing, or undefined (empty for sdk-ratified surfaces —
41
+ * validation truth lives in the pack's semantics, grounded per op). */
42
+ export declare function missingRequired(op: TwinOp, query: URLSearchParams, body: unknown): string | undefined;
43
+ /** SCAFFOLD default error body — the A2 station replaces this with the vendor's REAL envelope. */
44
+ export declare const errorBody: (message: string, field?: string) => {
45
+ field?: string | undefined;
46
+ success: boolean;
47
+ message: string;
48
+ };
@@ -0,0 +1,267 @@
1
+ // GENERATED by scripts/sdk-compile.ts from var/line/segment/SURFACE.json — the vendor
2
+ // publishes no machine-readable spec; every op below carries the evidence it was ratified on.
3
+ // Data only; regenerate, never hand-edit.
4
+ export const OPS = [
5
+ {
6
+ "id": "alias",
7
+ "area": "alias",
8
+ "method": "POST",
9
+ "path": "/v1/alias",
10
+ "summary": "associate one identity with another: a `previousId` (userId or anonymousId) is aliased onto a new `userId`",
11
+ "pathParams": [],
12
+ "requiredQuery": [],
13
+ "requiredBody": [],
14
+ "successStatus": 200,
15
+ "responseSkeleton": null,
16
+ "specFile": "SURFACE.json (sdk/docs-ratified, no vendor spec exists)",
17
+ "evidence": "DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/libraries/server/http-api/index.md '## Alias' declares `POST https://api.segment.io/v1/alias` with a worked body {previousId, userId, timestamp, writeKey} and a field table naming previousId. Absent from @segment/analytics-node@2's wire surface (batched as type:'alias'); the SDK's AliasParams (src/app/types/params.ts) requires both userId and previousId.",
18
+ "tier": "common"
19
+ },
20
+ {
21
+ "id": "batch",
22
+ "area": "batch",
23
+ "method": "POST",
24
+ "path": "/v1/batch",
25
+ "summary": "the SERVER-side batched ingestion envelope every official server SDK sends: {batch:[<message>...], writeKey, sentAt}, each message discriminated by its own `type`",
26
+ "pathParams": [],
27
+ "requiredQuery": [],
28
+ "requiredBody": [],
29
+ "successStatus": 200,
30
+ "responseSkeleton": null,
31
+ "specFile": "SURFACE.json (sdk/docs-ratified, no vendor spec exists)",
32
+ "evidence": "SDK WIRE LITERAL. @segment/analytics-node@2.3.0 src/plugins/segmentio/publisher.ts:73-76 builds the one and only URL this SDK ever posts to: tryCreateFormattedUrl(host ?? 'https://api.segment.io', path ?? '/v1/batch'); the body at :243-247 is JSON.stringify({batch: events, writeKey: this._writeKey, sentAt: new Date()}). It is the SOLE real path literal in var/line/segment/dossier/wire.json pathLiterals (the other, '/bar', is a JSDoc example). Corroborated in a second first-party SDK (analytics-python segment/analytics/request.py: url = remove_trailing_slash(host or 'https://api.segment.io') + '/v1/batch') and in the vendor's own reference, github.com/segmentio/segment-docs src/connections/sources/catalog/libraries/server/http-api/index.md '## Batch'.",
33
+ "tier": "core"
34
+ },
35
+ {
36
+ "id": "browser_track",
37
+ "area": "track",
38
+ "method": "POST",
39
+ "path": "/v1/t",
40
+ "summary": "analytics.js's short-form tracking route on the api.segment.io/v1 base — the path a browser proxy must forward",
41
+ "pathParams": [],
42
+ "requiredQuery": [],
43
+ "requiredBody": [],
44
+ "successStatus": 200,
45
+ "responseSkeleton": null,
46
+ "specFile": "SURFACE.json (sdk/docs-ratified, no vendor spec exists)",
47
+ "evidence": "DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/libraries/website/javascript/custom-proxy.md:123-125 documents the Analytics.js `integrations['Segment.io'].apiHost` proxy as: '// POST https://MY-CUSTOM-API-PROXY.com/v1/t --> proxies to // https://api.segment.io/v1/t'. Only the `/v1/t` short form appears on a first-party page; the sibling short routes analytics.js may use for the other call types are NOT documented anywhere in segment-docs and are deliberately not invented here. No literal in @segment/analytics-node@2 (a server SDK; this is the browser route).",
48
+ "tier": "niche"
49
+ },
50
+ {
51
+ "id": "group",
52
+ "area": "group",
53
+ "method": "POST",
54
+ "path": "/v1/group",
55
+ "summary": "associate an identified user with a group (company/account/team) and record group traits",
56
+ "pathParams": [],
57
+ "requiredQuery": [],
58
+ "requiredBody": [],
59
+ "successStatus": 200,
60
+ "responseSkeleton": null,
61
+ "specFile": "SURFACE.json (sdk/docs-ratified, no vendor spec exists)",
62
+ "evidence": "DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/libraries/server/http-api/index.md '## Group' declares `POST https://api.segment.io/v1/group` with a worked body {userId, groupId, traits, timestamp, writeKey} and a field table naming groupId and group traits. Absent from @segment/analytics-node@2's wire surface (batched as type:'group').",
63
+ "tier": "core"
64
+ },
65
+ {
66
+ "id": "identify",
67
+ "area": "identify",
68
+ "method": "POST",
69
+ "path": "/v1/identify",
70
+ "summary": "tie a user to their actions and record `traits` about them",
71
+ "pathParams": [],
72
+ "requiredQuery": [],
73
+ "requiredBody": [],
74
+ "successStatus": 200,
75
+ "responseSkeleton": null,
76
+ "specFile": "SURFACE.json (sdk/docs-ratified, no vendor spec exists)",
77
+ "evidence": "DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/libraries/server/http-api/index.md '## Identify' declares `POST https://api.segment.io/v1/identify` with a worked body {userId, traits, context, timestamp, writeKey} and a field table (anonymousId, context, integrations, timestamp, identify traits, userId); the '## Selecting Destinations' section posts an integrations-bearing identify to the same URL. Absent from @segment/analytics-node@2's wire surface (that SDK sends type:'identify' messages inside /v1/batch).",
78
+ "tier": "core"
79
+ },
80
+ {
81
+ "id": "mobile_batch",
82
+ "area": "batch",
83
+ "method": "POST",
84
+ "path": "/v1/b",
85
+ "summary": "the MOBILE batch endpoint — a distinct route from /v1/batch, which the vendor reserves for server-side sending",
86
+ "pathParams": [],
87
+ "requiredQuery": [],
88
+ "requiredBody": [],
89
+ "successStatus": 200,
90
+ "responseSkeleton": null,
91
+ "specFile": "SURFACE.json (sdk/docs-ratified, no vendor spec exists)",
92
+ "evidence": "DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/libraries/mobile/apple/index.md:163 and src/connections/sources/catalog/libraries/mobile/react-native/index.md:781: 'you must forward the batched events to https://api.segment.io/v1/b. The https://api.segment.io/v1/batch endpoint is reserved for events arriving from server-side sending, and proxying to that endpoint for your mobile events may result in unexpected behavior.' Also react-native/index.md:91, the `proxy` option: 'a batch url to post to instead of https://api.segment.io/v1/b'. No literal in @segment/analytics-node@2 — this is the Swift/Kotlin/React-Native route, and no mobile SDK is pinned in this dossier.",
93
+ "tier": "common"
94
+ },
95
+ {
96
+ "id": "oauth_token",
97
+ "area": "oauth",
98
+ "method": "POST",
99
+ "path": "/token",
100
+ "summary": "OAuth 2.0 client-credentials token exchange on the regional authorization server: a signed RS256 JWT client assertion is traded for a short-lived access token",
101
+ "pathParams": [],
102
+ "requiredQuery": [],
103
+ "requiredBody": [],
104
+ "successStatus": 200,
105
+ "responseSkeleton": null,
106
+ "specFile": "SURFACE.json (sdk/docs-ratified, no vendor spec exists)",
107
+ "evidence": "SDK WIRE LITERAL (interpolated, which is why wire.json's plain-string grep missed it). @segment/analytics-node@2.3.0 src/lib/token-manager.ts requestAccessToken(): `const accessTokenEndpoint = ${this.authServer}/token`, POSTed as application/x-www-form-urlencoded with grant_type=client_credentials & client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer & client_assertion=<signed JWT> & scope; authServer defaults to 'https://oauth2.segment.io' (same file's constructor) and the EU value 'https://oauth2.eu1.segmentapis.com' is documented on src/lib/types.ts OAuthSettings.authServer. Corroborated by github.com/segmentio/segment-docs src/connections/oauth.md '## Obtain the access token', which names both regional authorization servers and the same four form parameters.",
108
+ "tier": "niche"
109
+ },
110
+ {
111
+ "id": "page",
112
+ "area": "page",
113
+ "method": "POST",
114
+ "path": "/v1/page",
115
+ "summary": "record a web page view, with optional category, name and properties",
116
+ "pathParams": [],
117
+ "requiredQuery": [],
118
+ "requiredBody": [],
119
+ "successStatus": 200,
120
+ "responseSkeleton": null,
121
+ "specFile": "SURFACE.json (sdk/docs-ratified, no vendor spec exists)",
122
+ "evidence": "DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/libraries/server/http-api/index.md '## Page' declares `POST https://api.segment.io/v1/page` with a worked body {userId, name, timestamp, writeKey} and a field table (anonymousId, context, integrations, page name, page properties, timestamp, userId). Absent from @segment/analytics-node@2's wire surface (batched as type:'page').",
123
+ "tier": "core"
124
+ },
125
+ {
126
+ "id": "pixel_alias",
127
+ "area": "pixel",
128
+ "method": "GET",
129
+ "path": "/v1/pixel/alias",
130
+ "summary": "Tracking Pixel API: an alias call carried in the query string, answering 200 with a 1x1 transparent GIF",
131
+ "pathParams": [],
132
+ "requiredQuery": [],
133
+ "requiredBody": [],
134
+ "successStatus": 200,
135
+ "responseSkeleton": null,
136
+ "specFile": "SURFACE.json (sdk/docs-ratified, no vendor spec exists)",
137
+ "evidence": "DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/libraries/server/pixel-tracking-api/index.md '#### Pixel Routes' enumerates exactly six routes as a literal block — /v1/pixel/identify, /v1/pixel/group, /v1/pixel/alias, /v1/pixel/page, /v1/pixel/screen, /v1/pixel/track — under the signature `https://api.segment.io/v1/pixel/<METHOD ENDPOINT>?data=<base64-ENCODED-JSON>`, with 'Each endpoint *always* responds with a 200 <empty-gif>, even if an error occurs'. Not in any SDK.",
138
+ "tier": "niche"
139
+ },
140
+ {
141
+ "id": "pixel_group",
142
+ "area": "pixel",
143
+ "method": "GET",
144
+ "path": "/v1/pixel/group",
145
+ "summary": "Tracking Pixel API: a group call carried in the query string, answering 200 with a 1x1 transparent GIF",
146
+ "pathParams": [],
147
+ "requiredQuery": [],
148
+ "requiredBody": [],
149
+ "successStatus": 200,
150
+ "responseSkeleton": null,
151
+ "specFile": "SURFACE.json (sdk/docs-ratified, no vendor spec exists)",
152
+ "evidence": "DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/libraries/server/pixel-tracking-api/index.md '#### Pixel Routes' enumerates exactly six routes as a literal block — /v1/pixel/identify, /v1/pixel/group, /v1/pixel/alias, /v1/pixel/page, /v1/pixel/screen, /v1/pixel/track — under the signature `https://api.segment.io/v1/pixel/<METHOD ENDPOINT>?data=<base64-ENCODED-JSON>`, with 'Each endpoint *always* responds with a 200 <empty-gif>, even if an error occurs'. Not in any SDK.",
153
+ "tier": "niche"
154
+ },
155
+ {
156
+ "id": "pixel_identify",
157
+ "area": "pixel",
158
+ "method": "GET",
159
+ "path": "/v1/pixel/identify",
160
+ "summary": "Tracking Pixel API: an identify call carried in the query string, answering 200 with a 1x1 transparent GIF",
161
+ "pathParams": [],
162
+ "requiredQuery": [],
163
+ "requiredBody": [],
164
+ "successStatus": 200,
165
+ "responseSkeleton": null,
166
+ "specFile": "SURFACE.json (sdk/docs-ratified, no vendor spec exists)",
167
+ "evidence": "DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/libraries/server/pixel-tracking-api/index.md '#### Pixel Routes' enumerates exactly six routes as a literal block — /v1/pixel/identify, /v1/pixel/group, /v1/pixel/alias, /v1/pixel/page, /v1/pixel/screen, /v1/pixel/track — under the signature `https://api.segment.io/v1/pixel/<METHOD ENDPOINT>?data=<base64-ENCODED-JSON>`, with 'Each endpoint *always* responds with a 200 <empty-gif>, even if an error occurs'. Not in any SDK.",
168
+ "tier": "niche"
169
+ },
170
+ {
171
+ "id": "pixel_page",
172
+ "area": "pixel",
173
+ "method": "GET",
174
+ "path": "/v1/pixel/page",
175
+ "summary": "Tracking Pixel API: a page call carried in the query string, answering 200 with a 1x1 transparent GIF",
176
+ "pathParams": [],
177
+ "requiredQuery": [],
178
+ "requiredBody": [],
179
+ "successStatus": 200,
180
+ "responseSkeleton": null,
181
+ "specFile": "SURFACE.json (sdk/docs-ratified, no vendor spec exists)",
182
+ "evidence": "DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/libraries/server/pixel-tracking-api/index.md '#### Pixel Routes' enumerates exactly six routes as a literal block — /v1/pixel/identify, /v1/pixel/group, /v1/pixel/alias, /v1/pixel/page, /v1/pixel/screen, /v1/pixel/track — under the signature `https://api.segment.io/v1/pixel/<METHOD ENDPOINT>?data=<base64-ENCODED-JSON>`, with 'Each endpoint *always* responds with a 200 <empty-gif>, even if an error occurs'. Not in any SDK.",
183
+ "tier": "niche"
184
+ },
185
+ {
186
+ "id": "pixel_screen",
187
+ "area": "pixel",
188
+ "method": "GET",
189
+ "path": "/v1/pixel/screen",
190
+ "summary": "Tracking Pixel API: a screen call carried in the query string, answering 200 with a 1x1 transparent GIF",
191
+ "pathParams": [],
192
+ "requiredQuery": [],
193
+ "requiredBody": [],
194
+ "successStatus": 200,
195
+ "responseSkeleton": null,
196
+ "specFile": "SURFACE.json (sdk/docs-ratified, no vendor spec exists)",
197
+ "evidence": "DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/libraries/server/pixel-tracking-api/index.md '#### Pixel Routes' enumerates exactly six routes as a literal block — /v1/pixel/identify, /v1/pixel/group, /v1/pixel/alias, /v1/pixel/page, /v1/pixel/screen, /v1/pixel/track — under the signature `https://api.segment.io/v1/pixel/<METHOD ENDPOINT>?data=<base64-ENCODED-JSON>`, with 'Each endpoint *always* responds with a 200 <empty-gif>, even if an error occurs'. Not in any SDK.",
198
+ "tier": "niche"
199
+ },
200
+ {
201
+ "id": "pixel_track",
202
+ "area": "pixel",
203
+ "method": "GET",
204
+ "path": "/v1/pixel/track",
205
+ "summary": "Tracking Pixel API: a track call carried in the query string (base64 `?data=` or plain params), answering 200 with a 1x1 transparent GIF",
206
+ "pathParams": [],
207
+ "requiredQuery": [],
208
+ "requiredBody": [],
209
+ "successStatus": 200,
210
+ "responseSkeleton": null,
211
+ "specFile": "SURFACE.json (sdk/docs-ratified, no vendor spec exists)",
212
+ "evidence": "DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/libraries/server/pixel-tracking-api/index.md gives the signature `https://api.segment.io/v1/pixel/<METHOD ENDPOINT>?data=<base64-ENCODED-JSON>`, lists /v1/pixel/track among its six '#### Pixel Routes', states 'Each endpoint *always* responds with a 200 <empty-gif>, even if an error occurs', and shows both the base64 img-tag form (:67) and the unencoded query-param form (:61). Corroborated at src/connections/sources/custom-domain.md:79 and src/guides/how-to-guides/cross-channel-tracking.md:77. Not in any SDK: this route exists for email/ad contexts where JavaScript and POST are impossible.",
213
+ "tier": "niche"
214
+ },
215
+ {
216
+ "id": "screen",
217
+ "area": "screen",
218
+ "method": "POST",
219
+ "path": "/v1/screen",
220
+ "summary": "record a mobile app screen view, with optional category, name and properties",
221
+ "pathParams": [],
222
+ "requiredQuery": [],
223
+ "requiredBody": [],
224
+ "successStatus": 200,
225
+ "responseSkeleton": null,
226
+ "specFile": "SURFACE.json (sdk/docs-ratified, no vendor spec exists)",
227
+ "evidence": "DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/libraries/server/http-api/index.md '## Screen' declares `POST https://api.segment.io/v1/screen` with a worked body {userId, name, timestamp, writeKey}. @segment/analytics-node@2 exposes a .screen() method (src/app/analytics-node.ts) and its SegmentEventType union includes 'screen' (src/app/types/segment-event.ts), but the message still travels inside /v1/batch — no per-type path literal ships.",
228
+ "tier": "common"
229
+ },
230
+ {
231
+ "id": "track",
232
+ "area": "track",
233
+ "method": "POST",
234
+ "path": "/v1/track",
235
+ "summary": "record an action a user performed: required `event` name plus optional `properties`",
236
+ "pathParams": [],
237
+ "requiredQuery": [],
238
+ "requiredBody": [],
239
+ "successStatus": 200,
240
+ "responseSkeleton": null,
241
+ "specFile": "SURFACE.json (sdk/docs-ratified, no vendor spec exists)",
242
+ "evidence": "DOCS ONLY, NOT VERIFIED OFFLINE. github.com/segmentio/segment-docs src/connections/sources/catalog/libraries/server/http-api/index.md '## Track' declares `POST https://api.segment.io/v1/track` with a worked JSON body {userId, event, properties, context, timestamp, writeKey}; the same page's '### writeKey authentication' and '#### OAuth' curl examples both post to that exact URL. NO path literal for it exists in @segment/analytics-node@2 — that SDK batches every call type onto /v1/batch — so this entry is grounded in the vendor's reference page, not in shipped SDK bytes.",
243
+ "tier": "core"
244
+ }
245
+ ];
246
+ const compiled = OPS.map((op) => ({ op, re: new RegExp('^' + op.path.replace(/[.*+?^$()|[\]\\]/g, '\\$&').replace(/\{([^}]+)\}/g, '(?<$1>[^/]+)') + '$') }));
247
+ /** Match one request against the ratified surface. PURE LOOKUP — dispatch belongs to the pack. */
248
+ export function matchOp(req) {
249
+ const url = new URL(req.path, 'http://twin.local');
250
+ const match = compiled.find((c) => c.op.method === req.method.toUpperCase() && c.re.test(url.pathname));
251
+ if (!match)
252
+ return undefined;
253
+ return { op: match.op, params: { ...(match.re.exec(url.pathname)?.groups ?? {}) }, query: url.searchParams };
254
+ }
255
+ /** First required query/body field missing, or undefined (empty for sdk-ratified surfaces —
256
+ * validation truth lives in the pack's semantics, grounded per op). */
257
+ export function missingRequired(op, query, body) {
258
+ for (const q of op.requiredQuery)
259
+ if (!query.has(q))
260
+ return q;
261
+ for (const f of op.requiredBody)
262
+ if (!body || typeof body !== 'object' || body[f] === undefined)
263
+ return f;
264
+ return undefined;
265
+ }
266
+ /** SCAFFOLD default error body — the A2 station replaces this with the vendor's REAL envelope. */
267
+ export const errorBody = (message, field) => ({ success: false, message, ...(field ? { field } : {}) });
@@ -0,0 +1,98 @@
1
+ import { type OpHandler, type TwinResponse } from './segment-surface.gen.js';
2
+ export type SegmentTwinRequest = {
3
+ method: string;
4
+ path: string;
5
+ body?: string;
6
+ root?: string;
7
+ /** Captured, deliberately NOT enforced: the vendor says a content-type of 'application/json'
8
+ * "must be set" but never states what it does when one is absent or wrong, so inventing a
9
+ * refusal here would be surface Segment does not have. Filed as
10
+ * segment.api.limits.content_type_required. */
11
+ contentType?: string;
12
+ /** The raw Authorization header, when the caller sent one (Basic or Bearer). */
13
+ authorization?: string;
14
+ /** The WORLD instant this request is served at. Defaults to the world clock (R9). */
15
+ occurredAt?: string;
16
+ };
17
+ type Row = Record<string, unknown> & {
18
+ type?: string;
19
+ id?: string;
20
+ };
21
+ /** Every accepted message, oldest-first by ingestion ordinal. One row per Segment `messageId`. */
22
+ export declare function events(root?: string): Row[];
23
+ /** Users as identify (and alias) have folded them: merged traits + the ids they answer to. */
24
+ export declare function identities(root?: string): Row[];
25
+ /** Groups as `group` calls have folded them: merged group traits. */
26
+ export declare function groups(root?: string): Row[];
27
+ /** Messages the vendor ACCEPTS (200) and then drops. Twin-local observability, not vendor surface. */
28
+ export declare function dropped(root?: string): Row[];
29
+ /** Segment's error body. The SHAPE is grounded — analytics-python reads `payload["code"]` and
30
+ * `payload["message"]` off any non-200 (segment/analytics/request.py) — but the vendor
31
+ * publishes no vocabulary of `code` VALUES. The single documented literal is
32
+ * `no_user_anon_id` (http-api/index.md '## Errors'); every other code below is descriptive and
33
+ * is NOT claimed to be the vendor's own string. The gap is filed as
34
+ * segment.api.errors.code_vocabulary and no capability asserts a code the vendor never printed. */
35
+ export declare const segmentError: (status: number, code: string, message: string) => TwinResponse;
36
+ /** Documented drop reasons. Only `no_user_anon_id` is a vendor literal (see above). */
37
+ export type DropReason = 'no_user_anon_id' | 'track_missing_event' | 'group_missing_group_id' | 'alias_missing_previous_id' | 'unknown_message_type' | 'event_exceeds_32kb' | 'batch_exceeds_2500_events';
38
+ export declare const SEGMENT_MAX_REQUEST_BYTES: number;
39
+ export declare const SEGMENT_MAX_BATCH_BYTES: number;
40
+ export declare const SEGMENT_MAX_BATCH_EVENTS = 2500;
41
+ export type ResolvedAuth = {
42
+ writeKey: string | null;
43
+ scheme: 'body' | 'basic' | 'oauth' | 'none';
44
+ bearer: string | null;
45
+ };
46
+ /** Resolve the write key from whichever of the three documented schemes the caller used.
47
+ * The BODY wins when it carries a writeKey, because the docs' OAuth example keeps the writeKey
48
+ * in the payload while the header carries a different credential entirely; Basic is consulted
49
+ * only when the body has none, which is exactly the analytics-node v1 shape. */
50
+ export declare function resolveWriteKey(authorization: string | undefined, body: unknown): ResolvedAuth;
51
+ export type SegmentMessage = Record<string, unknown>;
52
+ export type SegmentEnvelope = {
53
+ messages: SegmentMessage[];
54
+ /** Batch-level context/integrations, merged into each message (see MERGE below). */
55
+ context: Record<string, unknown> | undefined;
56
+ integrations: Record<string, unknown> | undefined;
57
+ writeKey: string | null;
58
+ scheme: ResolvedAuth['scheme'];
59
+ sentAt: string | null;
60
+ /** true when the payload arrived through /v1/batch (a list) rather than a direct route. */
61
+ batched: boolean;
62
+ };
63
+ /** The five message types this twin models, plus the one it ratifies and refuses BY NAME.
64
+ * The union is the SDK's own: `type SegmentEventType = 'track' | 'page' | 'identify' | 'alias'
65
+ * | 'screen'` (src/app/types/segment-event.ts) widened with 'group', which the SDK's
66
+ * `Analytics.group()` produces through CoreEventFactory. */
67
+ export declare const MODELED_TYPES: readonly ["track", "identify", "page", "group", "alias"];
68
+ export type ModeledType = (typeof MODELED_TYPES)[number];
69
+ /** Ratified in SURFACE.json, deliberately not modeled: it earns the loud gap by name. */
70
+ export declare const UNMODELED_TYPES: readonly ["screen"];
71
+ /** THE per-message dispatcher. Every route funnels through it, so `/v1/batch`, `/v1/track` and
72
+ * their siblings can never disagree about what a message means.
73
+ *
74
+ * Two refusal modes, and the difference is deliberate:
75
+ * • a DOCUMENTED rejection (no identifier, Track without a name, an over-cap or oversize
76
+ * batch member) is ACCEPTED-AND-DROPPED — 200, nothing folded into `event` — because that is
77
+ * what Segment does: "Segment returns a 200 response for all API requests except errors
78
+ * caused by large payloads and JSON errors", and its list of silently-rejected events names
79
+ * exactly these cases;
80
+ * • a message whose `type` this twin has RATIFIED but not MODELED (today: `screen`) fails
81
+ * LOUDLY by name and the WHOLE payload is refused with nothing stored. That is a deliberate
82
+ * deviation from the vendor's 200 (ruled:
83
+ * denominator:200-for-nearly-everything-so-the-twin-gap-is-a-deliberate-deviation): a silent
84
+ * 200 storing nothing is indistinguishable from success, which is the one thing a twin may
85
+ * never do. Validation of every message therefore precedes ANY fold. */
86
+ export declare function ingest(env: SegmentEnvelope, endpoint: string, root: string | undefined, at: string): Promise<TwinResponse>;
87
+ /** The ctx a segment SEMANTICS handler gets: the generated one plus the WORLD instant. The
88
+ * generated surface is data + pure helpers and never carries a clock, so the instant is threaded
89
+ * here — `receivedAt` and every minted messageId derive from it (R9). */
90
+ type SegmentOpHandler = (ctx: Parameters<OpHandler>[0] & {
91
+ occurredAt: string;
92
+ }) => ReturnType<OpHandler>;
93
+ export declare const SEMANTICS: Record<string, SegmentOpHandler>;
94
+ /** Decode a request body into the shared envelope. `/v1/batch` carries `{batch: [...]}`; a
95
+ * direct route carries ONE message at the top level, with the writeKey alongside it. */
96
+ export declare function decodeEnvelope(parsed: unknown, auth: ResolvedAuth, isBatchRoute: boolean): SegmentEnvelope | undefined;
97
+ export declare function handleSegmentTwinRequest(req: SegmentTwinRequest): Promise<TwinResponse>;
98
+ export {};