@oxpulse/chat-sdk 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.
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # @oxpulse/chat-sdk
2
+
3
+ TypeScript client for the OxPulse encrypted chat message log API.
4
+
5
+ ## Install
6
+
7
+ > Publishing to npm is deferred to W8 (embed widget wave). Until then use a
8
+ > local path dependency or build from source.
9
+
10
+ ```bash
11
+ # After W8 publish:
12
+ npm install @oxpulse/chat-sdk
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { SDKChatClient } from '@oxpulse/chat-sdk';
19
+
20
+ // JWT obtained from POST /api/sdk/tokens (server-side mint).
21
+ const client = new SDKChatClient({
22
+ baseUrl: 'https://chat.example.com',
23
+ jwt: 'raw-jwt-here', // do NOT include "Bearer " prefix
24
+ });
25
+
26
+ // Send a sealed (E2EE) message.
27
+ const { seq, msgId } = await client.send('room-123', {
28
+ senderUid: 'user-1',
29
+ sealed: ciphertextArrayBuffer,
30
+ });
31
+
32
+ // List historical messages.
33
+ const rows = await client.list('room-123', { afterSeq: 0, limit: 100 });
34
+
35
+ // Subscribe to live messages via SSE.
36
+ // Auth uses a short-lived ticket (RFC 6750 compliant — no JWT in URL).
37
+ const teardown = client.subscribe('room-123', {
38
+ onMessage: (row) => {
39
+ // row.sealed — ciphertext as ArrayBuffer; pass to your E2EE decrypt function.
40
+ console.log('new message seq=%d', row.seq);
41
+ },
42
+ onError: (err) => console.error('SSE error', err),
43
+ });
44
+
45
+ // Stop subscribing.
46
+ teardown();
47
+ ```
48
+
49
+ ## Error model
50
+
51
+ All failures throw `SDKChatError` with a typed `code` field:
52
+
53
+ | Code | When |
54
+ |------|------|
55
+ | `unauthorized` | 401 — invalid or expired JWT / ticket |
56
+ | `forbidden` | 403 — missing scope |
57
+ | `not_found` | 404 |
58
+ | `rate_limited` | 429 |
59
+ | `invalid_args` | 400–4xx (other than above) |
60
+ | `server_error` | 5xx |
61
+ | `network` | fetch/network-level failure |
62
+
63
+ ## License
64
+
65
+ AGPL-3.0-or-later. See root [LICENSE](../../LICENSE).
66
+
67
+ ## Compression (optional)
68
+
69
+ By default all outgoing `POST /api/sdk/messages` requests use plain JSON
70
+ (`compression: 'none'`). Enable zstd compression to reduce payload size:
71
+
72
+ ```ts
73
+ import { SDKChatClient } from '@oxpulse/chat-sdk';
74
+
75
+ const client = new SDKChatClient({
76
+ baseUrl: 'https://chat.example.com',
77
+ jwt: 'jwt...',
78
+ compression: 'auto', // zstd dictless (0xC6) when payload ≥ 256 B
79
+ });
80
+
81
+ // Or use a shared dictionary for better compression on RU/FA/EN content:
82
+ const client2 = new SDKChatClient({
83
+ baseUrl: 'https://chat.example.com',
84
+ jwt: 'jwt...',
85
+ compression: 'dict',
86
+ dictBaseUrl: '/dicts', // served at /dicts/zstd-dict-ru-v1.zstd etc.
87
+ dictHint: 'zstd-dict-ru-v1',
88
+ });
89
+ ```
90
+
91
+ See `@oxpulse/wire-codec` README for codec internals, dict management, and
92
+ the server-side frame format.
93
+
94
+ ## CSP Compatibility
95
+
96
+ `@oxpulse/chat-sdk` is **strict-CSP-safe by design** — zero `eval()`, zero
97
+ `new Function()`. Tested against:
98
+
99
+ ```
100
+ script-src 'self' 'wasm-unsafe-eval' 'nonce-...' 'strict-dynamic'
101
+ ```
102
+
103
+ (no `'unsafe-eval'`, no `'unsafe-inline'`).
104
+
105
+ The SDK does NOT use zod internally — types are exported but runtime
106
+ validation is left to the integrator. This means no zod `Function()` probe
107
+ in the SDK bundle itself.
108
+
109
+ ### If you bundle the SDK with eval-using libraries
110
+
111
+ If your own bundle pulls in libraries that call `Function()`, you'll hit CSP
112
+ violations unrelated to the SDK. Common culprits and mitigations:
113
+
114
+ - **zod v4 (default mode):** calls `new Function(...)` at schema-build time
115
+ to JIT-compile validators. Set:
116
+ ```ts
117
+ // app boot script — BEFORE any module-level z.object() executes:
118
+ globalThis.__zod_globalConfig = { jitless: true };
119
+ ```
120
+ Putting `zod.config({ jitless: true })` inside an instance file is too
121
+ late — module-level `z.object()` already ran during import.
122
+
123
+ - **cbor-x (default import):** JIT-compiles decoders via `new Function`.
124
+ Import from the no-eval subpath instead:
125
+ ```ts
126
+ import { Encoder, decode } from 'cbor-x/index-no-eval';
127
+ ```
128
+ `@oxpulse/wire-codec` (a transitive dep of this SDK) already uses
129
+ `cbor-x/index-no-eval` internally — you only need this fix if you import
130
+ `cbor-x` directly in your application code.
131
+
132
+ ### Verifying your bundle
133
+
134
+ Scan your built output before shipping:
135
+
136
+ ```bash
137
+ grep -E 'new Function|eval\(' dist/**/*.js
138
+ ```
139
+
140
+ A build-time test that asserts the SDK bundle stays clean lives at
141
+ `packages/chat-sdk/src/__tests__/csp-cleanliness.test.ts`. Run it after
142
+ `npm run build` to catch regressions introduced by dependency updates.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * SDKChatClient — standalone npm package implementation.
3
+ *
4
+ * This is a standalone copy of the HTTP client for use by third-party
5
+ * integrations that import @oxpulse/chat-sdk directly. The SvelteKit
6
+ * front-end uses web/src/lib/api/sdkChat.ts (same semantics, same wire
7
+ * protocol) — kept separate to avoid pulling in SvelteKit build tooling.
8
+ *
9
+ * W4 skeleton: full implementation mirrors web/src/lib/api/sdkChat.ts.
10
+ * Publishing to npmjs.org is deferred to W8 (embed widget wave).
11
+ */
12
+ import type { SDKChatClientOptions, SendArgs, ListArgs, MessageRow, SubscribeArgs, Room, Member, CreateRoomArgs, UpdateRoomArgs, PresenceUser } from './types.js';
13
+ export declare class SDKChatClient {
14
+ #private;
15
+ constructor(opts: SDKChatClientOptions);
16
+ /**
17
+ * Encode a payload to the bytes that would be sent on the wire, without sending.
18
+ * Mirrors the encoding _encodeBody applies to POST /api/sdk/messages.
19
+ */
20
+ encodeEnvelope(payload: unknown): Promise<string | Uint8Array>;
21
+ /**
22
+ * Decode bytes received from the server back to the JSON payload.
23
+ * Server responses are always plain JSON; this also handles wire-codec
24
+ * compressed frames for completeness (round-trip with encodeEnvelope).
25
+ *
26
+ * Note: #compression governs OUTGOING encoding only. A server may send a
27
+ * compressed frame (0xC6/0xC7) regardless of the client's compression setting.
28
+ * decodeEnvelope checks the first byte directly and initializes zstd if needed.
29
+ */
30
+ decodeEnvelope(bytes: Uint8Array | string): Promise<unknown>;
31
+ send(roomId: string, args: SendArgs): Promise<{
32
+ seq: number;
33
+ msgId: string;
34
+ }>;
35
+ list(roomId: string, args?: ListArgs): Promise<MessageRow[]>;
36
+ subscribe(roomId: string, args: SubscribeArgs): () => void;
37
+ /**
38
+ * Broadcast a typing indicator for roomId.
39
+ * Wire-contract: POST /api/sdk/rooms/:room_id/typing
40
+ * Body: { ttl_secs? }
41
+ */
42
+ sendTyping(roomId: string, ttlSecs?: number): Promise<void>;
43
+ /**
44
+ * Send a presence heartbeat for roomId.
45
+ * Wire-contract: POST /api/sdk/rooms/:room_id/presence
46
+ * Body: {}
47
+ */
48
+ sendPresence(roomId: string): Promise<void>;
49
+ /**
50
+ * Fetch presence snapshot for roomId.
51
+ * Wire-contract: GET /api/sdk/rooms/:room_id/presence
52
+ * Returns: Array<PresenceUser>
53
+ */
54
+ getPresence(roomId: string): Promise<PresenceUser[]>;
55
+ /**
56
+ * Mark messages up to seq as read (monotonic — cannot regress).
57
+ * Wire-contract: POST /api/sdk/rooms/:room_id/read?seq=N
58
+ */
59
+ markRead(roomId: string, seq: number): Promise<void>;
60
+ /**
61
+ * Create a room. Idempotent per roomId.
62
+ * Wire-contract: POST /api/sdk/rooms
63
+ */
64
+ createRoom(args?: CreateRoomArgs): Promise<Room>;
65
+ /**
66
+ * Fetch room metadata + active members.
67
+ * Wire-contract: GET /api/sdk/rooms/:room_id
68
+ */
69
+ getRoom(roomId: string): Promise<Room>;
70
+ /**
71
+ * Update room title / metadata. Owner-only.
72
+ * Wire-contract: PATCH /api/sdk/rooms/:room_id
73
+ */
74
+ updateRoom(roomId: string, args: UpdateRoomArgs): Promise<Room>;
75
+ /**
76
+ * List active members of a room (fetches room and returns members).
77
+ */
78
+ listMembers(roomId: string): Promise<Member[]>;
79
+ /**
80
+ * Add a user to a room.
81
+ * Wire-contract: POST /api/sdk/rooms/:room_id/members
82
+ * Body: { user_id, role? }
83
+ */
84
+ addMember(roomId: string, userId: string, role?: string): Promise<Member>;
85
+ /**
86
+ * Remove a user from a room (soft-delete).
87
+ * Wire-contract: DELETE /api/sdk/rooms/:room_id/members/:user_id
88
+ */
89
+ removeMember(roomId: string, userId: string): Promise<void>;
90
+ /**
91
+ * Fetch all reply messages in a thread.
92
+ * Wire-contract: GET /api/sdk/rooms/:room_id/threads/:root_msg_id
93
+ * Returns: Array<MessageRow> sorted by seq ascending (server-side ORDER BY seq).
94
+ * Requires scope: chat:read:<room_id>.
95
+ */
96
+ getThread(roomId: string, rootMsgId: string): Promise<MessageRow[]>;
97
+ }
98
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAWH,OAAO,KAAK,EACV,oBAAoB,EACpB,QAAQ,EACR,QAAQ,EACR,UAAU,EAEV,aAAa,EACb,IAAI,EACJ,MAAM,EACN,cAAc,EACd,cAAc,EACd,YAAY,EACb,MAAM,YAAY,CAAC;AAgEpB,qBAAa,aAAa;;gBAQZ,IAAI,EAAE,oBAAoB;IAoDtC;;;OAGG;IACG,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,UAAU,CAAC;IAIpE;;;;;;;;OAQG;IACG,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAwB5D,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAuC7E,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,QAAa,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAwEtE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,MAAM,IAAI;IAwI1D;;;;OAIG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BjE;;;;OAIG;IACG,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA2BjD;;;;OAIG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IA+B1D;;;OAGG;IACG,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB1D;;;OAGG;IACG,UAAU,CAAC,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IA2C1D;;;OAGG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB5C;;;OAGG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BrE;;OAEG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAKpD;;;;OAIG;IACG,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,SAAW,GAAG,OAAO,CAAC,MAAM,CAAC;IAoCjF;;;OAGG;IACG,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBjE;;;;;OAKG;IACG,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;CA2C1E"}