@robono/server 0.26.0 → 0.27.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 (51) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/HISTORY-IMPORT.md +104 -5
  3. package/README.md +1 -1
  4. package/SBOM.spdx.json +9 -9
  5. package/dist/calling.d.ts +30 -0
  6. package/dist/calling.d.ts.map +1 -1
  7. package/dist/calling.js +13 -0
  8. package/dist/calling.js.map +1 -1
  9. package/dist/generated/history-import-interactions.d.ts +17 -0
  10. package/dist/generated/history-import-interactions.d.ts.map +1 -0
  11. package/dist/generated/history-import-interactions.js +40 -0
  12. package/dist/generated/history-import-interactions.js.map +1 -0
  13. package/dist/generated/interactions.d.ts +199 -0
  14. package/dist/generated/interactions.d.ts.map +1 -0
  15. package/dist/generated/interactions.js +89 -0
  16. package/dist/generated/interactions.js.map +1 -0
  17. package/dist/generated/sdk-versions.d.ts +6 -6
  18. package/dist/generated/sdk-versions.d.ts.map +1 -1
  19. package/dist/generated/sdk-versions.js +1 -1
  20. package/dist/generated/sdk-versions.js.map +1 -1
  21. package/dist/history-convert.d.ts +52 -0
  22. package/dist/history-convert.d.ts.map +1 -0
  23. package/dist/history-convert.js +276 -0
  24. package/dist/history-convert.js.map +1 -0
  25. package/dist/history-import-cli.d.ts +1 -7
  26. package/dist/history-import-cli.d.ts.map +1 -1
  27. package/dist/history-import-cli.js +4 -44
  28. package/dist/history-import-cli.js.map +1 -1
  29. package/dist/history-import-validation.d.ts +8 -0
  30. package/dist/history-import-validation.d.ts.map +1 -0
  31. package/dist/history-import-validation.js +85 -0
  32. package/dist/history-import-validation.js.map +1 -0
  33. package/dist/history-import.d.ts +8 -1
  34. package/dist/history-import.d.ts.map +1 -1
  35. package/dist/history-import.js +27 -6
  36. package/dist/history-import.js.map +1 -1
  37. package/dist/history-migrate-cli.d.ts +3 -0
  38. package/dist/history-migrate-cli.d.ts.map +1 -0
  39. package/dist/history-migrate-cli.js +260 -0
  40. package/dist/history-migrate-cli.js.map +1 -0
  41. package/dist/history-polls.d.ts +4 -0
  42. package/dist/history-polls.d.ts.map +1 -0
  43. package/dist/history-polls.js +66 -0
  44. package/dist/history-polls.js.map +1 -0
  45. package/dist/index.d.ts +3 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/messenger-example.json +1 -1
  50. package/dist/messenger-install.d.ts +1 -1
  51. package/package.json +2 -1
@@ -0,0 +1,66 @@
1
+ import { validatePoll } from './generated/interactions.js';
2
+ const key = (v) => typeof v === 'string' || typeof v === 'number' ? String(v) : null;
3
+ const object = (v) => v && typeof v === 'object' && !Array.isArray(v);
4
+ /** Consume complete provider exports. Counts are checks, never invented voters. */
5
+ export function convertImportedPoll(provider, p, exportedVotes, user) {
6
+ const fail = (code) => { throw Error(code); };
7
+ if (!object(p) || !Array.isArray(p.options))
8
+ fail('poll_requires_complete_options_and_votes');
9
+ const closed = provider === 'sendbird' ? p.status === 'closed' : p.is_closed === true;
10
+ if (p.voting_visibility === 'anonymous' || p.is_anonymous === true)
11
+ fail('anonymous_poll_not_supported');
12
+ if (p.status === 'removed')
13
+ fail('removed_poll_not_supported');
14
+ if (!closed && (p.allow_user_suggestion || p.allow_user_suggested_options || p.allow_answers || provider === 'sendbird' && p.close_at > 0))
15
+ fail('poll_behavior_requires_mapping');
16
+ if (p.description || p.data && p.data !== '' && JSON.stringify(p.data) !== '{}')
17
+ fail('poll_custom_data_requires_mapping');
18
+ const multiple = provider === 'sendbird' ? p.allow_multiple_votes === true : p.enforce_unique_vote !== true && p.max_votes_allowed !== 1;
19
+ if (!closed && multiple && p.max_votes_allowed && p.max_votes_allowed < p.options.length)
20
+ fail('poll_vote_limit_requires_mapping');
21
+ const optionIds = p.options.map((o) => key(o?.id));
22
+ if (optionIds.some((v) => !v) || new Set(optionIds).size !== optionIds.length)
23
+ fail('invalid_poll_options');
24
+ let poll;
25
+ try {
26
+ poll = validatePoll({ question: provider === 'sendbird' ? p.title : p.name, options: p.options.map((o) => o.text), multiple });
27
+ }
28
+ catch {
29
+ fail('poll_limits_require_mapping');
30
+ }
31
+ const votes = new Map();
32
+ function add(rawUser, option) { const who = user(rawUser), index = optionIds.indexOf(key(option)); if (!who || index < 0)
33
+ throw Error('poll_vote_mapping_required'); const choices = votes.get(who) ?? []; if (choices.includes(index))
34
+ fail('duplicate_poll_vote'); choices.push(index); votes.set(who, choices); }
35
+ if (provider === 'sendbird') {
36
+ p.options.forEach((o) => {
37
+ const list = o.voters ?? o.partial_voter_list ?? [];
38
+ if (!Array.isArray(list) || !Number.isSafeInteger(o.vote_count) || o.vote_count !== list.length || o.next)
39
+ fail('complete_poll_voters_required');
40
+ for (const v of list)
41
+ add(v.user_id, o.id);
42
+ });
43
+ if (!Number.isSafeInteger(p.voter_count) || p.voter_count !== votes.size)
44
+ fail('complete_poll_voters_required');
45
+ }
46
+ else {
47
+ const list = exportedVotes ?? p.votes ?? (p.vote_count === 0 ? [] : undefined);
48
+ if (!Array.isArray(list))
49
+ fail('complete_poll_voters_required');
50
+ if (p.vote_count !== undefined && (!Number.isSafeInteger(p.vote_count) || p.vote_count !== list.length))
51
+ fail('complete_poll_voters_required');
52
+ for (const v of list) {
53
+ if (v.is_answer || v.answer_text || v.poll_id !== undefined && key(v.poll_id) !== key(p.id))
54
+ fail('poll_vote_mapping_required');
55
+ add(v.user_id ?? v.user, v.option_id);
56
+ }
57
+ if (object(p.vote_counts_by_option))
58
+ for (const [id, count] of Object.entries(p.vote_counts_by_option))
59
+ if (count !== [...votes.values()].filter(indices => indices.includes(optionIds.indexOf(id))).length)
60
+ fail('complete_poll_voters_required');
61
+ }
62
+ if (votes.size > 2000 || [...votes.values()].some(v => !multiple && v.length > 1))
63
+ fail('poll_limits_require_mapping');
64
+ return { content: { kind: 'poll', poll: poll }, interactions: { closed, votes: [...votes].map(([userId, options]) => ({ userId, options: options.sort((a, b) => a - b) })) } };
65
+ }
66
+ //# sourceMappingURL=history-polls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history-polls.js","sourceRoot":"","sources":["../src/history-polls.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { RobonoServer } from "./client.js";
2
2
  export { RobonoCallingBackend } from './calling.js';
3
- export type { RobonoCallingBackendOptions, CallingRelationshipInput, CallingRelationshipResult, CallingSession } from './calling.js';
3
+ export type { RobonoCallingBackendOptions, CallingRelationshipInput, CallingRelationshipResult, CallingSession, CallingRecording } from './calling.js';
4
4
  export { createRobonoBackendAdapter } from "./adapter.js";
5
5
  export type { RobonoAuthorizationAction, RobonoAuthorizationContext, RobonoBackendAdapterOptions, } from "./adapter.js";
6
6
  export { RobonoAuthenticationError, RobonoError, RobonoWebhookError, } from "./errors.js";
@@ -35,5 +35,7 @@ export { RobonoHistoryImport } from './history-import.js';
35
35
  export { RobonoHistoryExport } from './history-export.js';
36
36
  export type { HistoryExportAttachment, HistoryExportSelection, HistoryExportCursor, HistoryExportStatus, HistoryExportPage, HistoryExportRecord } from './history-export.js';
37
37
  export type { HistoryImportMessage, HistoryImportStatus } from './history-import.js';
38
+ export { convertHistory } from './history-convert.js';
39
+ export type { HistoryMigrationMapping, HistoryMigrationPlan, HistoryConversionIssue } from './history-convert.js';
38
40
  export type { CustomMessageContent, MessageDataValue } from './generated/custom-messages.js';
39
41
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -23,4 +23,5 @@ export { RobonoMessengerConnections } from './messenger-connections.js';
23
23
  export { RobonoSessions, createRobonoTokenHandler } from './hosted-sessions.js';
24
24
  export { RobonoHistoryImport } from './history-import.js';
25
25
  export { RobonoHistoryExport } from './history-export.js';
26
+ export { convertHistory } from './history-convert.js';
26
27
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAM1D,OAAO,EACL,yBAAyB,EACzB,WAAW,EACX,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAGtE,OAAO,EAAE,sBAAsB,EAAE,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;AAGvF,OAAO,EAAC,kCAAkC,EAAC,MAAM,sBAAsB,CAAC;AAExE,OAAO,EAAC,kBAAkB,EAAC,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAC,sCAAsC,EAAC,MAAM,2BAA2B,CAAC;AAEjF,OAAO,EAAC,qBAAqB,EAAC,4BAA4B,EAAC,MAAM,eAAe,CAAC;AAEjF,OAAO,EAAC,yBAAyB,EAAC,MAAM,oBAAoB,CAAC;AAE7D,OAAO,EAAC,2BAA2B,EAA4B,MAAM,gBAAgB,CAAC;AAGtF,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uCAAuC,CAAC;AACtD,cAAc,sCAAsC,CAAC;AAErD,OAAO,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAG/C,OAAO,EAAC,yBAAyB,EAA0B,MAAM,sBAAsB,CAAC;AACxF,OAAO,EAAC,+BAA+B,EAAC,+BAA+B,EAAC,MAAM,qBAAqB,CAAC;AAGpG,OAAO,EAAC,4BAA4B,EAAC,mCAAmC,EAAC,MAAM,uBAAuB,CAAC;AAEvG,OAAO,EAAC,0BAA0B,EAAyD,MAAM,4BAA4B,CAAC;AAC9H,OAAO,EAAC,cAAc,EAAC,wBAAwB,EAA0B,MAAM,sBAAsB,CAAC;AAEtG,OAAO,EAAC,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAC,mBAAmB,EAAC,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAM1D,OAAO,EACL,yBAAyB,EACzB,WAAW,EACX,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAGtE,OAAO,EAAE,sBAAsB,EAAE,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;AAGvF,OAAO,EAAC,kCAAkC,EAAC,MAAM,sBAAsB,CAAC;AAExE,OAAO,EAAC,kBAAkB,EAAC,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAC,sCAAsC,EAAC,MAAM,2BAA2B,CAAC;AAEjF,OAAO,EAAC,qBAAqB,EAAC,4BAA4B,EAAC,MAAM,eAAe,CAAC;AAEjF,OAAO,EAAC,yBAAyB,EAAC,MAAM,oBAAoB,CAAC;AAE7D,OAAO,EAAC,2BAA2B,EAA4B,MAAM,gBAAgB,CAAC;AAGtF,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uCAAuC,CAAC;AACtD,cAAc,sCAAsC,CAAC;AAErD,OAAO,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAG/C,OAAO,EAAC,yBAAyB,EAA0B,MAAM,sBAAsB,CAAC;AACxF,OAAO,EAAC,+BAA+B,EAAC,+BAA+B,EAAC,MAAM,qBAAqB,CAAC;AAGpG,OAAO,EAAC,4BAA4B,EAAC,mCAAmC,EAAC,MAAM,uBAAuB,CAAC;AAEvG,OAAO,EAAC,0BAA0B,EAAyD,MAAM,4BAA4B,CAAC;AAC9H,OAAO,EAAC,cAAc,EAAC,wBAAwB,EAA0B,MAAM,sBAAsB,CAAC;AAEtG,OAAO,EAAC,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAC,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AAGxD,OAAO,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAC"}
@@ -1 +1 @@
1
- {"schema":1,"preview":{"package.json":"{\n \"name\": \"robono-messenger-preview\",\n \"private\": true,\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=22.12\"\n },\n \"scripts\": {\n \"start\": \"vite --host 127.0.0.1\",\n \"dev\": \"vite --host 127.0.0.1\",\n \"build\": \"tsc --noEmit && vite build\"\n },\n \"dependencies\": {\n \"@robono/messaging\": \"^0.7.0\",\n \"@robono/messaging-web\": \"^0.9.0\",\n \"react\": \"^19.2.3\",\n \"react-dom\": \"^19.2.3\",\n \"@robono/location\": \"^0.4.0\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^19.2.2\",\n \"@types/react-dom\": \"^19.2.2\",\n \"typescript\": \"^6.0.3\",\n \"vite\": \"^7.3.6\"\n }\n}\n","index.html":"<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Robono website messaging</title></head><body><div id=\"root\"><main style=\"font-family:system-ui;max-width:600px;margin:48px auto;padding:20px\"><h1>Robono website messaging</h1><p id=\"startup-message\">Opening the chat…</p><noscript>Enable JavaScript to use this example.</noscript></main></div><script>if(location.protocol==='file:')document.getElementById('startup-message').textContent='Start this example with npm start in its folder, then open http://127.0.0.1:5188 in your browser. Opening this file directly cannot connect the chat.';window.addEventListener('error',function(){var message=document.getElementById('startup-message');if(message&&location.protocol!=='file:')message.textContent='The chat could not start. Check that npm start is still running, then refresh this page.';},true);</script><script type=\"module\" src=\"/src/main.tsx\"></script></body></html>\n","tsconfig.json":"{\n \"compilerOptions\": {\n \"target\": \"ES2022\", \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"ESNext\", \"moduleResolution\": \"Bundler\", \"jsx\": \"react-jsx\",\n \"strict\": true, \"skipLibCheck\": true, \"esModuleInterop\": true,\n \"noEmit\": true, \"types\": [\"vite/client\"]\n },\n \"include\": [\"src\"]\n}\n","src/main.tsx":"import React from 'react';\nimport {createRoot} from 'react-dom/client';\nimport RobonoPreview from './RobonoPreview';\ncreateRoot(document.getElementById('root')!).render(<RobonoPreview/>);\n","src/RobonoPreview.tsx":"'use client';\nimport React,{useEffect,useRef} from 'react';\nimport {mountRobonoMessenger} from '@robono/messaging-web';\nimport {defaultMessagingSettings} from '@robono/messaging';\nexport default function RobonoPreview(){\n const alice=useRef<HTMLDivElement>(null),bob=useRef<HTMLDivElement>(null);\n useEffect(()=>{\n if(!alice.current||!bob.current)return;\n const settings=defaultMessagingSettings();settings.enabled=true;\n const messages:any[]=[];\n const transport=(userId:string):typeof fetch=>async(_request,init)=>{\n const body=JSON.parse(String(init?.body??'{}')),other=userId==='alice'?'bob':'alice';let result:unknown;\n switch(body.operation){\n case 'settings':result={settings};break;\n case 'conversations':result={appearance:settings.appearance,conversations:[{id:'demo-chat',kind:'direct',contactId:other,lastMessage:messages.at(-1)??null,unreadCount:0,canSend:true}],hasMore:false,nextCursor:null};break;\n case 'list':result={settings,messages:[...messages],hasMore:false,nextCursor:null,conversation:{kind:'direct',canSend:true}};break;\n case 'send':{\n let message=messages.find(m=>m.senderId===userId&&m.clientMessageId===body.clientMessageId);\n if(!message){message={...body,id:String(messages.length+1),senderId:userId,createdAt:new Date().toISOString(),readAt:null};messages.push(message);}\n result={message};break;\n }\n case 'read':for(const m of messages)if(m.senderId!==userId)m.readAt=new Date().toISOString();result={ok:true};break;\n case 'seen':case 'inventory':result={ok:true};break;\n default:return Response.json({error:'This local preview demonstrates text only. Connect your test account to try other features.'},{status:400});\n }\n return Response.json(result);\n };\n const mount=(element:HTMLDivElement,userId:string)=>mountRobonoMessenger(element,{companyName:'Example company',userId,getAccessToken:async()=>'local-preview-only',contacts:{alice:{name:'Alice'},bob:{name:'Bob'}},endpoint:'/local-preview-never-sent',fetch:transport(userId),notifications:false,mediaUrl:false,persistence:false});\n const a=mount(alice.current,'alice'),b=mount(bob.current,'bob');return()=>{a.unmount();b.unmount();};\n },[]);\n return <main style={{padding:16,fontFamily:'system-ui'}}><h1>Try a conversation</h1><p>Local preview · Fictional Alice and Bob · Text stays in this page and disappears on reload. This does not test your live connection.</p><p>Open Bob on the left and Alice on the right, then send a message.</p><div style={{display:'flex',flexWrap:'wrap',gap:16}}><section style={{flex:'1 1 320px'}}><h2>Alice</h2><div ref={alice} style={{height:640}}/></section><section style={{flex:'1 1 320px'}}><h2>Bob</h2><div ref={bob} style={{height:640}}/></section></div></main>;\n}\n","src/vite-env.d.ts":"/// <reference types=\"vite/client\" />\n","README.md":"# Robono Messenger preview\n\nRun `npm start`, then open the printed localhost address. This uses the supplied Messenger with fictional Alice and Bob; it sends no messages to Robono and clears on reload. It demonstrates text and layout, not uploads, calls, Maps or live delivery.\n\nFor a real test connection, generate a separate example with `npx --package @robono/server robono-messenger-example --live`. For your own app, run `npx --package @robono/server robono-messenger-install` inside that app and follow https://robono.com/guides/hosted-messenger.html . Keep your existing login and server-side permissions.\n",".gitignore":"node_modules/\ndist/\n.env\n.env.*.local\n"},"live":{"package.json":"{\n \"name\": \"robono-messaging-web-example\",\n \"private\": true,\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=22.12\"\n },\n \"scripts\": {\n \"dev\": \"node --env-file-if-exists=.env server.mjs\",\n \"build\": \"tsc --noEmit && vite build\",\n \"start\": \"node --env-file-if-exists=.env server.mjs\"\n },\n \"dependencies\": {\n \"@robono/messaging\": \"^0.7.0\",\n \"@robono/messaging-web\": \"^0.9.0\",\n \"@robono/server\": \"^0.26.0\",\n \"react\": \"^19.2.3\",\n \"react-dom\": \"^19.2.3\",\n \"@robono/location\": \"^0.4.0\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^19.2.2\",\n \"@types/react-dom\": \"^19.2.2\",\n \"typescript\": \"^6.0.3\",\n \"vite\": \"^7.3.6\"\n }\n}\n","server.mjs":"// Local example only. Production hosts supply their own identity/session service.\nimport http from \"node:http\";\nimport { randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { pathToFileURL } from \"node:url\";\nimport * as publishedSdk from \"@robono/server\";\nexport async function startExample({\n apiKey = process.env.ROBONO_API_KEY,\n password = process.env.DEMO_PASSWORD,\n baseUrl = process.env.ROBONO_MESSAGING_URL,\n sessionsUrl,\n connectionsUrl,\n port = Number(process.env.PORT || 5188),\n userPrefix = process.env.DEMO_USER_PREFIX || \"website-demo\",\n robono: provided,\n // Repository development only. Downloaded examples use the published packages.\n workspaceSdk = process.env.ROBONO_WORKSPACE_SDK === '1',\n} = {}) {\n const {RobonoMessagingBackend,RobonoMessengerConnections,createRobonoTokenHandler} = workspaceSdk\n ? await import('../../packages/server/dist/index.js') : publishedSdk;\n if (process.env.NODE_ENV === \"production\")\n throw Error(\n \"The example sign-in is for local development. Use your own production authentication.\",\n );\n if (!password || password.length < 12)\n throw Error(\"Set DEMO_PASSWORD to at least 12 characters.\");\n if (!apiKey)\n throw Error(\"Set your test integration ROBONO_API_KEY.\");\n const robono =\n provided ??\n new RobonoMessagingBackend({ apiKey, ...(baseUrl ? { baseUrl } : {}) });\n const users = { alice: userPrefix + \"-alice\", bob: userPrefix + \"-bob\" };\n const connections=new RobonoMessengerConnections({apiKey,...(connectionsUrl?{baseUrl:connectionsUrl}:{})});\n const relationship = await connections.synchronizeContact({\n externalUserIds: Object.values(users),\n allowed: true,\n revision: 1,\n });\n const group = await robono.setGroup({\n externalGroupId: userPrefix + \"-group\",\n name: \"Website group\",\n ownerId: users.alice,\n revision: 1,\n allowed: true,\n members: Object.values(users).map((userId) => ({ userId, canSend: true })),\n });\n const conversations = [\n { id: relationship.conversationId, name: \"Direct conversation\" },\n { id: group.conversationId, name: \"Website group\" },\n ];\n const sessions = new Map(),\n attempts = new Map();\n const authenticate = async (request) => {\n const token = request.headers\n .get(\"authorization\")\n ?.replace(/^Bearer\\s+/i, \"\");\n const session = sessions.get(token);\n return session && session.expires > Date.now() ? session.userId : null;\n };\n let tokenHandler;\n const { createServer } = await import(\"vite\");\n let vite;\n const server = http.createServer(async (req, res) => {\n const send = (value, status = 200) => {\n res.writeHead(status, {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n });\n res.end(JSON.stringify(value));\n };\n const host = \"127.0.0.1:\" + server.address().port,\n origin = \"http://\" + host;\n if (\n req.headers.host !== host ||\n (req.headers.origin && req.headers.origin !== origin)\n )\n return send({ error: { message: \"Origin not allowed.\" } }, 403);\n if (!req.url?.startsWith(\"/api/\"))\n return vite.middlewares(req, res, () =>\n send({ error: { message: \"Not found.\" } }, 404),\n );\n if (req.method !== \"POST\")\n return send({ error: { message: \"Use POST.\" } }, 405);\n try {\n let raw = \"\";\n for await (const part of req) {\n raw += part;\n if (raw.length > 65536)\n return send({ error: { message: \"Request too large.\" } }, 413);\n }\n const body = JSON.parse(raw || \"{}\");\n if (req.url === \"/api/session\") {\n const address = req.socket.remoteAddress,\n now = Date.now();\n const attempt = attempts.get(address);\n const count =\n attempt && attempt.reset > now\n ? attempt\n : { count: 0, reset: now + 60000 };\n attempts.set(address, count);\n if (++count.count > 10)\n return send({ error: { message: \"Try again in a minute.\" } }, 429);\n const received = Buffer.from(String(body.password ?? \"\")),\n expected = Buffer.from(password);\n if (\n !Object.hasOwn(users, body.user) ||\n received.length !== expected.length ||\n !timingSafeEqual(received, expected)\n )\n return send({ error: { message: \"Sign-in failed.\" } }, 401);\n const token = randomBytes(32).toString(\"base64url\");\n sessions.set(token, {\n userId: users[body.user],\n expires: now + 3600000,\n });\n return send({\n token,\n userId: users[body.user],\n conversations,\n participants: Object.fromEntries(\n Object.entries(users).map(([name, id]) => [\n id,\n { name: name === \"alice\" ? \"Alice\" : \"Bob\" },\n ]),\n ),\n });\n }\n const request = new Request(origin + req.url, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: req.headers.authorization ?? \"\",\n ...(req.headers.origin?{origin:req.headers.origin}:{}),\n },\n body: raw,\n });\n if (req.url === \"/api/logout\") {\n sessions.delete(req.headers.authorization?.replace(/^Bearer\\s+/i, \"\"));\n return send({ ok: true });\n }\n if(new URL(request.url).pathname!==\"/api/robono-token\")return send({error:{message:\"Not found.\"}},404);\n const response = await tokenHandler(request);\n res.writeHead(response.status, Object.fromEntries(response.headers));\n res.end(await response.text());\n } catch {\n send({ error: { message: \"Request failed.\" } }, 503);\n }\n });\n vite = await createServer({\n root: new URL(\".\", import.meta.url).pathname,\n server: { middlewareMode: true, hmr: {server} },\n appType: \"spa\",\n cacheDir: \"node_modules/.vite-robono-example\",\n resolve: { dedupe: [\"react\", \"react-dom\", \"@robono/messaging\"], ...(workspaceSdk ? {alias:{\n '@robono/location':new URL('../../packages/location/dist/index.js',import.meta.url).pathname,\n '@robono/messaging':new URL('../../packages/messaging/dist/index.js',import.meta.url).pathname,\n '@robono/messaging-web':new URL('../../packages/messaging-web/dist/index.js',import.meta.url).pathname,\n }} : {}) },\n ...(workspaceSdk ? {optimizeDeps:{noDiscovery:true,include:['react','react-dom','react-dom/client','react/jsx-runtime','react/jsx-dev-runtime'],exclude:['@robono/messaging','@robono/messaging-web','@robono/location','make-plural/cardinals']}} : {}),\n });\n await new Promise((resolve) => server.listen(port, \"127.0.0.1\", resolve));\n tokenHandler=createRobonoTokenHandler({apiKey,authenticate,origins:[\"http://127.0.0.1:\"+server.address().port],...(sessionsUrl?{baseUrl:sessionsUrl}:{})});\n return {\n url: \"http://127.0.0.1:\" + server.address().port,\n users,\n conversations,\n async close() {\n await vite.close();\n server.closeAllConnections();\n await new Promise((resolve) => server.close(resolve));\n },\n };\n}\nif (\n process.argv[1] &&\n import.meta.url === pathToFileURL(process.argv[1]).href\n) {\n const app = await startExample();\n console.log(\"Website messaging example: \" + app.url);\n for (const signal of [\"SIGINT\", \"SIGTERM\"])\n process.once(signal, () => void app.close().then(() => process.exit()));\n}\n","index.html":"<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Robono website messaging</title></head><body><div id=\"root\"><main style=\"font-family:system-ui;max-width:600px;margin:48px auto;padding:20px\"><h1>Robono website messaging</h1><p id=\"startup-message\">Opening the chat…</p><noscript>Enable JavaScript to use this example.</noscript></main></div><script>if(location.protocol==='file:')document.getElementById('startup-message').textContent='Start this example with npm start in its folder, then open http://127.0.0.1:5188 in your browser. Opening this file directly cannot connect the chat.';window.addEventListener('error',function(){var message=document.getElementById('startup-message');if(message&&location.protocol!=='file:')message.textContent='The chat could not start. Check that npm start is still running, then refresh this page.';},true);</script><script type=\"module\" src=\"/src/main.tsx\"></script></body></html>\n","tsconfig.json":"{\n \"compilerOptions\": {\n \"target\": \"ES2022\", \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"ESNext\", \"moduleResolution\": \"Bundler\", \"jsx\": \"react-jsx\",\n \"strict\": true, \"skipLibCheck\": true, \"esModuleInterop\": true,\n \"noEmit\": true, \"types\": [\"vite/client\"]\n },\n \"include\": [\"src\"]\n}\n","src/main.tsx":"import React, { useEffect, useState, useRef } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport {mountHostedRobonoMessenger} from \"@robono/messaging-web\";\nimport {hostedRobonoTokenProvider} from \"@robono/messaging\";\nimport \"./style.css\";\ntype Session = {\n token: string;\n userId: string;\n conversations: { id: string; name: string }[];\n participants: Record<string, { name: string }>;\n};\nfunction App() {\n const [session,setSession]=useState<Session|null>(null),[error,setError]=useState(''),[busy,setBusy]=useState(false),[attempt,setAttempt]=useState(0);\n const host=useRef<HTMLDivElement>(null),messenger=useRef<Awaited<ReturnType<typeof mountHostedRobonoMessenger>>|null>(null);\n useEffect(()=>{\n if(!session||!host.current)return;\n const controller=new AbortController();let mounted:Awaited<ReturnType<typeof mountHostedRobonoMessenger>>|undefined;setError('');\n void mountHostedRobonoMessenger(host.current,{\n userId:session.userId,signal:controller.signal,\n tokenProvider:hostedRobonoTokenProvider({tokenEndpoint:'/api/robono-token',getAccessToken:async()=>session.token}),\n companyName:'Example company',contacts:session.participants,participants:session.participants,\n account:{name:session.participants[session.userId]?.name??'Example account'},style:{height:'min(700px,75dvh)'},\n }).then(value=>{if(controller.signal.aborted){void value.signOut().catch(()=>{});return;}mounted=value;messenger.current=value;}).catch(()=>{if(!controller.signal.aborted)setError('Could not connect to messaging. Check your sign-in connection.');});\n return()=>{controller.abort();if(messenger.current===mounted)messenger.current=null;mounted?.unmount();};\n },[session,attempt]);\n async function login(e: React.FormEvent<HTMLFormElement>) {\n e.preventDefault();\n setBusy(true);\n setError(\"\");\n const form = new FormData(e.currentTarget);\n try {\n const r = await fetch(\"/api/session\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(Object.fromEntries(form)),\n });\n const data = await r.json();\n if (!r.ok) throw Error(data.error?.message ?? \"Sign-in failed.\");\n setSession(data);\n } catch (e) {\n setError((e as Error).message);\n } finally {\n setBusy(false);\n }\n }\n async function logout() {\n const token = session?.token;\n try {\n await messenger.current?.signOut();\n } catch {\n setError(\"Signed out, but this browser could not erase saved drafts. Clear this site's browser data before sharing this computer.\");\n } finally {\n setSession(null);\n }\n if (token)\n await fetch(\"/api/logout\", {\n method: \"POST\",\n headers: {\n authorization: \"Bearer \" + token,\n \"content-type\": \"application/json\",\n },\n body: \"{}\",\n }).catch(() => {});\n }\n return (\n <main>\n <h1>Robono website messaging</h1>\n <p>\n This local example uses your test integration and your existing\n messaging design.\n </p>\n {!session ? (\n <form onSubmit={login}>\n <label>\n Example user\n <select name=\"user\">\n <option value=\"alice\">Alice</option>\n <option value=\"bob\">Bob</option>\n </select>\n </label>\n <label>\n Demo password\n <input\n name=\"password\"\n type=\"password\"\n required\n autoComplete=\"current-password\"\n />\n </label>\n <button disabled={busy}>Sign in</button>\n </form>\n ) : (\n <>\n <div className=\"toolbar\"><button onClick={()=>void logout()}>Sign out</button></div>\n <div ref={host} style={{height:'min(700px,75dvh)'}}/>\n </>\n )}\n {error && <p role=\"alert\">{error} {session&&<button onClick={()=>setAttempt(value=>value+1)}>Retry</button>}</p>}\n </main>\n );\n}\ncreateRoot(document.getElementById(\"root\")!).render(\n <React.StrictMode>\n <App />\n </React.StrictMode>,\n);\n","src/style.css":"body {\n margin: 0;\n background: #f4f5f2;\n color: #252b21;\n font:\n 16px system-ui,\n sans-serif;\n}\nmain {\n max-width: 960px;\n margin: 24px auto;\n padding: 0 16px;\n}\nh1 {\n font-size: 24px;\n}\nform,\n.toolbar {\n display: flex;\n gap: 12px;\n align-items: end;\n flex-wrap: wrap;\n margin: 16px 0;\n}\nform label,\n.toolbar label {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\nform input,\nform select,\n.toolbar select,\nform button,\n.toolbar button {\n font: inherit;\n padding: 10px;\n border: 1px solid #bfc6ba;\n border-radius: 8px;\n background: white;\n}\nbutton {\n cursor: pointer;\n}\nmain > section {\n border: 1px solid #cbd0c6;\n border-radius: 12px;\n}\np[role=\"alert\"] {\n color: #9b241f;\n}\n@media (max-width: 500px) {\n main {\n margin: 12px auto;\n padding: 0 8px;\n }\n h1 {\n font-size: 20px;\n }\n .toolbar {\n margin: 10px 0;\n }\n}\n",".env.example":"# Server only. Never prefix this key with VITE_. Use a TEST integration.\nROBONO_API_KEY=\n# Local demo sign-in only; replace with your own identity service in production.\nDEMO_PASSWORD=\nPORT=5188\n# Each independent example installation can choose its own fixture prefix.\nDEMO_USER_PREFIX=website-demo\n","README.md":"# Website messaging integration example\n\nThis example uses Robono's hosted Messenger. Its small local server signs Alice or Bob in and issues a short-lived user token. Messages, files, calls and Maps connect directly to Robono; they do not pass through that local server. The Robono API key stays on the server.\n\nRequirements: Node 22+, a Robono **test** integration with Messaging and Group messaging enabled. Choose the tools and appearance in **Features & preview**.\n\n```sh\ncd examples/messaging-web\nnpm install\ncp .env.example .env\n# Set ROBONO_API_KEY and a random DEMO_PASSWORD of at least 12 characters.\nnpm start\n```\n\nOpen the printed `http://127.0.0.1:5188` address in two browser profiles. Sign in as Alice and Bob with the demo password. Open the direct conversation or Website group and exchange messages. Selected photos and videos have removable thumbnails; sent images open in the supplied viewer. Interrupted sends remain available for recovery.\n\nThe default test users are `website-demo-alice` and `website-demo-bob`. Use a different `DEMO_USER_PREFIX` for independent demonstrations. The example creates these contacts and a group at revision 1. Do not reuse that prefix for real users or independently change these memberships. Sent messages and attachments remain subject to your test integration's storage, retention and usage settings after the example stops.\n\n## Use in your application\n\nThe automatic installer is the recommended starting point:\n\n```sh\nnpx --package @robono/server robono-messenger-install\n```\n\nKeep your existing sign-in. The generated `/api/robono-token` handler verifies that login and obtains a token for that verified user. Synchronize who may communicate, group membership and blocks from your own authoritative records. Pass your users' names and avatars to the supplied Messenger. No chat-forwarding server is required.\n\nThe password sign-in in this example is **local development only**. The example binds to localhost and refuses production mode. `npm run build` validates the frontend; it does not turn the example's authentication into a production login service. Never expose the demo server or put the private API key in browser files.\n\nFor notifications, register your HTTPS website origin and chat page in **Your app → Message notifications**. The installer puts the notification helper on your own website; browser rules require this small file on the same origin. Users enable alerts in the supplied Account screen. Website notifications do not require Firebase credentials. On iPhone, browser notifications require a Home Screen installation. Native iPhone/Android apps need their own Apple/Google notification credentials, as explained during setup.\n\nCall `await chat.signOut()` before ending your application's user session. This clears private saved work and revokes the Robono session. The supplied example's Sign out button follows that order.\n\nSee [the hosted setup guide](https://robono.com/guides/hosted-messenger.html) for supported frameworks, existing login connections and optional customization. Repository development can use `ROBONO_WORKSPACE_SDK=1 npm start` after building the SDK packages. Downloaded files always use the published packages.\n",".gitignore":"node_modules/\ndist/\n.env\n.env.*.local\n"}}
1
+ {"schema":1,"preview":{"package.json":"{\n \"name\": \"robono-messenger-preview\",\n \"private\": true,\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=22.12\"\n },\n \"scripts\": {\n \"start\": \"vite --host 127.0.0.1\",\n \"dev\": \"vite --host 127.0.0.1\",\n \"build\": \"tsc --noEmit && vite build\"\n },\n \"dependencies\": {\n \"@robono/messaging\": \"^0.7.1\",\n \"@robono/messaging-web\": \"^0.10.0\",\n \"react\": \"^19.2.3\",\n \"react-dom\": \"^19.2.3\",\n \"@robono/location\": \"^0.4.0\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^19.2.2\",\n \"@types/react-dom\": \"^19.2.2\",\n \"typescript\": \"^6.0.3\",\n \"vite\": \"^7.3.6\"\n }\n}\n","index.html":"<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Robono website messaging</title></head><body><div id=\"root\"><main style=\"font-family:system-ui;max-width:600px;margin:48px auto;padding:20px\"><h1>Robono website messaging</h1><p id=\"startup-message\">Opening the chat…</p><noscript>Enable JavaScript to use this example.</noscript></main></div><script>if(location.protocol==='file:')document.getElementById('startup-message').textContent='Start this example with npm start in its folder, then open http://127.0.0.1:5188 in your browser. Opening this file directly cannot connect the chat.';window.addEventListener('error',function(){var message=document.getElementById('startup-message');if(message&&location.protocol!=='file:')message.textContent='The chat could not start. Check that npm start is still running, then refresh this page.';},true);</script><script type=\"module\" src=\"/src/main.tsx\"></script></body></html>\n","tsconfig.json":"{\n \"compilerOptions\": {\n \"target\": \"ES2022\", \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"ESNext\", \"moduleResolution\": \"Bundler\", \"jsx\": \"react-jsx\",\n \"strict\": true, \"skipLibCheck\": true, \"esModuleInterop\": true,\n \"noEmit\": true, \"types\": [\"vite/client\"]\n },\n \"include\": [\"src\"]\n}\n","src/main.tsx":"import React from 'react';\nimport {createRoot} from 'react-dom/client';\nimport RobonoPreview from './RobonoPreview';\ncreateRoot(document.getElementById('root')!).render(<RobonoPreview/>);\n","src/RobonoPreview.tsx":"'use client';\nimport React,{useEffect,useRef} from 'react';\nimport {mountRobonoMessenger} from '@robono/messaging-web';\nimport {defaultMessagingSettings} from '@robono/messaging';\nexport default function RobonoPreview(){\n const alice=useRef<HTMLDivElement>(null),bob=useRef<HTMLDivElement>(null);\n useEffect(()=>{\n if(!alice.current||!bob.current)return;\n const settings=defaultMessagingSettings();settings.enabled=true;\n const messages:any[]=[];\n const transport=(userId:string):typeof fetch=>async(_request,init)=>{\n const body=JSON.parse(String(init?.body??'{}')),other=userId==='alice'?'bob':'alice';let result:unknown;\n switch(body.operation){\n case 'settings':result={settings};break;\n case 'conversations':result={appearance:settings.appearance,conversations:[{id:'demo-chat',kind:'direct',contactId:other,lastMessage:messages.at(-1)??null,unreadCount:0,canSend:true}],hasMore:false,nextCursor:null};break;\n case 'list':result={settings,messages:[...messages],hasMore:false,nextCursor:null,conversation:{kind:'direct',canSend:true}};break;\n case 'send':{\n let message=messages.find(m=>m.senderId===userId&&m.clientMessageId===body.clientMessageId);\n if(!message){message={...body,id:String(messages.length+1),senderId:userId,createdAt:new Date().toISOString(),readAt:null};messages.push(message);}\n result={message};break;\n }\n case 'read':for(const m of messages)if(m.senderId!==userId)m.readAt=new Date().toISOString();result={ok:true};break;\n case 'seen':case 'inventory':result={ok:true};break;\n default:return Response.json({error:'This local preview demonstrates text only. Connect your test account to try other features.'},{status:400});\n }\n return Response.json(result);\n };\n const mount=(element:HTMLDivElement,userId:string)=>mountRobonoMessenger(element,{companyName:'Example company',userId,getAccessToken:async()=>'local-preview-only',contacts:{alice:{name:'Alice'},bob:{name:'Bob'}},endpoint:'/local-preview-never-sent',fetch:transport(userId),notifications:false,mediaUrl:false,persistence:false});\n const a=mount(alice.current,'alice'),b=mount(bob.current,'bob');return()=>{a.unmount();b.unmount();};\n },[]);\n return <main style={{padding:16,fontFamily:'system-ui'}}><h1>Try a conversation</h1><p>Local preview · Fictional Alice and Bob · Text stays in this page and disappears on reload. This does not test your live connection.</p><p>Open Bob on the left and Alice on the right, then send a message.</p><div style={{display:'flex',flexWrap:'wrap',gap:16}}><section style={{flex:'1 1 320px'}}><h2>Alice</h2><div ref={alice} style={{height:640}}/></section><section style={{flex:'1 1 320px'}}><h2>Bob</h2><div ref={bob} style={{height:640}}/></section></div></main>;\n}\n","src/vite-env.d.ts":"/// <reference types=\"vite/client\" />\n","README.md":"# Robono Messenger preview\n\nRun `npm start`, then open the printed localhost address. This uses the supplied Messenger with fictional Alice and Bob; it sends no messages to Robono and clears on reload. It demonstrates text and layout, not uploads, calls, Maps or live delivery.\n\nFor a real test connection, generate a separate example with `npx --package @robono/server robono-messenger-example --live`. For your own app, run `npx --package @robono/server robono-messenger-install` inside that app and follow https://robono.com/guides/hosted-messenger.html . Keep your existing login and server-side permissions.\n",".gitignore":"node_modules/\ndist/\n.env\n.env.*.local\n"},"live":{"package.json":"{\n \"name\": \"robono-messaging-web-example\",\n \"private\": true,\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=22.12\"\n },\n \"scripts\": {\n \"dev\": \"node --env-file-if-exists=.env server.mjs\",\n \"build\": \"tsc --noEmit && vite build\",\n \"start\": \"node --env-file-if-exists=.env server.mjs\"\n },\n \"dependencies\": {\n \"@robono/messaging\": \"^0.7.1\",\n \"@robono/messaging-web\": \"^0.10.0\",\n \"@robono/server\": \"^0.27.0\",\n \"react\": \"^19.2.3\",\n \"react-dom\": \"^19.2.3\",\n \"@robono/location\": \"^0.4.0\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^19.2.2\",\n \"@types/react-dom\": \"^19.2.2\",\n \"typescript\": \"^6.0.3\",\n \"vite\": \"^7.3.6\"\n }\n}\n","server.mjs":"// Local example only. Production hosts supply their own identity/session service.\nimport http from \"node:http\";\nimport { randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { pathToFileURL } from \"node:url\";\nimport * as publishedSdk from \"@robono/server\";\nexport async function startExample({\n apiKey = process.env.ROBONO_API_KEY,\n password = process.env.DEMO_PASSWORD,\n baseUrl = process.env.ROBONO_MESSAGING_URL,\n sessionsUrl,\n connectionsUrl,\n port = Number(process.env.PORT || 5188),\n userPrefix = process.env.DEMO_USER_PREFIX || \"website-demo\",\n robono: provided,\n // Repository development only. Downloaded examples use the published packages.\n workspaceSdk = process.env.ROBONO_WORKSPACE_SDK === '1',\n} = {}) {\n const {RobonoMessagingBackend,RobonoMessengerConnections,createRobonoTokenHandler} = workspaceSdk\n ? await import('../../packages/server/dist/index.js') : publishedSdk;\n if (process.env.NODE_ENV === \"production\")\n throw Error(\n \"The example sign-in is for local development. Use your own production authentication.\",\n );\n if (!password || password.length < 12)\n throw Error(\"Set DEMO_PASSWORD to at least 12 characters.\");\n if (!apiKey)\n throw Error(\"Set your test integration ROBONO_API_KEY.\");\n const robono =\n provided ??\n new RobonoMessagingBackend({ apiKey, ...(baseUrl ? { baseUrl } : {}) });\n const users = { alice: userPrefix + \"-alice\", bob: userPrefix + \"-bob\" };\n const connections=new RobonoMessengerConnections({apiKey,...(connectionsUrl?{baseUrl:connectionsUrl}:{})});\n const relationship = await connections.synchronizeContact({\n externalUserIds: Object.values(users),\n allowed: true,\n revision: 1,\n });\n const group = await robono.setGroup({\n externalGroupId: userPrefix + \"-group\",\n name: \"Website group\",\n ownerId: users.alice,\n revision: 1,\n allowed: true,\n members: Object.values(users).map((userId) => ({ userId, canSend: true })),\n });\n const conversations = [\n { id: relationship.conversationId, name: \"Direct conversation\" },\n { id: group.conversationId, name: \"Website group\" },\n ];\n const sessions = new Map(),\n attempts = new Map();\n const authenticate = async (request) => {\n const token = request.headers\n .get(\"authorization\")\n ?.replace(/^Bearer\\s+/i, \"\");\n const session = sessions.get(token);\n return session && session.expires > Date.now() ? session.userId : null;\n };\n let tokenHandler;\n const { createServer } = await import(\"vite\");\n let vite;\n const server = http.createServer(async (req, res) => {\n const send = (value, status = 200) => {\n res.writeHead(status, {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n });\n res.end(JSON.stringify(value));\n };\n const host = \"127.0.0.1:\" + server.address().port,\n origin = \"http://\" + host;\n if (\n req.headers.host !== host ||\n (req.headers.origin && req.headers.origin !== origin)\n )\n return send({ error: { message: \"Origin not allowed.\" } }, 403);\n if (!req.url?.startsWith(\"/api/\"))\n return vite.middlewares(req, res, () =>\n send({ error: { message: \"Not found.\" } }, 404),\n );\n if (req.method !== \"POST\")\n return send({ error: { message: \"Use POST.\" } }, 405);\n try {\n let raw = \"\";\n for await (const part of req) {\n raw += part;\n if (raw.length > 65536)\n return send({ error: { message: \"Request too large.\" } }, 413);\n }\n const body = JSON.parse(raw || \"{}\");\n if (req.url === \"/api/session\") {\n const address = req.socket.remoteAddress,\n now = Date.now();\n const attempt = attempts.get(address);\n const count =\n attempt && attempt.reset > now\n ? attempt\n : { count: 0, reset: now + 60000 };\n attempts.set(address, count);\n if (++count.count > 10)\n return send({ error: { message: \"Try again in a minute.\" } }, 429);\n const received = Buffer.from(String(body.password ?? \"\")),\n expected = Buffer.from(password);\n if (\n !Object.hasOwn(users, body.user) ||\n received.length !== expected.length ||\n !timingSafeEqual(received, expected)\n )\n return send({ error: { message: \"Sign-in failed.\" } }, 401);\n const token = randomBytes(32).toString(\"base64url\");\n sessions.set(token, {\n userId: users[body.user],\n expires: now + 3600000,\n });\n return send({\n token,\n userId: users[body.user],\n conversations,\n participants: Object.fromEntries(\n Object.entries(users).map(([name, id]) => [\n id,\n { name: name === \"alice\" ? \"Alice\" : \"Bob\" },\n ]),\n ),\n });\n }\n const request = new Request(origin + req.url, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: req.headers.authorization ?? \"\",\n ...(req.headers.origin?{origin:req.headers.origin}:{}),\n },\n body: raw,\n });\n if (req.url === \"/api/logout\") {\n sessions.delete(req.headers.authorization?.replace(/^Bearer\\s+/i, \"\"));\n return send({ ok: true });\n }\n if(new URL(request.url).pathname!==\"/api/robono-token\")return send({error:{message:\"Not found.\"}},404);\n const response = await tokenHandler(request);\n res.writeHead(response.status, Object.fromEntries(response.headers));\n res.end(await response.text());\n } catch {\n send({ error: { message: \"Request failed.\" } }, 503);\n }\n });\n vite = await createServer({\n root: new URL(\".\", import.meta.url).pathname,\n server: { middlewareMode: true, hmr: {server} },\n appType: \"spa\",\n cacheDir: \"node_modules/.vite-robono-example\",\n resolve: { dedupe: [\"react\", \"react-dom\", \"@robono/messaging\"], ...(workspaceSdk ? {alias:{\n '@robono/location':new URL('../../packages/location/dist/index.js',import.meta.url).pathname,\n '@robono/messaging':new URL('../../packages/messaging/dist/index.js',import.meta.url).pathname,\n '@robono/messaging-web':new URL('../../packages/messaging-web/dist/index.js',import.meta.url).pathname,\n }} : {}) },\n ...(workspaceSdk ? {optimizeDeps:{noDiscovery:true,include:['react','react-dom','react-dom/client','react/jsx-runtime','react/jsx-dev-runtime'],exclude:['@robono/messaging','@robono/messaging-web','@robono/location','make-plural/cardinals']}} : {}),\n });\n await new Promise((resolve) => server.listen(port, \"127.0.0.1\", resolve));\n tokenHandler=createRobonoTokenHandler({apiKey,authenticate,origins:[\"http://127.0.0.1:\"+server.address().port],...(sessionsUrl?{baseUrl:sessionsUrl}:{})});\n return {\n url: \"http://127.0.0.1:\" + server.address().port,\n users,\n conversations,\n async close() {\n await vite.close();\n server.closeAllConnections();\n await new Promise((resolve) => server.close(resolve));\n },\n };\n}\nif (\n process.argv[1] &&\n import.meta.url === pathToFileURL(process.argv[1]).href\n) {\n const app = await startExample();\n console.log(\"Website messaging example: \" + app.url);\n for (const signal of [\"SIGINT\", \"SIGTERM\"])\n process.once(signal, () => void app.close().then(() => process.exit()));\n}\n","index.html":"<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Robono website messaging</title></head><body><div id=\"root\"><main style=\"font-family:system-ui;max-width:600px;margin:48px auto;padding:20px\"><h1>Robono website messaging</h1><p id=\"startup-message\">Opening the chat…</p><noscript>Enable JavaScript to use this example.</noscript></main></div><script>if(location.protocol==='file:')document.getElementById('startup-message').textContent='Start this example with npm start in its folder, then open http://127.0.0.1:5188 in your browser. Opening this file directly cannot connect the chat.';window.addEventListener('error',function(){var message=document.getElementById('startup-message');if(message&&location.protocol!=='file:')message.textContent='The chat could not start. Check that npm start is still running, then refresh this page.';},true);</script><script type=\"module\" src=\"/src/main.tsx\"></script></body></html>\n","tsconfig.json":"{\n \"compilerOptions\": {\n \"target\": \"ES2022\", \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"ESNext\", \"moduleResolution\": \"Bundler\", \"jsx\": \"react-jsx\",\n \"strict\": true, \"skipLibCheck\": true, \"esModuleInterop\": true,\n \"noEmit\": true, \"types\": [\"vite/client\"]\n },\n \"include\": [\"src\"]\n}\n","src/main.tsx":"import React, { useEffect, useState, useRef } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport {mountHostedRobonoMessenger} from \"@robono/messaging-web\";\nimport {hostedRobonoTokenProvider} from \"@robono/messaging\";\nimport \"./style.css\";\ntype Session = {\n token: string;\n userId: string;\n conversations: { id: string; name: string }[];\n participants: Record<string, { name: string }>;\n};\nfunction App() {\n const [session,setSession]=useState<Session|null>(null),[error,setError]=useState(''),[busy,setBusy]=useState(false),[attempt,setAttempt]=useState(0);\n const host=useRef<HTMLDivElement>(null),messenger=useRef<Awaited<ReturnType<typeof mountHostedRobonoMessenger>>|null>(null);\n useEffect(()=>{\n if(!session||!host.current)return;\n const controller=new AbortController();let mounted:Awaited<ReturnType<typeof mountHostedRobonoMessenger>>|undefined;setError('');\n void mountHostedRobonoMessenger(host.current,{\n userId:session.userId,signal:controller.signal,\n tokenProvider:hostedRobonoTokenProvider({tokenEndpoint:'/api/robono-token',getAccessToken:async()=>session.token}),\n companyName:'Example company',contacts:session.participants,participants:session.participants,\n account:{name:session.participants[session.userId]?.name??'Example account'},style:{height:'min(700px,75dvh)'},\n }).then(value=>{if(controller.signal.aborted){void value.signOut().catch(()=>{});return;}mounted=value;messenger.current=value;}).catch(()=>{if(!controller.signal.aborted)setError('Could not connect to messaging. Check your sign-in connection.');});\n return()=>{controller.abort();if(messenger.current===mounted)messenger.current=null;mounted?.unmount();};\n },[session,attempt]);\n async function login(e: React.FormEvent<HTMLFormElement>) {\n e.preventDefault();\n setBusy(true);\n setError(\"\");\n const form = new FormData(e.currentTarget);\n try {\n const r = await fetch(\"/api/session\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(Object.fromEntries(form)),\n });\n const data = await r.json();\n if (!r.ok) throw Error(data.error?.message ?? \"Sign-in failed.\");\n setSession(data);\n } catch (e) {\n setError((e as Error).message);\n } finally {\n setBusy(false);\n }\n }\n async function logout() {\n const token = session?.token;\n try {\n await messenger.current?.signOut();\n } catch {\n setError(\"Signed out, but this browser could not erase saved drafts. Clear this site's browser data before sharing this computer.\");\n } finally {\n setSession(null);\n }\n if (token)\n await fetch(\"/api/logout\", {\n method: \"POST\",\n headers: {\n authorization: \"Bearer \" + token,\n \"content-type\": \"application/json\",\n },\n body: \"{}\",\n }).catch(() => {});\n }\n return (\n <main>\n <h1>Robono website messaging</h1>\n <p>\n This local example uses your test integration and your existing\n messaging design.\n </p>\n {!session ? (\n <form onSubmit={login}>\n <label>\n Example user\n <select name=\"user\">\n <option value=\"alice\">Alice</option>\n <option value=\"bob\">Bob</option>\n </select>\n </label>\n <label>\n Demo password\n <input\n name=\"password\"\n type=\"password\"\n required\n autoComplete=\"current-password\"\n />\n </label>\n <button disabled={busy}>Sign in</button>\n </form>\n ) : (\n <>\n <div className=\"toolbar\"><button onClick={()=>void logout()}>Sign out</button></div>\n <div ref={host} style={{height:'min(700px,75dvh)'}}/>\n </>\n )}\n {error && <p role=\"alert\">{error} {session&&<button onClick={()=>setAttempt(value=>value+1)}>Retry</button>}</p>}\n </main>\n );\n}\ncreateRoot(document.getElementById(\"root\")!).render(\n <React.StrictMode>\n <App />\n </React.StrictMode>,\n);\n","src/style.css":"body {\n margin: 0;\n background: #f4f5f2;\n color: #252b21;\n font:\n 16px system-ui,\n sans-serif;\n}\nmain {\n max-width: 960px;\n margin: 24px auto;\n padding: 0 16px;\n}\nh1 {\n font-size: 24px;\n}\nform,\n.toolbar {\n display: flex;\n gap: 12px;\n align-items: end;\n flex-wrap: wrap;\n margin: 16px 0;\n}\nform label,\n.toolbar label {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\nform input,\nform select,\n.toolbar select,\nform button,\n.toolbar button {\n font: inherit;\n padding: 10px;\n border: 1px solid #bfc6ba;\n border-radius: 8px;\n background: white;\n}\nbutton {\n cursor: pointer;\n}\nmain > section {\n border: 1px solid #cbd0c6;\n border-radius: 12px;\n}\np[role=\"alert\"] {\n color: #9b241f;\n}\n@media (max-width: 500px) {\n main {\n margin: 12px auto;\n padding: 0 8px;\n }\n h1 {\n font-size: 20px;\n }\n .toolbar {\n margin: 10px 0;\n }\n}\n",".env.example":"# Server only. Never prefix this key with VITE_. Use a TEST integration.\nROBONO_API_KEY=\n# Local demo sign-in only; replace with your own identity service in production.\nDEMO_PASSWORD=\nPORT=5188\n# Each independent example installation can choose its own fixture prefix.\nDEMO_USER_PREFIX=website-demo\n","README.md":"# Website messaging integration example\n\nThis example uses Robono's hosted Messenger. Its small local server signs Alice or Bob in and issues a short-lived user token. Messages, files, calls and Maps connect directly to Robono; they do not pass through that local server. The Robono API key stays on the server.\n\nRequirements: Node 22+, a Robono **test** integration with Messaging and Group messaging enabled. Choose the tools and appearance in **Features & preview**.\n\n```sh\ncd examples/messaging-web\nnpm install\ncp .env.example .env\n# Set ROBONO_API_KEY and a random DEMO_PASSWORD of at least 12 characters.\nnpm start\n```\n\nOpen the printed `http://127.0.0.1:5188` address in two browser profiles. Sign in as Alice and Bob with the demo password. Open the direct conversation or Website group and exchange messages. Selected photos and videos have removable thumbnails; sent images open in the supplied viewer. Interrupted sends remain available for recovery.\n\nThe default test users are `website-demo-alice` and `website-demo-bob`. Use a different `DEMO_USER_PREFIX` for independent demonstrations. The example creates these contacts and a group at revision 1. Do not reuse that prefix for real users or independently change these memberships. Sent messages and attachments remain subject to your test integration's storage, retention and usage settings after the example stops.\n\n## Use in your application\n\nThe automatic installer is the recommended starting point:\n\n```sh\nnpx --package @robono/server robono-messenger-install\n```\n\nKeep your existing sign-in. The generated `/api/robono-token` handler verifies that login and obtains a token for that verified user. Synchronize who may communicate, group membership and blocks from your own authoritative records. Pass your users' names and avatars to the supplied Messenger. No chat-forwarding server is required.\n\nThe password sign-in in this example is **local development only**. The example binds to localhost and refuses production mode. `npm run build` validates the frontend; it does not turn the example's authentication into a production login service. Never expose the demo server or put the private API key in browser files.\n\nFor notifications, register your HTTPS website origin and chat page in **Your app → Message notifications**. The installer puts the notification helper on your own website; browser rules require this small file on the same origin. Users enable alerts in the supplied Account screen. Website notifications do not require Firebase credentials. On iPhone, browser notifications require a Home Screen installation. Native iPhone/Android apps need their own Apple/Google notification credentials, as explained during setup.\n\nCall `await chat.signOut()` before ending your application's user session. This clears private saved work and revokes the Robono session. The supplied example's Sign out button follows that order.\n\nSee [the hosted setup guide](https://robono.com/guides/hosted-messenger.html) for supported frameworks, existing login connections and optional customization. Repository development can use `ROBONO_WORKSPACE_SDK=1 npm start` after building the SDK packages. Downloaded files always use the published packages.\n",".gitignore":"node_modules/\ndist/\n.env\n.env.*.local\n"}}
@@ -32,7 +32,7 @@ export interface MessengerInstallPlan {
32
32
  notes: string[];
33
33
  setupFile?: string;
34
34
  }
35
- export declare const messengerInstallerVersion: "0.26.0";
35
+ export declare const messengerInstallerVersion: "0.27.0";
36
36
  export declare const generatedHash: (content: string) => string;
37
37
  export declare function generatedKey(file: string, client: string, server: string): string;
38
38
  export declare function recordGeneratedFiles(files: MessengerInstallPlan['files'], client: string, server: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robono/server",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Provider-neutral server SDK for the Robono Bridge API.",
5
5
  "keywords": [
6
6
  "robono",
@@ -60,6 +60,7 @@
60
60
  "robono-messenger-init": "dist/messenger-init-cli.js",
61
61
  "robono-messenger-install": "dist/messenger-install-cli.js",
62
62
  "robono-history-import": "dist/history-import-cli.js",
63
+ "robono-history-migrate": "dist/history-migrate-cli.js",
63
64
  "robono-messenger-example": "dist/messenger-example-cli.js",
64
65
  "robono-history-export": "dist/history-export-cli.js"
65
66
  },