@portalsdk/wire-protocol 0.2.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.
package/dist/index.js ADDED
@@ -0,0 +1,160 @@
1
+ // src/constants.ts
2
+ var PROTOCOL_VERSION = 1;
3
+ var UPGRADE_PARAMS = {
4
+ version: "v",
5
+ token: "token",
6
+ leaf: "leaf",
7
+ meta: "meta",
8
+ last: "last"
9
+ };
10
+ var PORTAL_ERROR_HEADER = "x-portal-error";
11
+
12
+ // src/errors.ts
13
+ var REFUSAL_STATUS = {
14
+ invalid_token: 401,
15
+ token_expired: 401,
16
+ invalid_api_key: 403,
17
+ not_member: 403,
18
+ banned: 403,
19
+ anonymous_not_allowed: 403,
20
+ unknown_channel: 404,
21
+ unsupported_version: 426,
22
+ channel_at_capacity: 429
23
+ };
24
+ var REFUSAL_CODES = new Set(Object.keys(REFUSAL_STATUS));
25
+ var isRefusalCode = (value) => typeof value === "string" && REFUSAL_CODES.has(value);
26
+
27
+ // src/parse.ts
28
+ var isRec = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
29
+ var isStr = (v) => typeof v === "string";
30
+ var isBool = (v) => typeof v === "boolean";
31
+ var isNum = (v) => typeof v === "number" && Number.isFinite(v);
32
+ var opt = (v, check) => v === void 0 || check(v);
33
+ var arrayOf = (v, check) => Array.isArray(v) && v.every(check);
34
+ var isStrRec = (v) => isRec(v) && Object.values(v).every(isStr);
35
+ var isMention = (v) => isRec(v) && isStr(v["userId"]);
36
+ var isWireSender = (v) => isRec(v) && isStr(v["id"]) && isBool(v["anon"]) && opt(v["username"], isStr);
37
+ var isWireMessage = (v) => isRec(v) && isStr(v["id"]) && (v["seq"] === null || isNum(v["seq"])) && isStr(v["type"]) && isStr(v["kind"]) && "content" in v && isWireSender(v["sender"]) && isNum(v["timestamp"]) && opt(v["to"], isStr) && (v["mentions"] === void 0 || arrayOf(v["mentions"], isMention)) && isBool(v["retracted"]) && isBool(v["ephemeral"]);
38
+ var isChannelInfo = (v) => isRec(v) && isStr(v["id"]) && (v["mode"] === "standard" || v["mode"] === "broadcast") && opt(v["name"], isStr) && opt(v["meta"], isRec);
39
+ var isCapabilities = (v) => isRec(v) && opt(v["publish"], isBool) && opt(v["sendDirect"], isBool);
40
+ var isMeInfo = (v) => isRec(v) && isStr(v["id"]) && isBool(v["anon"]) && isRec(v["claims"]) && isCapabilities(v["capabilities"]);
41
+ var isParticipant = (v) => isRec(v) && isStr(v["userId"]) && isRec(v["claims"]);
42
+ var isReadyPresence = (v) => {
43
+ if (!isRec(v)) return false;
44
+ if (v["mode"] === "detailed") {
45
+ return arrayOf(v["participants"], isParticipant) && isNum(v["count"]);
46
+ }
47
+ if (v["mode"] === "aggregate") {
48
+ return isNum(v["count"]) && (v["recent"] === void 0 || Array.isArray(v["recent"]));
49
+ }
50
+ return false;
51
+ };
52
+ var isInboxEntryWire = (v) => isRec(v) && isStr(v["id"]) && opt(v["name"], isStr) && opt(v["meta"], isRec) && opt(v["latest"], isLatest) && isNum(v["unread"]) && isBool(v["muted"]) && isNum(v["at"]);
53
+ var isLatest = (v) => isRec(v) && isStr(v["text"]) && isRec(v["sender"]) && isStr(v["sender"]["id"]) && isNum(v["at"]);
54
+ var isInboxItemWire = (v) => isRec(v) && isStr(v["id"]) && isStr(v["type"]) && opt(v["title"], isStr) && "data" in v && opt(v["channelId"], isStr) && isNum(v["at"]) && isBool(v["read"]);
55
+ var CHANNEL_SERVER_FRAMES = {
56
+ ready: (v) => isChannelInfo(v["channel"]) && isMeInfo(v["me"]) && isNum(v["seq"]) && isStr(v["leaf"]) && isReadyPresence(v["presence"]) && opt(v["watermark"], isNum) && opt(v["ext"], isRec) && opt(v["bindings"], isStrRec),
57
+ batch: (v) => arrayOf(v["msgs"], isWireMessage),
58
+ retract: (v) => isStr(v["id"]) && isNum(v["seq"]) && opt(v["reason"], isStr),
59
+ presence: (v) => {
60
+ if (v["mode"] === "detailed") {
61
+ return arrayOf(v["joined"], isParticipant) && arrayOf(v["left"], isParticipant) && isNum(v["count"]);
62
+ }
63
+ if (v["mode"] === "aggregate") {
64
+ return isNum(v["count"]) && Array.isArray(v["recent"]);
65
+ }
66
+ return false;
67
+ },
68
+ activity: (v) => isStr(v["userId"]) && isStr(v["kind"]) && isNum(v["since"]),
69
+ direct: (v) => isWireMessage(v["msg"]),
70
+ reassign: (v) => isStr(v["leaf"]),
71
+ error: (v) => isStr(v["code"]) && opt(v["reason"], isStr) && opt(v["ref"], isStr),
72
+ pong: () => true
73
+ };
74
+ var INBOX_SERVER_FRAMES = {
75
+ ready: (v) => arrayOf(v["entries"], isInboxEntryWire) && arrayOf(v["items"], isInboxItemWire) && isNum(v["counter"]),
76
+ entry: (v) => isInboxEntryWire(v["entry"]),
77
+ item: (v) => isInboxItemWire(v["item"]),
78
+ counter: (v) => isNum(v["n"]),
79
+ pong: () => true
80
+ };
81
+ var CHANNEL_CLIENT_FRAMES = {
82
+ ephemeral: (v) => isStr(v["cl"]) && isStr(v["type"]) && "content" in v,
83
+ activity: (v) => isStr(v["kind"]),
84
+ watermark: (v) => isNum(v["seq"]),
85
+ meta: (v) => isRec(v["metadata"]),
86
+ ping: () => true
87
+ };
88
+ var INBOX_CLIENT_FRAMES = {
89
+ read: (v) => isStr(v["channelId"]),
90
+ "item.read": (v) => isStr(v["id"]),
91
+ "read.all": () => true,
92
+ mute: (v) => isStr(v["channelId"]) && isBool(v["muted"]),
93
+ ping: () => true
94
+ };
95
+ var validatorFor = (table, t) => Object.hasOwn(table, t) ? table[t] : void 0;
96
+ var parseJson = (raw) => {
97
+ try {
98
+ return JSON.parse(raw);
99
+ } catch {
100
+ return void 0;
101
+ }
102
+ };
103
+ var asFrameShape = (raw) => {
104
+ const value = parseJson(raw);
105
+ if (!isRec(value) || !isStr(value["t"])) return null;
106
+ return value;
107
+ };
108
+ function parseChannelFrame(raw) {
109
+ const frame = asFrameShape(raw);
110
+ if (frame === null) return null;
111
+ const validate = validatorFor(CHANNEL_SERVER_FRAMES, frame.t);
112
+ if (validate === void 0) return frame;
113
+ return validate(frame) ? frame : null;
114
+ }
115
+ function parseInboxFrame(raw) {
116
+ const frame = asFrameShape(raw);
117
+ if (frame === null) return null;
118
+ const validate = validatorFor(INBOX_SERVER_FRAMES, frame.t);
119
+ if (validate === void 0) return frame;
120
+ return validate(frame) ? frame : null;
121
+ }
122
+ function parseChannelClientFrame(raw) {
123
+ const frame = asFrameShape(raw);
124
+ if (frame === null) return null;
125
+ const validate = validatorFor(CHANNEL_CLIENT_FRAMES, frame.t);
126
+ if (validate === void 0) return frame;
127
+ return validate(frame) ? frame : null;
128
+ }
129
+ function parseInboxClientFrame(raw) {
130
+ const frame = asFrameShape(raw);
131
+ if (frame === null) return null;
132
+ const validate = validatorFor(INBOX_CLIENT_FRAMES, frame.t);
133
+ if (validate === void 0) return frame;
134
+ return validate(frame) ? frame : null;
135
+ }
136
+ function serializeFrame(frame) {
137
+ return JSON.stringify(frame);
138
+ }
139
+ var isKnownChannelFrame = (frame) => Object.hasOwn(CHANNEL_SERVER_FRAMES, frame.t);
140
+ var isKnownInboxFrame = (frame) => Object.hasOwn(INBOX_SERVER_FRAMES, frame.t);
141
+ var isKnownChannelClientFrame = (frame) => Object.hasOwn(CHANNEL_CLIENT_FRAMES, frame.t);
142
+ var isKnownInboxClientFrame = (frame) => Object.hasOwn(INBOX_CLIENT_FRAMES, frame.t);
143
+ var isChannelReady = (f) => f.t === "ready";
144
+ var isBatch = (f) => f.t === "batch";
145
+ var isRetract = (f) => f.t === "retract";
146
+ var isPresence = (f) => f.t === "presence";
147
+ var isActivity = (f) => f.t === "activity";
148
+ var isDirect = (f) => f.t === "direct";
149
+ var isReassign = (f) => f.t === "reassign";
150
+ var isError = (f) => f.t === "error";
151
+ var isPong = (f) => f.t === "pong";
152
+ var isInboxReady = (f) => f.t === "ready";
153
+ var isInboxEntry = (f) => f.t === "entry";
154
+ var isInboxItem = (f) => f.t === "item";
155
+ var isInboxCounter = (f) => f.t === "counter";
156
+ var isInboxPong = (f) => f.t === "pong";
157
+
158
+ export { PORTAL_ERROR_HEADER, PROTOCOL_VERSION, REFUSAL_STATUS, UPGRADE_PARAMS, isActivity, isBatch, isChannelReady, isDirect, isError, isInboxCounter, isInboxEntry, isInboxItem, isInboxPong, isInboxReady, isKnownChannelClientFrame, isKnownChannelFrame, isKnownInboxClientFrame, isKnownInboxFrame, isPong, isPresence, isReassign, isRefusalCode, isRetract, parseChannelClientFrame, parseChannelFrame, parseInboxClientFrame, parseInboxFrame, serializeFrame };
159
+ //# sourceMappingURL=index.js.map
160
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts","../src/errors.ts","../src/parse.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Protocol version carried by every upgrade as `?v=` (§1.1, §6).\n *\n * An unknown version is refused at the upgrade with HTTP 426 `unsupported_version`.\n * Within v1 the protocol evolves additively only; a breaking change bumps this.\n */\nexport const PROTOCOL_VERSION = 1;\n\n/**\n * Upgrade query-parameter names (§1.1). Build upgrade URLs from these rather than\n * string literals, so a rename is a compile error rather than a silent 4xx.\n *\n * - `version` — required on every upgrade; unknown → 426.\n * - `token` — the signed JWT. Identifies the user; the apiKey is resolved from it.\n * - `leaf` — opaque reconnect token; echo back what `ready` gave you, unchanged.\n * - `meta` — initial presence metadata, base64 JSON (standard channels; ≤1KB decoded).\n * - `last` — highest contiguous seq held, sent on reconnect to request replay (§1.4).\n */\nexport const UPGRADE_PARAMS = {\n version: \"v\",\n token: \"token\",\n leaf: \"leaf\",\n meta: \"meta\",\n last: \"last\",\n} as const;\n\n/**\n * Response header carrying the refusal code on a refused upgrade (§1.1).\n *\n * Duplicates `code` from the body so a client behind a body-eating proxy can still\n * tell why the socket never opened.\n */\nexport const PORTAL_ERROR_HEADER = \"x-portal-error\";\n","/**\n * Why an upgrade was refused (§1.1).\n *\n * Refusals happen at the HTTP upgrade — **the socket never opens**. They are therefore\n * disjoint from {@link PublishErrorCode} (HTTP publish rejections) and from the\n * in-session `error` frame, which both presuppose a working connection.\n */\nexport type RefusalCode =\n | \"invalid_token\"\n | \"token_expired\"\n | \"invalid_api_key\"\n | \"not_member\"\n | \"banned\"\n | \"anonymous_not_allowed\"\n | \"unknown_channel\"\n | \"unsupported_version\"\n | \"channel_at_capacity\";\n\n/**\n * The HTTP status each refusal is delivered with (§1.1 table).\n *\n * The mapping is many-to-one: status alone does not identify the cause, so read\n * `code` from the body (or the `x-portal-error` header) rather than branching on status.\n */\nexport const REFUSAL_STATUS = {\n invalid_token: 401,\n token_expired: 401,\n invalid_api_key: 403,\n not_member: 403,\n banned: 403,\n anonymous_not_allowed: 403,\n unknown_channel: 404,\n unsupported_version: 426,\n channel_at_capacity: 429,\n} as const satisfies Record<RefusalCode, number>;\n\nconst REFUSAL_CODES: ReadonlySet<string> = new Set(Object.keys(REFUSAL_STATUS));\n\n/**\n * Whether a value is a refusal code this version knows (§1.1).\n *\n * A refusal body arriving with an unrecognised code is not a refusal this client can\n * reason about — treat it as an opaque failure rather than coercing it.\n */\nexport const isRefusalCode = (value: unknown): value is RefusalCode =>\n typeof value === \"string\" && REFUSAL_CODES.has(value);\n\n/** Body of a refused upgrade (§1.1). */\nexport type RefusalBody = {\n code: RefusalCode;\n reason?: string;\n /**\n * Seconds to wait before retrying. Documented for `channel_at_capacity` (429) only\n * (§1.1); absent on every other refusal.\n */\n retryAfter?: number;\n};\n\n/**\n * Why an HTTP publish was rejected (§3.1).\n *\n * Deliberately NOT merged into {@link RefusalCode}: these arrive on a live connection\n * in response to `POST /v1/channels/{id}/messages`, and a client reacts to them per\n * send, not per connection.\n *\n * `blocked_by_middleware` carries user-visible copy in `reason`.\n */\nexport type PublishErrorCode =\n | \"not_permitted\"\n | \"blocked_by_middleware\"\n | \"content_too_large\"\n | \"rate_limited\";\n\n/**\n * Body of a rejected publish (§3.1).\n *\n * SPEC: §3.1 specifies the shape as `4xx { code, reason? }` but does not map each code\n * to a status, so no status record is exported here (unlike {@link REFUSAL_STATUS}).\n */\nexport type PublishErrorBody = {\n code: PublishErrorCode;\n reason?: string;\n};\n","import type {\n ActivityFrame,\n BatchFrame,\n ChannelClientFrame,\n ChannelReadyFrame,\n ChannelServerFrame,\n Capabilities,\n ChannelInfo,\n DirectFrame,\n ErrorFrame,\n MeInfo,\n PongFrame,\n PresenceFrame,\n ReadyPresence,\n ReassignFrame,\n RetractFrame,\n WirePresenceParticipant,\n} from \"./channel.js\";\nimport type {\n InboxClientFrame,\n InboxCounterFrame,\n InboxEntryFrame,\n InboxEntryWire,\n InboxItemFrame,\n InboxItemWire,\n InboxReadyFrame,\n InboxServerFrame,\n} from \"./inbox.js\";\nimport type {\n AnyFrame,\n ParsedChannelClientFrame,\n ParsedChannelFrame,\n ParsedInboxClientFrame,\n ParsedInboxFrame,\n UnknownFrame,\n} from \"./frames.js\";\nimport type { Mention, WireMessage, WireSender } from \"./message.js\";\n\n// ============================================================\n// Primitives\n// ============================================================\n\ntype Rec = Record<string, unknown>;\n\nconst isRec = (v: unknown): v is Rec =>\n typeof v === \"object\" && v !== null && !Array.isArray(v);\n\nconst isStr = (v: unknown): v is string => typeof v === \"string\";\nconst isBool = (v: unknown): v is boolean => typeof v === \"boolean\";\n\n/** JSON has no NaN/Infinity, so any non-finite number means a hand-built object. */\nconst isNum = (v: unknown): v is number =>\n typeof v === \"number\" && Number.isFinite(v);\n\n/** An absent optional field is valid; a present one must match. */\nconst opt = <T>(v: unknown, check: (x: unknown) => x is T): boolean =>\n v === undefined || check(v);\n\nconst arrayOf = <T>(v: unknown, check: (x: unknown) => x is T): v is T[] =>\n Array.isArray(v) && v.every(check);\n\nconst isStrRec = (v: unknown): v is Record<string, string> =>\n isRec(v) && Object.values(v).every(isStr);\n\n// ============================================================\n// Shape validators\n//\n// Each validator checks exactly the fields its type declares required — no more, no\n// less. Unknown fields are ignored and survive untouched (§6), but a frame missing a\n// required field is rejected rather than returned as a type that lies about it.\n// ============================================================\n\nconst isMention = (v: unknown): v is Mention => isRec(v) && isStr(v[\"userId\"]);\n\nconst isWireSender = (v: unknown): v is WireSender =>\n isRec(v) && isStr(v[\"id\"]) && isBool(v[\"anon\"]) && opt(v[\"username\"], isStr);\n\nconst isWireMessage = (v: unknown): v is WireMessage =>\n isRec(v) &&\n isStr(v[\"id\"]) &&\n (v[\"seq\"] === null || isNum(v[\"seq\"])) &&\n isStr(v[\"type\"]) &&\n isStr(v[\"kind\"]) &&\n \"content\" in v &&\n isWireSender(v[\"sender\"]) &&\n isNum(v[\"timestamp\"]) &&\n opt(v[\"to\"], isStr) &&\n (v[\"mentions\"] === undefined || arrayOf(v[\"mentions\"], isMention)) &&\n isBool(v[\"retracted\"]) &&\n isBool(v[\"ephemeral\"]);\n\nconst isChannelInfo = (v: unknown): v is ChannelInfo =>\n isRec(v) &&\n isStr(v[\"id\"]) &&\n (v[\"mode\"] === \"standard\" || v[\"mode\"] === \"broadcast\") &&\n opt(v[\"name\"], isStr) &&\n opt(v[\"meta\"], isRec);\n\nconst isCapabilities = (v: unknown): v is Capabilities =>\n isRec(v) && opt(v[\"publish\"], isBool) && opt(v[\"sendDirect\"], isBool);\n\nconst isMeInfo = (v: unknown): v is MeInfo =>\n isRec(v) &&\n isStr(v[\"id\"]) &&\n isBool(v[\"anon\"]) &&\n isRec(v[\"claims\"]) &&\n isCapabilities(v[\"capabilities\"]);\n\nconst isParticipant = (v: unknown): v is WirePresenceParticipant =>\n isRec(v) && isStr(v[\"userId\"]) && isRec(v[\"claims\"]);\n\nconst isReadyPresence = (v: unknown): v is ReadyPresence => {\n if (!isRec(v)) return false;\n if (v[\"mode\"] === \"detailed\") {\n return arrayOf(v[\"participants\"], isParticipant) && isNum(v[\"count\"]);\n }\n if (v[\"mode\"] === \"aggregate\") {\n return isNum(v[\"count\"]) && (v[\"recent\"] === undefined || Array.isArray(v[\"recent\"]));\n }\n return false;\n};\n\nconst isInboxEntryWire = (v: unknown): v is InboxEntryWire =>\n isRec(v) &&\n isStr(v[\"id\"]) &&\n opt(v[\"name\"], isStr) &&\n opt(v[\"meta\"], isRec) &&\n opt(v[\"latest\"], isLatest) &&\n isNum(v[\"unread\"]) &&\n isBool(v[\"muted\"]) &&\n isNum(v[\"at\"]);\n\nconst isLatest = (v: unknown): v is InboxEntryWire[\"latest\"] =>\n isRec(v) &&\n isStr(v[\"text\"]) &&\n isRec(v[\"sender\"]) &&\n isStr((v[\"sender\"] as Rec)[\"id\"]) &&\n isNum(v[\"at\"]);\n\nconst isInboxItemWire = (v: unknown): v is InboxItemWire =>\n isRec(v) &&\n isStr(v[\"id\"]) &&\n isStr(v[\"type\"]) &&\n opt(v[\"title\"], isStr) &&\n \"data\" in v &&\n opt(v[\"channelId\"], isStr) &&\n isNum(v[\"at\"]) &&\n isBool(v[\"read\"]);\n\n// ============================================================\n// Frame tables — the single source of truth for \"known `t`\"\n// ============================================================\n\ntype Validator = (v: Rec) => boolean;\n\nconst CHANNEL_SERVER_FRAMES: Record<ChannelServerFrame[\"t\"], Validator> = {\n ready: (v) =>\n isChannelInfo(v[\"channel\"]) &&\n isMeInfo(v[\"me\"]) &&\n isNum(v[\"seq\"]) &&\n isStr(v[\"leaf\"]) &&\n isReadyPresence(v[\"presence\"]) &&\n opt(v[\"watermark\"], isNum) &&\n opt(v[\"ext\"], isRec) &&\n opt(v[\"bindings\"], isStrRec),\n batch: (v) => arrayOf(v[\"msgs\"], isWireMessage),\n retract: (v) => isStr(v[\"id\"]) && isNum(v[\"seq\"]) && opt(v[\"reason\"], isStr),\n presence: (v) => {\n if (v[\"mode\"] === \"detailed\") {\n return (\n arrayOf(v[\"joined\"], isParticipant) &&\n arrayOf(v[\"left\"], isParticipant) &&\n isNum(v[\"count\"])\n );\n }\n if (v[\"mode\"] === \"aggregate\") {\n return isNum(v[\"count\"]) && Array.isArray(v[\"recent\"]);\n }\n return false;\n },\n activity: (v) => isStr(v[\"userId\"]) && isStr(v[\"kind\"]) && isNum(v[\"since\"]),\n direct: (v) => isWireMessage(v[\"msg\"]),\n reassign: (v) => isStr(v[\"leaf\"]),\n error: (v) => isStr(v[\"code\"]) && opt(v[\"reason\"], isStr) && opt(v[\"ref\"], isStr),\n pong: () => true,\n};\n\nconst INBOX_SERVER_FRAMES: Record<InboxServerFrame[\"t\"], Validator> = {\n ready: (v) =>\n arrayOf(v[\"entries\"], isInboxEntryWire) &&\n arrayOf(v[\"items\"], isInboxItemWire) &&\n isNum(v[\"counter\"]),\n entry: (v) => isInboxEntryWire(v[\"entry\"]),\n item: (v) => isInboxItemWire(v[\"item\"]),\n counter: (v) => isNum(v[\"n\"]),\n pong: () => true,\n};\n\n// C→S tables. A server (or the test mock) receives these; the same totality contract\n// applies as for the S→C tables. `ping` and `activity` also appear in S→C sets with\n// different shapes — the families are disjoint, so each is validated against its own\n// direction's table.\nconst CHANNEL_CLIENT_FRAMES: Record<ChannelClientFrame[\"t\"], Validator> = {\n ephemeral: (v) => isStr(v[\"cl\"]) && isStr(v[\"type\"]) && \"content\" in v,\n activity: (v) => isStr(v[\"kind\"]),\n watermark: (v) => isNum(v[\"seq\"]),\n meta: (v) => isRec(v[\"metadata\"]),\n ping: () => true,\n};\n\nconst INBOX_CLIENT_FRAMES: Record<InboxClientFrame[\"t\"], Validator> = {\n read: (v) => isStr(v[\"channelId\"]),\n \"item.read\": (v) => isStr(v[\"id\"]),\n \"read.all\": () => true,\n mute: (v) => isStr(v[\"channelId\"]) && isBool(v[\"muted\"]),\n ping: () => true,\n};\n\nconst validatorFor = <T extends string>(\n table: Record<T, Validator>,\n t: string,\n): Validator | undefined =>\n Object.hasOwn(table, t) ? table[t as T] : undefined;\n\n// ============================================================\n// Parse / serialize\n// ============================================================\n\nconst parseJson = (raw: string): unknown => {\n try {\n return JSON.parse(raw) as unknown;\n } catch {\n return undefined;\n }\n};\n\n/**\n * A frame-shaped value: an object carrying a string `t`. Anything else is not a frame.\n */\nconst asFrameShape = (raw: string): (Rec & { t: string }) | null => {\n const value = parseJson(raw);\n if (!isRec(value) || !isStr(value[\"t\"])) return null;\n return value as Rec & { t: string };\n};\n\n/**\n * Parse a text frame from a **channel** socket (S→C).\n *\n * Total and non-throwing:\n * - malformed JSON, a non-object, or a missing/non-string `t` → `null`\n * - a known `t` whose shape does not match → `null`\n * - an unknown `t` → the frame is returned intact as an {@link UnknownFrame} (§6),\n * because forward compatibility says ignore it, not lose it\n *\n * Unknown *fields* on known frames are preserved, so `parse` → {@link serializeFrame}\n * round-trips without dropping anything.\n */\nexport function parseChannelFrame(raw: string): ParsedChannelFrame | null {\n const frame = asFrameShape(raw);\n if (frame === null) return null;\n\n const validate = validatorFor(CHANNEL_SERVER_FRAMES, frame.t);\n if (validate === undefined) return frame as UnknownFrame;\n\n return validate(frame) ? (frame as ChannelServerFrame) : null;\n}\n\n/**\n * Parse a text frame from an **inbox** socket (S→C).\n *\n * Same contract as {@link parseChannelFrame}. The two families are disjoint: an inbox\n * `ready` and a channel `ready` share a `t` but not a shape, so a frame must be parsed\n * with the function matching the socket it arrived on.\n */\nexport function parseInboxFrame(raw: string): ParsedInboxFrame | null {\n const frame = asFrameShape(raw);\n if (frame === null) return null;\n\n const validate = validatorFor(INBOX_SERVER_FRAMES, frame.t);\n if (validate === undefined) return frame as UnknownFrame;\n\n return validate(frame) ? (frame as InboxServerFrame) : null;\n}\n\n/**\n * Parse a text frame a client sent on a **channel** socket (C→S).\n *\n * The upstream counterpart of {@link parseChannelFrame}, for a server or test mock\n * receiving the frames a client sends. Same totality contract: `null` on malformed JSON\n * / missing `t` / a known `t` with a bad shape, and an {@link UnknownFrame} passthrough\n * for an unrecognised `t` (§6).\n */\nexport function parseChannelClientFrame(\n raw: string,\n): ParsedChannelClientFrame | null {\n const frame = asFrameShape(raw);\n if (frame === null) return null;\n\n const validate = validatorFor(CHANNEL_CLIENT_FRAMES, frame.t);\n if (validate === undefined) return frame as UnknownFrame;\n\n return validate(frame) ? (frame as ChannelClientFrame) : null;\n}\n\n/**\n * Parse a text frame a client sent on an **inbox** socket (C→S).\n *\n * Same contract as {@link parseChannelClientFrame}.\n */\nexport function parseInboxClientFrame(raw: string): ParsedInboxClientFrame | null {\n const frame = asFrameShape(raw);\n if (frame === null) return null;\n\n const validate = validatorFor(INBOX_CLIENT_FRAMES, frame.t);\n if (validate === undefined) return frame as UnknownFrame;\n\n return validate(frame) ? (frame as InboxClientFrame) : null;\n}\n\n/**\n * Serialize a frame to a JSON text frame.\n *\n * Primarily for C→S sends. It accepts any frame so that a parsed S→C frame can be\n * re-serialized intact — the round-trip that proves unknown fields survive (§6).\n */\nexport function serializeFrame(frame: AnyFrame): string {\n return JSON.stringify(frame);\n}\n\n// ============================================================\n// Guards\n//\n// These narrow a frame that has already been through parseChannelFrame /\n// parseInboxFrame, which is what makes a `t` check sufficient: the parser has already\n// validated the shape, so narrowing on the discriminator cannot lie.\n// ============================================================\n\n/** True when the parser recognised this channel frame — i.e. it is not an `UnknownFrame`. */\nexport const isKnownChannelFrame = (\n frame: ParsedChannelFrame,\n): frame is ChannelServerFrame => Object.hasOwn(CHANNEL_SERVER_FRAMES, frame.t);\n\n/** True when the parser recognised this inbox frame — i.e. it is not an `UnknownFrame`. */\nexport const isKnownInboxFrame = (\n frame: ParsedInboxFrame,\n): frame is InboxServerFrame => Object.hasOwn(INBOX_SERVER_FRAMES, frame.t);\n\n/** True when the parser recognised this C→S channel frame (not an `UnknownFrame`). */\nexport const isKnownChannelClientFrame = (\n frame: ParsedChannelClientFrame,\n): frame is ChannelClientFrame => Object.hasOwn(CHANNEL_CLIENT_FRAMES, frame.t);\n\n/** True when the parser recognised this C→S inbox frame (not an `UnknownFrame`). */\nexport const isKnownInboxClientFrame = (\n frame: ParsedInboxClientFrame,\n): frame is InboxClientFrame => Object.hasOwn(INBOX_CLIENT_FRAMES, frame.t);\n\n// ── Channel S→C ─────────────────────────────────────────────\n\nexport const isChannelReady = (f: ParsedChannelFrame): f is ChannelReadyFrame =>\n f.t === \"ready\";\nexport const isBatch = (f: ParsedChannelFrame): f is BatchFrame => f.t === \"batch\";\nexport const isRetract = (f: ParsedChannelFrame): f is RetractFrame =>\n f.t === \"retract\";\nexport const isPresence = (f: ParsedChannelFrame): f is PresenceFrame =>\n f.t === \"presence\";\nexport const isActivity = (f: ParsedChannelFrame): f is ActivityFrame =>\n f.t === \"activity\";\nexport const isDirect = (f: ParsedChannelFrame): f is DirectFrame => f.t === \"direct\";\nexport const isReassign = (f: ParsedChannelFrame): f is ReassignFrame =>\n f.t === \"reassign\";\nexport const isError = (f: ParsedChannelFrame): f is ErrorFrame => f.t === \"error\";\nexport const isPong = (f: ParsedChannelFrame): f is PongFrame => f.t === \"pong\";\n\n// ── Inbox S→C ───────────────────────────────────────────────\n\nexport const isInboxReady = (f: ParsedInboxFrame): f is InboxReadyFrame =>\n f.t === \"ready\";\nexport const isInboxEntry = (f: ParsedInboxFrame): f is InboxEntryFrame =>\n f.t === \"entry\";\nexport const isInboxItem = (f: ParsedInboxFrame): f is InboxItemFrame =>\n f.t === \"item\";\nexport const isInboxCounter = (f: ParsedInboxFrame): f is InboxCounterFrame =>\n f.t === \"counter\";\nexport const isInboxPong = (f: ParsedInboxFrame): f is PongFrame => f.t === \"pong\";\n"]}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@portalsdk/wire-protocol",
3
+ "version": "0.2.0",
4
+ "description": "Wire format types and codecs for the Portal protocol.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/useportal/portal-sdk.git",
28
+ "directory": "packages/wire-protocol"
29
+ },
30
+ "scripts": {
31
+ "build": "tsup",
32
+ "test": "vitest run",
33
+ "typecheck": "tsc --noEmit",
34
+ "lint": "tsc --noEmit"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.18.8",
38
+ "tsup": "^8.5.0",
39
+ "typescript": "^5.9.3",
40
+ "vitest": "^3.2.4"
41
+ }
42
+ }