@msgmesh/sdk
Advanced tools
+15
-5
@@ -346,3 +346,4 @@ "use strict"; | ||
| * 提供 capabilities → 細粒度能力鍵(scope 須為 producer/consumer,非 admin), | ||
| * 形狀 [{ ops:["publish"|"subscribe"], topics:["orders","support*","*"] }]。 | ||
| * 形狀 [{ ops:["publish"|"subscribe"], topics:["orders","support*","*"], rooms?:["room.42"] }]。 | ||
| * rooms 選用(房間隔離):省略/空 = 所有房間;非空 = 僅限這些房間(發佈的 ?key / 訂閱的 ?room),平台強制。 | ||
| */ | ||
@@ -560,8 +561,13 @@ async createKey(scope, opts) { | ||
| * (MAX_AUTH_RETRIES),達上限視為永久撤權而停止(一次成功連上即歸零),以免對已撤權者無限重連。 | ||
| * | ||
| * opts.room(可選,多房間路由):只接收 Kafka record key 等於 room 的訊息(發佈時用 publish 的 key 指定 | ||
| * 房間)。省略=收該 topic 全部訊息。⚠️ room 只是路由過濾、**無伺服器強制隔離**——惡意 client 可改成別人的 | ||
| * room 偷聽同 topic 其他房間;真隔離需 token 帶 room scope,MVP 階段隔離靠你的後端 token-broker + 誠實 client。 | ||
| */ | ||
| stream(topic, onMessage, onError) { | ||
| stream(topic, onMessage, onError, opts) { | ||
| let stopped = false; | ||
| let es; | ||
| let failures = 0; | ||
| const sseUrl = (cred) => `${this.rt}/v1/topics/${encodeURIComponent(topic)}/sse?key=${encodeURIComponent(cred)}`; | ||
| const roomQ = opts?.room ? `&room=${encodeURIComponent(opts.room)}` : ""; | ||
| const sseUrl = (cred) => `${this.rt}/v1/topics/${encodeURIComponent(topic)}/sse?key=${encodeURIComponent(cred)}${roomQ}`; | ||
| const scheduleReconnect = () => { | ||
@@ -633,4 +639,7 @@ if (stopped) return; | ||
| * (關閉,可讀 e.code / e.reason)。 | ||
| * | ||
| * opts.room(可選,多房間路由):同 stream() ——只收 Kafka record key==room 的訊息;省略=收全部。 | ||
| * ⚠️ 只做路由過濾、無伺服器強制隔離(見 stream() 說明)。 | ||
| */ | ||
| streamWs(topic, onMessage, onError) { | ||
| streamWs(topic, onMessage, onError, opts) { | ||
| const WS = globalThis.WebSocket; | ||
@@ -647,3 +656,4 @@ if (typeof WS !== "function") { | ||
| const wsBase = this.rt.replace(/^http/, "ws"); | ||
| const wsUrl = (cred) => `${wsBase}/v1/topics/${encodeURIComponent(topic)}/ws?key=${encodeURIComponent(cred)}`; | ||
| const roomQ = opts?.room ? `&room=${encodeURIComponent(opts.room)}` : ""; | ||
| const wsUrl = (cred) => `${wsBase}/v1/topics/${encodeURIComponent(topic)}/ws?key=${encodeURIComponent(cred)}${roomQ}`; | ||
| const scheduleReconnect = () => { | ||
@@ -650,0 +660,0 @@ if (stopped) return; |
+16
-3
@@ -341,3 +341,4 @@ interface MsgMeshOptions { | ||
| * 提供 capabilities → 細粒度能力鍵(scope 須為 producer/consumer,非 admin), | ||
| * 形狀 [{ ops:["publish"|"subscribe"], topics:["orders","support*","*"] }]。 | ||
| * 形狀 [{ ops:["publish"|"subscribe"], topics:["orders","support*","*"], rooms?:["room.42"] }]。 | ||
| * rooms 選用(房間隔離):省略/空 = 所有房間;非空 = 僅限這些房間(發佈的 ?key / 訂閱的 ?room),平台強制。 | ||
| */ | ||
@@ -349,2 +350,3 @@ createKey(scope: string, opts?: { | ||
| topics: string[]; | ||
| rooms?: string[]; | ||
| }>; | ||
@@ -424,4 +426,10 @@ }): Promise<{ | ||
| * (MAX_AUTH_RETRIES),達上限視為永久撤權而停止(一次成功連上即歸零),以免對已撤權者無限重連。 | ||
| * | ||
| * opts.room(可選,多房間路由):只接收 Kafka record key 等於 room 的訊息(發佈時用 publish 的 key 指定 | ||
| * 房間)。省略=收該 topic 全部訊息。⚠️ room 只是路由過濾、**無伺服器強制隔離**——惡意 client 可改成別人的 | ||
| * room 偷聽同 topic 其他房間;真隔離需 token 帶 room scope,MVP 階段隔離靠你的後端 token-broker + 誠實 client。 | ||
| */ | ||
| stream(topic: string, onMessage: (data: string) => void, onError?: (e: Event | MessageEvent) => void): () => void; | ||
| stream(topic: string, onMessage: (data: string) => void, onError?: (e: Event | MessageEvent) => void, opts?: { | ||
| room?: string; | ||
| }): () => void; | ||
| /** | ||
@@ -443,6 +451,11 @@ * streamWs 透過 WebSocket 即時接收訊息,介面與 stream(SSE)一致、回傳停止函式。用全域 `WebSocket` | ||
| * (關閉,可讀 e.code / e.reason)。 | ||
| * | ||
| * opts.room(可選,多房間路由):同 stream() ——只收 Kafka record key==room 的訊息;省略=收全部。 | ||
| * ⚠️ 只做路由過濾、無伺服器強制隔離(見 stream() 說明)。 | ||
| */ | ||
| streamWs(topic: string, onMessage: (data: string) => void, onError?: (e: Event | CloseEvent) => void): () => void; | ||
| streamWs(topic: string, onMessage: (data: string) => void, onError?: (e: Event | CloseEvent) => void, opts?: { | ||
| room?: string; | ||
| }): () => void; | ||
| } | ||
| export { type APIKey, type AdjustResult, type AuditEntry, AuthError, type Billing, type Deposit, type DepositAddress, type DepositStatus, type FinanceChainAmount, type FinanceOverview, type FinanceTenant, type LedgerEntry, type Message, MsgMesh, MsgMeshError, type MsgMeshOptions, NotFoundError, type Overview, type Page, type PlanLimits, type Presence, type PublishResult, RateLimitError, type ReplayResult, type SchemaVersion, type Settings, type Tenant, type TokenResponse, type Topic, type TopicFunction, type UsageDebit, type UsageResponse, type UsageRow, ValidationError, type Webhook, errorFromResponse, errorFromStatus }; |
+16
-3
@@ -341,3 +341,4 @@ interface MsgMeshOptions { | ||
| * 提供 capabilities → 細粒度能力鍵(scope 須為 producer/consumer,非 admin), | ||
| * 形狀 [{ ops:["publish"|"subscribe"], topics:["orders","support*","*"] }]。 | ||
| * 形狀 [{ ops:["publish"|"subscribe"], topics:["orders","support*","*"], rooms?:["room.42"] }]。 | ||
| * rooms 選用(房間隔離):省略/空 = 所有房間;非空 = 僅限這些房間(發佈的 ?key / 訂閱的 ?room),平台強制。 | ||
| */ | ||
@@ -349,2 +350,3 @@ createKey(scope: string, opts?: { | ||
| topics: string[]; | ||
| rooms?: string[]; | ||
| }>; | ||
@@ -424,4 +426,10 @@ }): Promise<{ | ||
| * (MAX_AUTH_RETRIES),達上限視為永久撤權而停止(一次成功連上即歸零),以免對已撤權者無限重連。 | ||
| * | ||
| * opts.room(可選,多房間路由):只接收 Kafka record key 等於 room 的訊息(發佈時用 publish 的 key 指定 | ||
| * 房間)。省略=收該 topic 全部訊息。⚠️ room 只是路由過濾、**無伺服器強制隔離**——惡意 client 可改成別人的 | ||
| * room 偷聽同 topic 其他房間;真隔離需 token 帶 room scope,MVP 階段隔離靠你的後端 token-broker + 誠實 client。 | ||
| */ | ||
| stream(topic: string, onMessage: (data: string) => void, onError?: (e: Event | MessageEvent) => void): () => void; | ||
| stream(topic: string, onMessage: (data: string) => void, onError?: (e: Event | MessageEvent) => void, opts?: { | ||
| room?: string; | ||
| }): () => void; | ||
| /** | ||
@@ -443,6 +451,11 @@ * streamWs 透過 WebSocket 即時接收訊息,介面與 stream(SSE)一致、回傳停止函式。用全域 `WebSocket` | ||
| * (關閉,可讀 e.code / e.reason)。 | ||
| * | ||
| * opts.room(可選,多房間路由):同 stream() ——只收 Kafka record key==room 的訊息;省略=收全部。 | ||
| * ⚠️ 只做路由過濾、無伺服器強制隔離(見 stream() 說明)。 | ||
| */ | ||
| streamWs(topic: string, onMessage: (data: string) => void, onError?: (e: Event | CloseEvent) => void): () => void; | ||
| streamWs(topic: string, onMessage: (data: string) => void, onError?: (e: Event | CloseEvent) => void, opts?: { | ||
| room?: string; | ||
| }): () => void; | ||
| } | ||
| export { type APIKey, type AdjustResult, type AuditEntry, AuthError, type Billing, type Deposit, type DepositAddress, type DepositStatus, type FinanceChainAmount, type FinanceOverview, type FinanceTenant, type LedgerEntry, type Message, MsgMesh, MsgMeshError, type MsgMeshOptions, NotFoundError, type Overview, type Page, type PlanLimits, type Presence, type PublishResult, RateLimitError, type ReplayResult, type SchemaVersion, type Settings, type Tenant, type TokenResponse, type Topic, type TopicFunction, type UsageDebit, type UsageResponse, type UsageRow, ValidationError, type Webhook, errorFromResponse, errorFromStatus }; |
+15
-5
@@ -313,3 +313,4 @@ // src/errors.ts | ||
| * 提供 capabilities → 細粒度能力鍵(scope 須為 producer/consumer,非 admin), | ||
| * 形狀 [{ ops:["publish"|"subscribe"], topics:["orders","support*","*"] }]。 | ||
| * 形狀 [{ ops:["publish"|"subscribe"], topics:["orders","support*","*"], rooms?:["room.42"] }]。 | ||
| * rooms 選用(房間隔離):省略/空 = 所有房間;非空 = 僅限這些房間(發佈的 ?key / 訂閱的 ?room),平台強制。 | ||
| */ | ||
@@ -527,8 +528,13 @@ async createKey(scope, opts) { | ||
| * (MAX_AUTH_RETRIES),達上限視為永久撤權而停止(一次成功連上即歸零),以免對已撤權者無限重連。 | ||
| * | ||
| * opts.room(可選,多房間路由):只接收 Kafka record key 等於 room 的訊息(發佈時用 publish 的 key 指定 | ||
| * 房間)。省略=收該 topic 全部訊息。⚠️ room 只是路由過濾、**無伺服器強制隔離**——惡意 client 可改成別人的 | ||
| * room 偷聽同 topic 其他房間;真隔離需 token 帶 room scope,MVP 階段隔離靠你的後端 token-broker + 誠實 client。 | ||
| */ | ||
| stream(topic, onMessage, onError) { | ||
| stream(topic, onMessage, onError, opts) { | ||
| let stopped = false; | ||
| let es; | ||
| let failures = 0; | ||
| const sseUrl = (cred) => `${this.rt}/v1/topics/${encodeURIComponent(topic)}/sse?key=${encodeURIComponent(cred)}`; | ||
| const roomQ = opts?.room ? `&room=${encodeURIComponent(opts.room)}` : ""; | ||
| const sseUrl = (cred) => `${this.rt}/v1/topics/${encodeURIComponent(topic)}/sse?key=${encodeURIComponent(cred)}${roomQ}`; | ||
| const scheduleReconnect = () => { | ||
@@ -600,4 +606,7 @@ if (stopped) return; | ||
| * (關閉,可讀 e.code / e.reason)。 | ||
| * | ||
| * opts.room(可選,多房間路由):同 stream() ——只收 Kafka record key==room 的訊息;省略=收全部。 | ||
| * ⚠️ 只做路由過濾、無伺服器強制隔離(見 stream() 說明)。 | ||
| */ | ||
| streamWs(topic, onMessage, onError) { | ||
| streamWs(topic, onMessage, onError, opts) { | ||
| const WS = globalThis.WebSocket; | ||
@@ -614,3 +623,4 @@ if (typeof WS !== "function") { | ||
| const wsBase = this.rt.replace(/^http/, "ws"); | ||
| const wsUrl = (cred) => `${wsBase}/v1/topics/${encodeURIComponent(topic)}/ws?key=${encodeURIComponent(cred)}`; | ||
| const roomQ = opts?.room ? `&room=${encodeURIComponent(opts.room)}` : ""; | ||
| const wsUrl = (cred) => `${wsBase}/v1/topics/${encodeURIComponent(topic)}/ws?key=${encodeURIComponent(cred)}${roomQ}`; | ||
| const scheduleReconnect = () => { | ||
@@ -617,0 +627,0 @@ if (stopped) return; |
+1
-1
| { | ||
| "name": "@msgmesh/sdk", | ||
| "version": "0.1.3", | ||
| "version": "0.1.4", | ||
| "description": "MsgMesh TypeScript SDK — 多租戶事件總線的收發 / 即時(SSE·WS)/ 治理 client(Node 與瀏覽器通用)。", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
+38
-1
@@ -44,4 +44,41 @@ # @msgmesh/sdk | ||
| 後端(~5 行)代呼 `/v1/tokens`(可帶 `capabilities` 降權為金鑰能力子集,只准更窄)後把結果回傳即可。 | ||
| ## 多房間(rooms) | ||
| 一個 topic 內可再切多個房間(room = Kafka record key),脫鉤「房間數」與「topic 數」。分兩層: | ||
| **① 路由**——發佈時用 `publish(topic, body, { key: roomId })` 指定房間,訂閱時傳選用 `room`(第四參數 `opts`,`stream`/`streamWs` 皆同)只收該房間: | ||
| ```js | ||
| mq.stream("chat", (data) => console.log(data), undefined, { room: "room-42" }); // 只收 room-42 | ||
| mq.streamWs("chat", (data) => console.log(data), undefined, { room: "room-42" }); | ||
| await mq.publish("chat", { text: "hi" }, { key: "room-42" }); // 發到 room-42 | ||
| ``` | ||
| 省略 `room`=收該 topic 全部訊息(向後相容)。路由本身只做過濾、**無強制隔離**——惡意 client 可改成別人的 `room` 偷聽同 topic 其他房間。要真隔離看 ②。 | ||
| **② 隔離(平台強制)**——把憑證的 `capabilities` 加上選用 `rooms`,平台即強制該憑證只能收發指定房間(逾越 403)。`rooms` 省略/空 = 所有房間(向後相容);非空 = 僅限這些。典型作法是後端持一把全房間金鑰,向 `POST /v1/tokens` **降權**簽出「只准某房間」的短期 token 給前端(降權只准更窄、須為金鑰能力子集,逾越 403): | ||
| ```ts | ||
| // 後端 token-broker:用全房間 key 降權鑄「只准 chat / room-42」的短期 token,回給前端當 getToken | ||
| const r = await fetch(`${controlPlaneUrl}/v1/tokens`, { | ||
| method: "POST", | ||
| headers: { Authorization: `Bearer ${process.env.MSGMESH_KEY}`, "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| ttl_seconds: 600, | ||
| capabilities: [{ ops: ["subscribe", "publish"], topics: ["chat"], rooms: ["room-42"] }], | ||
| }), | ||
| }); | ||
| const { token, expires_in } = await r.json(); // 回給前端;前端以 getToken 用它連 SSE/WS | ||
| ``` | ||
| 也可用 `createKey("key", { capabilities: [{ ops, topics, rooms }] })` 簽一把常駐 room-scoped 鍵。平台強制點:訂閱(SSE/WS)必須帶允許集內的 `?room`(不帶=想收全部房間,一樣 403);發佈的 `?key` 必須 ∈ 允許集。 | ||
| > ⚠️ **room-scoped 憑證只能走即時(SSE/WS)+ 對其房間 publish**;**不能** `poll` / `consume` / DLQ。後者是整個 topic 的 firehose(consumer-group offset 會吃掉別房間、每房一 group = 讀取放大),無法乾淨 per-room 過濾,受限房間憑證一律 403(`use realtime SSE/WS ?room=`)。需要 poll/consume 時請改用不限房間的憑證。 | ||
| ### 房間隔離的安全須知(必讀) | ||
| - **隔離強度 = 你發的 token 範圍。** 只有在「後端用全房間金鑰**降權**鑄 room-scoped token 給前端」時才有隔離。**別把不限房間的憑證(全權 key、或沒有 `rooms` 的 token)放進前端**——那樣 client 改個 `room` 就能看到所有房間,隔離形同虛設。 | ||
| - **平台不驗「發訊者是誰」。** 房間隔離管的是「能收發哪些房間」,不是「你是房裡的誰」。同一房內,任何持該房 token 的人都能在 payload 裡冒充任何 sender。要防房內冒名:**後端為每個使用者各自鑄 token、並由後端戳上 / 驗證 sender**,別讓前端自報身分。 | ||
| - 附帶:`presence`(在線數)目前是 per-topic 非 per-room(只洩漏聚合數字);短期 token 為 bearer,洩漏 = 該房 ≤TTL 可用(故 TTL 短、勿記進 log)。 | ||
| ## 生產環境設定(必讀) | ||
@@ -48,0 +85,0 @@ |
106303
6.9%1858
1.81%136
37.37%