@sdkwork/im-sdk
v0.1.1
Published
Unified IM SDK package with embedded OpenAPI-generated transport and handwritten realtime/business modules
Readme
SDKWork IM TypeScript SDK
Official package: @sdkwork/im-sdk
This package is the single consumer-facing TypeScript SDK in the IM SDK family. It targets browser and Node.js and follows one package rule:
- semantic SDK surface lives at the package root
- generated OpenAPI transport is assembled under
src/generated/**inside the same package - application code should start from
ImSdkClient - route-aligned transport control stays available on the same client through
sdk.session,sdk.presence,sdk.realtime,sdk.device,sdk.inbox, andsdk.stream
Authoring boundaries inside this workspace remain explicit:
generated/server-openapiGenerator-owned transport authoring output before root-package assembly. This workspace carries the private internal package identity@sdkwork-internal/im-sdk-generatedand is not a second public consumer package.composedManual-owned semantic authoring layer used before the single-package root is assembled.- root package
The published
@sdkwork/im-sdkconsumer package that assembles generated transport and handwritten runtime modules together.
Install
npm install @sdkwork/im-sdkpnpm add @sdkwork/im-sdkQuick Start
Browser
import { ImSdkClient } from '@sdkwork/im-sdk';
const sdk = new ImSdkClient({
baseUrl: import.meta.env.VITE_CRAW_CHAT_BASE_URL,
authToken: window.localStorage.getItem('craw-chat-token') ?? undefined,
});Node.js
import { ImSdkClient } from '@sdkwork/im-sdk';
const sdk = new ImSdkClient({
baseUrl: process.env.CRAW_CHAT_BASE_URL!,
authToken: process.env.CRAW_CHAT_TOKEN,
});When HTTP API traffic and realtime WebSocket traffic do not share the same origin:
const sdk = new ImSdkClient({
apiBaseUrl: 'https://api.example.com',
websocketBaseUrl: 'wss://realtime.example.com',
authToken: process.env.CRAW_CHAT_TOKEN,
});The constructor is synchronous. Network traffic starts only when you call an explicit method such
as sdk.auth.login(), sdk.send(), sdk.connect(), or sdk.sync.catchUp().
Chat CLI
The TypeScript workspace ships a standard chat smoke CLI for SDK integration testing. It stays on
top of ImSdkClient and does not bypass the SDK with handwritten transport calls.
Workspace entrypoints:
node ./bin/sdk-chat.mjs --help
./bin/sdk-chat.sh --help
.\bin\sdk-chat.ps1 --helpRoot workspace standard entrypoints:
node ../bin/sdk-chat.mjs --language typescript --help
../bin/sdk-chat.sh --language typescript --helppowershell -ExecutionPolicy Bypass -File ..\bin\sdk-chat.ps1 -Language typescript --helpThe local sdk-chat.ps1 and sdk-chat.sh forward back to the root workspace dispatcher so every
language workspace converges on the same sdk-chat standard as more language implementations are
added.
Published package entrypoint after install:
npx sdkwork-im-chat --helpStandard environment variables:
export SDKWORK_IM_BASE_URL="https://chat.example.test"
export SDKWORK_IM_WEBSOCKET_BASE_URL="wss://chat.example.test"
export SDKWORK_IM_TENANT_ID="tenant-demo"
export SDKWORK_IM_LOGIN="ops-demo"
export SDKWORK_IM_PASSWORD="secret-demo"
export SDKWORK_IM_CONVERSATION_ID="conversation-demo"Login-first interactive chat:
node ./bin/sdk-chat.mjs \
--base-url https://chat.example.test \
--tenant-id tenant-demo \
--login ops-demo \
--password secret-demo \
--conversation-id conversation-demoToken-first interactive chat:
node ./bin/sdk-chat.mjs \
--base-url https://chat.example.test \
--token "<access-token>" \
--conversation-id conversation-demoBehavior:
receive-mode=autoprefers realtime live receive when Node.jswsis availablereceive-mode=autofalls back to durable catch-up when live receive is unavailable/catchuptriggers one immediate catch-up batch/helpprints CLI help/exitor/quitcloses the session
Authentication
Authentication is exposed through sdk.auth.
const login = await sdk.auth.login({
tenantId: 'tenant-acme',
login: 'ops_lead',
password: '***',
clientKind: 'portal_operator',
});
await sdk.auth.me();
sdk.auth.useToken(login.accessToken);
sdk.auth.clearToken();Behavior:
sdk.auth.login()automatically appliesaccessTokenwhen the service returns onesdk.auth.useToken(token)updates the underlying generated clientsdk.auth.clearToken()clears the bearer token used by HTTP and realtime helpers
Portal
Portal snapshot reads are exposed directly on sdk.portal.
const home = await sdk.portal.getHome();
const workspace = await sdk.portal.getWorkspace();
const dashboard = await sdk.portal.getDashboard();
console.log(home.hero?.title, workspace.name, dashboard.hero?.title);Available portal helpers:
sdk.portal.getHome()sdk.portal.getAuth()sdk.portal.getWorkspace()sdk.portal.getDashboard()sdk.portal.getConversations()sdk.portal.getRealtime()sdk.portal.getMedia()sdk.portal.getAutomation()sdk.portal.getGovernance()
Conversations
Use sdk.conversations when you want route-aligned access for conversation lifecycle, handoff,
membership, read-state, and direct message posting.
| Task | Method |
| --- | --- |
| Create a conversation | sdk.conversations.create(...) |
| Create a one-to-one agent dialog | sdk.conversations.createAgentDialog(...) |
| Create a handoff conversation | sdk.conversations.createAgentHandoff(...) |
| Create a system channel | sdk.conversations.createSystemChannel(...) |
| Read the conversation summary | sdk.conversations.get(...) |
| Read handoff state | sdk.conversations.getAgentHandoffState(...) |
| Accept / resolve / close handoff | sdk.conversations.acceptAgentHandoff(...), sdk.conversations.resolveAgentHandoff(...), sdk.conversations.closeAgentHandoff(...) |
| Manage members | sdk.conversations.listMembers(...), sdk.conversations.addMember(...), sdk.conversations.removeMember(...), sdk.conversations.transferOwner(...), sdk.conversations.changeMemberRole(...), sdk.conversations.leave(...) |
| Manage read state | sdk.conversations.getReadCursor(...), sdk.conversations.updateReadCursor(...) |
| Read and post route-level messages | sdk.conversations.listMessages(...), sdk.conversations.postMessage(...), sdk.conversations.postText(...), sdk.conversations.publishSystemMessage(...), sdk.conversations.publishSystemText(...) |
const conversation = await sdk.conversations.create({
conversationId: 'conversation-order-1',
conversationType: 'group',
});
await sdk.conversations.postText(conversation.conversationId, 'hello world', {
summary: 'Greeting',
});
await sdk.conversations.publishSystemText(
conversation.conversationId,
'Maintenance starts in 5 minutes',
{
summary: 'Maintenance window',
},
);
const inbox = await sdk.inbox.getInbox();
console.log(conversation.conversationId, inbox.items.length);Messages
The outbound model is message-first at the client root:
- create a message with
sdk.createXxxMessage(...) - send it with
sdk.send(message)
The same builders remain available on sdk.messages when you want a namespaced module surface.
The primary message entrypoints are sdk.createTextMessage(...), sdk.send(...),
sdk.upload(...), sdk.uploadAndSendMessage(...), and sdk.decodeMessage(...).
Text
const message = sdk.createTextMessage({
conversationId: 'conversation-1',
text: 'hello world',
summary: 'Greeting',
renderHints: { tone: 'friendly' },
});
await sdk.send(message);Media
const image = sdk.createImageMessage({
conversationId: 'conversation-1',
mediaAssetId: 'asset-image-1',
text: 'Latest storefront concept',
summary: 'Storefront concept',
});
await sdk.send(image);Upload And Send
const uploaded = await sdk.uploadAndSendMessage({
upload: {
mediaAssetId: 'asset-image-1',
bucket: 'tenant-media',
objectKey: 'conversation-1/storefront.png',
resource: {
type: 'image',
name: 'storefront.png',
mimeType: 'image/png',
size: file.size,
},
body: file,
},
createMessage: (preparedUpload) =>
sdk.createImageMessage({
conversationId: 'conversation-1',
mediaAssetId: preparedUpload.mediaAssetId,
text: 'Uploaded storefront image',
summary: 'Storefront image',
}),
});
console.log(uploaded.mediaAssetId, uploaded.url, uploaded.delivery.messageId);When you only need the presigned upload flow without sending a message immediately, use
sdk.upload(...). It returns a ImUploadedMediaAsset with the completed asset metadata,
upload session details, and the resolved CDN or download URL when the service provides one.
const uploadedAsset = await sdk.upload({
mediaAssetId: 'asset-image-2',
bucket: 'tenant-media',
objectKey: 'conversation-1/manual-upload.png',
resource: {
type: 'image',
name: 'manual-upload.png',
mimeType: 'image/png',
size: file.size,
},
body: file,
});
console.log(
uploadedAsset.mediaAssetId,
uploadedAsset.asset.status,
uploadedAsset.session.uploadUrl,
);Rich Standard Messages
The semantic layer includes common IM and AI-era message families:
createLocationMessagecreateLinkMessagecreateCardMessagecreateMusicMessagecreateContactMessagecreateStickerMessagecreateVoiceMessagecreateCustomMessagecreateAiTextMessagecreateAiImageGenerationMessagecreateAiVideoGenerationMessagecreateAgentMessagecreateAgentStateMessagecreateAgentHandoffMessagecreateToolResultMessagecreateWorkflowEventMessagecreateDataMessagecreateSignalMessagecreateStreamReferenceMessage
Examples:
const location = sdk.createLocationMessage({
conversationId: 'conversation-1',
text: 'Meet me here',
latitude: 31.2304,
longitude: 121.4737,
name: 'The Bund',
address: 'Shanghai',
});
const aiText = sdk.createAiTextMessage({
conversationId: 'conversation-1',
text: 'Assistant answer',
prompt: 'summarize the last order',
model: 'gpt-5.4',
status: 'completed',
});
const aiImage = sdk.createAiImageGenerationMessage({
conversationId: 'conversation-1',
text: 'Generated hero illustration',
prompt: 'cinematic storefront at sunrise',
model: 'gpt-image-1',
mediaAssetId: 'asset-generated-hero-1',
resource: {
type: 'image',
name: 'storefront-hero.png',
mimeType: 'image/png',
url: 'https://cdn.example.com/generated/storefront-hero.png',
},
status: 'completed',
});
const agent = sdk.createAgentMessage({
conversationId: 'conversation-1',
text: 'Primary support agent joined',
agentId: 'assistant-1',
agentName: 'Assistant',
stage: 'active',
status: 'online',
capabilities: ['summarize', 'route'],
});
const handoff = sdk.createAgentHandoffMessage({
conversationId: 'conversation-1',
text: 'Escalating to billing specialist',
fromAgentId: 'router',
toAgentId: 'billing-specialist',
reason: 'invoice_exception',
status: 'pending',
});
await sdk.send(location);
await sdk.send(aiText);
await sdk.send(aiImage);
await sdk.send(agent);
await sdk.send(handoff);System Channel
Set channel: 'system' to publish into the system channel. sdk.send(...) will route to
the correct HTTP endpoint automatically.
const notice = sdk.createTextMessage({
conversationId: 'conversation-system-1',
channel: 'system',
text: 'Deployment starts in 5 minutes',
summary: 'Maintenance window',
});
await sdk.send(notice);Decode
If you already own a raw MessageBody or PostMessageRequest, decode it through the semantic
message codec:
const decoded = sdk.decodeMessage(aiText.body);
console.log(decoded.type, decoded.summary);Media
Use sdk.media for explicit upload lifecycle control.
sdk.media.createUpload(...)sdk.media.createUploadSession(...)sdk.media.upload(...)sdk.media.completeUpload(...)sdk.media.uploadAndComplete(...)sdk.media.getDownloadUrl(...)sdk.media.get(...)sdk.media.attach(...)sdk.media.attachText(...)
const prepared = await sdk.media.uploadAndComplete({
mediaAssetId: 'asset-file-1',
bucket: 'tenant-media',
objectKey: 'conversation-1/brief.pdf',
resource: {
type: 'file',
name: 'brief.pdf',
mimeType: 'application/pdf',
size: file.size,
},
body: file,
});
await sdk.media.attachText(prepared.mediaAssetId, {
conversationId: 'conversation-1',
text: 'Uploaded project brief',
summary: 'Project brief',
});sdk.upload(...) is the preferred root helper for standard application uploads. sdk.media.upload
and sdk.media.uploadAndComplete(...) expose the same presigned S3-style client upload flow from
the namespaced media module. Use sdk.media.createUploadSession(...) plus
sdk.media.completeUpload(...) only when you need to control the upload transaction manually.
Realtime
The SDK exposes two receive models on purpose:
sdk.connect(...)Live push over WebSocketsdk.sync.catchUp(...)Durable HTTP catch-up and replay window acknowledgement
Live Push
const live = await sdk.connect({
deviceId: 'web-chrome-01',
subscriptions: {
conversations: ['conversation-1'],
rtcSessions: ['rtc-1'],
},
});
live.messages.on((message, context) => {
console.log(message.type, message.summary, context.sequence);
void context.ack();
});
live.messages.onConversation('conversation-1', (message, context) => {
console.log(context.conversationId, message.type);
});
live.data.on((data, context) => {
console.log(data.schemaRef, data.payload, context.sequence);
});
live.signals.on((signal, context) => {
console.log(signal.signalType, signal.payload, context.scopeId);
});
live.signals.onRtcSession('rtc-1', (signal, context) => {
console.log(signal.signalType, context.scopeId);
});
live.events.on((context) => {
console.log(context.kind, context.sequence, context.source);
});
live.lifecycle.onStateChange((state) => {
console.log(state.status);
});
live.lifecycle.onError((context) => {
console.log(context.code, context.error);
});
console.log(live.lifecycle.getState().status);The recommended live callback shape is payload-first by domain stream:
message,data, orsignalas the first callback argumentcontextas the second callback argumentcontext.sequencecontext.receivedAtcontext.sendercontext.sourcecontext.rawEventcontext.ack()
The live runtime surface is:
live.messages.on(...)for the primary inbound message streamlive.messages.onConversation(...)for conversation-scoped inbound message handlinglive.data.on(...)for non-message structured data deliverylive.signals.on(...)for generic signaling deliverylive.signals.onRtcSession(...)for RTC-session-scoped signalinglive.events.on(...)for the normalized receive context before your app-specific routinglive.lifecycle.onStateChange(...)forconnected,error, andclosedtransitionslive.lifecycle.onError(...)for realtime protocol and socket-level failureslive.lifecycle.getState()for the latest connection snapshot
Durable Catch-Up
const batch = await sdk.sync.catchUp({ limit: 50 });
for (const item of batch.items) {
if (item.kind === 'message') {
console.log(item.sequence, item.message.type, item.message.summary);
await item.ack();
}
}
await sdk.sync.ack(batch);Use sdk.sync when your app needs resume, replay, background processing, or explicit ACK control.
context.ack() acknowledges through the current receive context, while sdk.sync.ack(...) lets you
commit the highest durable replay position explicitly.
Browser And Node WebSocket Factories
If the realtime gateway requires a custom upgrade strategy, provide webSocketFactory.
Important: the default global WebSocket constructor cannot attach Authorization headers. In
plain browser environments, authenticated realtime should use a browser-safe gateway strategy:
ImWebSocketAuthOptions.automatic()is the standard default- automatic auth resolves to query bearer for the default browser
WebSocketpath - automatic auth resolves to header bearer when you provide
webSocketFactory - prefer exchanging the main access token for a short-lived realtime ticket or query credential
- pass that credential through a gateway-owned
wss://URL or cookie-based upgrade path - use
connect({ url })when the browser runtime must join through a fully pre-signed realtime URL
Node.js and custom runtimes can provide header-based upgrades through webSocketFactory.
import { ImSdkClient, ImWebSocketAuthOptions } from '@sdkwork/im-sdk';
const sdk = new ImSdkClient({
baseUrl: 'https://api.example.com',
authToken: window.localStorage.getItem('craw-chat-token') ?? undefined,
webSocketAuth: ImWebSocketAuthOptions.queryBearer({
queryParameterName: 'rt',
credentialProvider: async ({ authToken }) =>
issueRealtimeTicket(authToken),
}),
});
const live = await sdk.connect({
subscriptions: {
conversations: ['conversation-1'],
},
});const realtimeTicket = await issueRealtimeTicket();
const live = await sdk.connect({
url: `wss://realtime.example.com/api/v1/realtime/ws?rt=${encodeURIComponent(realtimeTicket)}`,
subscriptions: {
conversations: ['conversation-1'],
},
});import WebSocket from 'ws';
const sdk = new ImSdkClient({
baseUrl: process.env.CRAW_CHAT_BASE_URL!,
authToken: process.env.CRAW_CHAT_TOKEN,
webSocketAuth: ImWebSocketAuthOptions.headerBearer(),
webSocketFactory: ({ url, protocols, headers }) =>
new WebSocket(url, protocols, { headers }),
});RTC
RTC lifecycle operations are available from sdk.rtc.
const session = await sdk.rtc.create({
rtcSessionId: 'rtc-1',
conversationId: 'conversation-1',
rtcMode: 'group_call',
});
await sdk.rtc.invite(session.rtcSessionId, {
signalingStreamId: 'rtc-signal-1',
});
await sdk.rtc.postJsonSignal(session.rtcSessionId, 'offer', {
signalingStreamId: 'rtc-signal-1',
payload: {
sdp: 'v=0...',
},
});
const credential = await sdk.rtc.issueParticipantCredential(session.rtcSessionId, {
participantId: 'user-1',
});
const recording = await sdk.rtc.getRecordingArtifact(session.rtcSessionId);
live.signals.onRtcSession(session.rtcSessionId, (signal, context) => {
console.log(signal.signalType, signal.payload, context.scopeId);
});Realtime RTC signals are received through live.signals.onRtcSession(...). Use
sdk.rtc.postJsonSignal(...) for common JSON signaling,
sdk.rtc.issueParticipantCredential(...) for provider join credentials, and
sdk.rtc.getRecordingArtifact(...) for recording metadata.
Route-Aligned Transport Modules
Some route groups stay exposed directly on ImSdkClient because they already match the public
OpenAPI contract cleanly:
const sdk = new ImSdkClient({
baseUrl: 'http://127.0.0.1:18090',
authToken: 'token',
});
await sdk.session.resume({ deviceId: 'web-chrome-01' });
await sdk.presence.getPresenceMe();
await sdk.realtime.listRealtimeEvents({ limit: 20 });
await sdk.device.register({ deviceId: 'web-chrome-01' });
await sdk.inbox.getInbox();
await sdk.stream.open({
streamId: 'stream-demo-1',
streamType: 'custom.delta.text',
scopeKind: 'conversation',
scopeId: 'conversation-1',
durabilityClass: 'durableSession',
schemaRef: 'custom.delta.text.v1',
});Guidance:
- start from
ImSdkClientfor application code - use the root transport modules when you need exact route-group control such as
sdk.session.resume(...),sdk.presence.getPresenceMe(),sdk.realtime.listRealtimeEvents(...),sdk.device.register(...),sdk.inbox.getInbox(), andsdk.stream.open(...) - keep generated transport authoring inside
generated/server-openapiandsrc/generated/**
Verification
Local workspace checks:
node ./bin/package-task.mjs typecheck
node ./bin/package-task.mjs build
node ../bin/verify-typescript-live-contract.mjs --package root
node ./bin/package-task.mjs smoke
node ../bin/verify-typescript-single-package-publishability.mjs
node ../bin/verify-typescript-single-package-install-safety.mjsFrom the SDK workspace root, the default cross-language verification entrypoint is sdk-verify via
./bin/verify-sdk.sh or .\bin\verify-sdk.ps1.
Release-oriented verification also checks npm pack --dry-run, verify-typescript-live-contract.mjs, verify-typescript-single-package-install-safety.mjs, verify-typescript-generated-build-determinism.mjs, runtime root exports, and cleanup of dead auth scaffolding plus stray src/index.js and src/index.d.ts residue before the TypeScript package is considered publishable.
On Windows hosts, release-oriented verification also includes verify-typescript-generated-build-concurrency.mjs and verify-typescript-workspace-concurrency.mjs so both stable generated-package builds and full workspace verifications stay race-free under overlapping runs.
Release Snapshot Boundary
This workspace inherits the current SDK release snapshot from artifacts/releases/wave-d-2026-04-08/sdk-release-catalog.json.
state = generated_pending_publicationgenerationStatus = generatedreleaseStatus = not_publishedplannedVersion = nullversionStatus = version_unassigned_pending_freezeversionDecisionSourcePath = null
