@robono/server 0.24.0 → 0.25.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/CHANGELOG.md +4 -0
- package/HISTORY-IMPORT.md +97 -0
- package/HOSTED.md +241 -0
- package/README.md +11 -3
- package/SBOM.spdx.json +9 -9
- package/dist/calling-session-handle.d.ts.map +1 -1
- package/dist/calling-session-handle.js +1 -0
- package/dist/calling-session-handle.js.map +1 -1
- package/dist/history-import-cli.d.ts +11 -0
- package/dist/history-import-cli.d.ts.map +1 -0
- package/dist/history-import-cli.js +83 -0
- package/dist/history-import-cli.js.map +1 -0
- package/dist/history-import.d.ts +64 -0
- package/dist/history-import.d.ts.map +1 -0
- package/dist/history-import.js +69 -0
- package/dist/history-import.js.map +1 -0
- package/dist/hosted-sessions.d.ts +38 -0
- package/dist/hosted-sessions.d.ts.map +1 -0
- package/dist/hosted-sessions.js +68 -0
- package/dist/hosted-sessions.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/managed-media.d.ts +2 -1
- package/dist/managed-media.d.ts.map +1 -1
- package/dist/managed-media.js +1 -1
- package/dist/managed-media.js.map +1 -1
- package/dist/messaging-notification-providers.d.ts.map +1 -1
- package/dist/messaging-notification-providers.js +1 -0
- package/dist/messaging-notification-providers.js.map +1 -1
- package/dist/messaging-push-worker.d.ts.map +1 -1
- package/dist/messaging-push-worker.js +3 -2
- package/dist/messaging-push-worker.js.map +1 -1
- package/dist/messenger-check.d.ts +1 -0
- package/dist/messenger-check.d.ts.map +1 -1
- package/dist/messenger-check.js +7 -5
- package/dist/messenger-check.js.map +1 -1
- package/dist/messenger-connected-templates.d.ts.map +1 -1
- package/dist/messenger-connected-templates.js +9 -6
- package/dist/messenger-connected-templates.js.map +1 -1
- package/dist/messenger-framework-templates.d.ts +3 -0
- package/dist/messenger-framework-templates.d.ts.map +1 -0
- package/dist/messenger-framework-templates.js +68 -0
- package/dist/messenger-framework-templates.js.map +1 -0
- package/dist/messenger-hosted-install.d.ts +3 -0
- package/dist/messenger-hosted-install.d.ts.map +1 -0
- package/dist/messenger-hosted-install.js +107 -0
- package/dist/messenger-hosted-install.js.map +1 -0
- package/dist/messenger-hosted-templates.d.ts +6 -0
- package/dist/messenger-hosted-templates.d.ts.map +1 -0
- package/dist/messenger-hosted-templates.js +84 -0
- package/dist/messenger-hosted-templates.js.map +1 -0
- package/dist/messenger-install-cli.js +10 -7
- package/dist/messenger-install-cli.js.map +1 -1
- package/dist/messenger-install.d.ts +23 -0
- package/dist/messenger-install.d.ts.map +1 -1
- package/dist/messenger-install.js +22 -8
- package/dist/messenger-install.js.map +1 -1
- package/dist/messenger-node.d.ts.map +1 -1
- package/dist/messenger-node.js +1 -0
- package/dist/messenger-node.js.map +1 -1
- package/dist/messenger-upgrade.js +1 -1
- package/dist/messenger-upgrade.js.map +1 -1
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## 0.25.0 — release candidate
|
|
2
|
+
|
|
3
|
+
Make Robono-hosted connections the default, add framework installation and sign-in examples, and import historical conversations with validation and retry-safe jobs. Customer-hosted forwarding remains optional.
|
|
4
|
+
|
|
1
5
|
## 0.24.0
|
|
2
6
|
|
|
3
7
|
Add explicit Robono-managed membership permissions to the supplied Messenger server and installer. Preserve existing custom authorization during upgrades. Register native incoming-call devices and deliver genuine Apple VoIP / Android data call invitations separately from message notifications.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Bring existing conversations to Robono
|
|
2
|
+
|
|
3
|
+
Use this server-only tool before opening the destination conversation to users.
|
|
4
|
+
It preserves original authors, dates and replies, and keeps attachment files private.
|
|
5
|
+
It does not send old push notifications, create unread badges, fabricate read
|
|
6
|
+
receipts, or charge old messages as new sends. Storage, transfer and any enabled
|
|
7
|
+
screening still follow your selected plan and spending limits.
|
|
8
|
+
|
|
9
|
+
## One conversation at a time
|
|
10
|
+
|
|
11
|
+
1. Create the authorized empty conversation using your existing Robono connection.
|
|
12
|
+
Full history must be selected. For direct chats, authors must be the two members.
|
|
13
|
+
For groups, the import can name former authors without granting them membership.
|
|
14
|
+
2. Export messages into the JSON format below. Keep the job UUID and original source
|
|
15
|
+
IDs unchanged for retries. Your existing provider's export must be mapped to
|
|
16
|
+
these fields; this command does not request your old provider's credentials.
|
|
17
|
+
3. Validate locally, then apply from your server:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npx --package @robono/server robono-history-import --file history.json
|
|
21
|
+
# Set ROBONO_API_KEY securely in this server's environment, then:
|
|
22
|
+
npx --package @robono/server robono-history-import --file history.json --apply
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Use a key with `messaging:manage`. Never put it in a website, phone app, exported
|
|
26
|
+
JSON, screenshot or source control. Validation alone does not contact Robono.
|
|
27
|
+
The command retries temporary connection failures using the same import identity.
|
|
28
|
+
A policy failure stays visible. Correct the cause and rerun the same command.
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"jobId": "66a3ee0d-18bb-49dc-bb06-a7adab745ef4",
|
|
33
|
+
"conversationId": "c829bd4f-1f67-4cd1-b285-6502923885b5",
|
|
34
|
+
"authors": ["alice", "bob"],
|
|
35
|
+
"messages": [
|
|
36
|
+
{"sourceId":"old-1","authorId":"alice","sentAt":"2026-01-01T12:00:00Z",
|
|
37
|
+
"content":{"kind":"text","text":"Meet at noon?"}},
|
|
38
|
+
{"sourceId":"old-2","authorId":"bob","sentAt":"2026-01-01T12:01:00Z",
|
|
39
|
+
"replyToSourceId":"old-1","content":{"kind":"text","text":"Yes."}}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Messages become visible together when the whole conversation passes validation.
|
|
45
|
+
Ordinary sends wait while the import is active. A retry after a lost final response
|
|
46
|
+
returns the existing result; it does not create another copy. Replies must point
|
|
47
|
+
to earlier messages in the same import. Original timestamps order the history;
|
|
48
|
+
file order resolves ties. Current permission, blocked-word/link, attachment and
|
|
49
|
+
screening rules apply again at the final commit.
|
|
50
|
+
|
|
51
|
+
## Private attachments
|
|
52
|
+
|
|
53
|
+
Text, photos, videos, voice messages and documents are supported. Public URLs and
|
|
54
|
+
another customer's asset IDs are not accepted. Use `RobonoHistoryImport` on your
|
|
55
|
+
server to start the job, upload each original file privately, and use the returned
|
|
56
|
+
`media` object in the message. The importer never sends your API key to file storage.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import {randomUUID} from 'node:crypto';
|
|
60
|
+
import {readFile} from 'node:fs/promises';
|
|
61
|
+
import {RobonoHistoryImport} from '@robono/server';
|
|
62
|
+
const importer = new RobonoHistoryImport({apiKey: process.env.ROBONO_API_KEY!});
|
|
63
|
+
// Persist these IDs and the export before starting; reuse them after interruption.
|
|
64
|
+
const jobId = savedJobId;
|
|
65
|
+
await importer.start({jobId, conversationId, authors: ['alice', 'bob'], count: 1});
|
|
66
|
+
const bytes = await readFile('./private-export/photo.jpg');
|
|
67
|
+
const media = await importer.upload({
|
|
68
|
+
jobId, conversationId, authorId: 'alice', uploadId: 'old-photo-1', kind: 'image',
|
|
69
|
+
file: {fileName: 'photo.jpg', mimeType: 'image/jpeg', byteSize: bytes.byteLength}, bytes,
|
|
70
|
+
});
|
|
71
|
+
await importer.importMessages({jobId, conversationId, authors: ['alice', 'bob'],
|
|
72
|
+
messages: [{sourceId: 'old-photo-1', authorId: 'alice', sentAt: originalTimestamp,
|
|
73
|
+
content: {kind: 'image', media}}],
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For video/voice, provide the original `durationMs`. Normal file verification and
|
|
78
|
+
screening can keep an upload pending; reuse the same upload ID when retrying.
|
|
79
|
+
Changing the file under the same ID is rejected. Imported file retention starts
|
|
80
|
+
when the imported message is committed and uses your selected storage retention.
|
|
81
|
+
|
|
82
|
+
## Limits and recovery
|
|
83
|
+
|
|
84
|
+
- Up to 10,000 messages and 32 MiB of staged message content per conversation;
|
|
85
|
+
three active imports per customer. Each HTTP append accepts up to 100 messages;
|
|
86
|
+
the SDK uses smaller batches. Existing nonempty conversations cannot be merged.
|
|
87
|
+
- A job expires after 24 hours. Call `status(jobId)` to check it or `cancel(jobId)`
|
|
88
|
+
to stop an unfinished import. An expired/cancelled job requires a new job UUID.
|
|
89
|
+
- `cancel` removes staging copies, not already committed conversations. Unused
|
|
90
|
+
uploads follow ordinary cleanup. Completed staging copies are removed within
|
|
91
|
+
seven days; the content-free job result remains for safe retries.
|
|
92
|
+
- User erasure cancels affected unfinished imports and removes their staging
|
|
93
|
+
copies, so an old import cannot restore erased content.
|
|
94
|
+
- Poll votes, reactions, delivered/read status and provider-specific custom
|
|
95
|
+
objects are not imported by this format. Keep the original export if needed.
|
|
96
|
+
|
|
97
|
+
This is an import into Robono-managed messaging, not a live cross-provider bridge.
|
package/HOSTED.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# Robono-hosted Messenger
|
|
2
|
+
|
|
3
|
+
Robono runs messaging, calling, private attachment storage and location services.
|
|
4
|
+
Your existing server verifies your existing users. It issues a short-lived Robono
|
|
5
|
+
session and tells Robono who is allowed to communicate. Chat traffic does not
|
|
6
|
+
pass through your server. Users do not create a second account.
|
|
7
|
+
|
|
8
|
+
## 1. Install the interface
|
|
9
|
+
|
|
10
|
+
From the website or Expo project:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npx --package @robono/server@^0.25.0 robono-messenger-install --project . --origin https://your-company.com --company 'Your company'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Review the files, then accept installation. The default is Robono hosting.
|
|
17
|
+
Next.js receives a small sign-in route. React/Vite, Vue, Svelte, Angular and Expo
|
|
18
|
+
connect to your existing sign-in server; they do not need a Node messaging server.
|
|
19
|
+
Use `--token-endpoint https://your-server.com/api/robono-token` if that route is on
|
|
20
|
+
another server. That server must explicitly allow your website origin for this
|
|
21
|
+
bearer-authenticated request; never use a wildcard with credentials.
|
|
22
|
+
|
|
23
|
+
For Expo, rebuild both phone apps after installing the native dependencies. A
|
|
24
|
+
static app.json gets the grouped plugin automatically. For a config written in
|
|
25
|
+
JavaScript/TypeScript, add `@robono/messaging-react-native/setup` to `plugins`.
|
|
26
|
+
The installer preserves existing screens, configuration code and environment files.
|
|
27
|
+
|
|
28
|
+
The generated `.robono/SETUP.md` explains exactly which component to import.
|
|
29
|
+
React/Expo use `CustomerMessenger` with your existing Supabase/Firebase instance
|
|
30
|
+
or the small generated account interface. Other website frameworks receive a
|
|
31
|
+
thin component around the same supplied Messenger. Give the component a height
|
|
32
|
+
and keep it mounted while signed in. Supply names, avatars and phone numbers
|
|
33
|
+
from your own contact directory; Robono does not invent your employee/customer
|
|
34
|
+
records or make every user visible to every other user.
|
|
35
|
+
|
|
36
|
+
## 2. Verify the existing login
|
|
37
|
+
|
|
38
|
+
Keep `ROBONO_API_KEY` on your existing server. Never put it in a phone app,
|
|
39
|
+
JavaScript served to the browser, a URL, or a public environment variable.
|
|
40
|
+
|
|
41
|
+
The client sends its existing bearer login to your `/api/robono-token` route.
|
|
42
|
+
The route verifies that login using your existing authentication provider. Use
|
|
43
|
+
the verified account ID, never a user ID supplied in the request body.
|
|
44
|
+
|
|
45
|
+
### JavaScript / TypeScript
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import {createRobonoTokenHandler} from '@robono/server';
|
|
49
|
+
import {verifyExistingLogin} from './your-existing-login';
|
|
50
|
+
|
|
51
|
+
export const POST = createRobonoTokenHandler({
|
|
52
|
+
apiKey: process.env.ROBONO_API_KEY!,
|
|
53
|
+
origins: ['https://your-company.com'],
|
|
54
|
+
authenticate: async request => {
|
|
55
|
+
const account = await verifyExistingLogin(request);
|
|
56
|
+
return account?.id ?? null;
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
export const OPTIONS = POST;
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Supabase and Firebase verification are generated when selected in the installer.
|
|
63
|
+
For another login system, `verifyExistingLogin` is the one customer-owned
|
|
64
|
+
connection. It must check expiry, revocation and disabled accounts. Decoding a
|
|
65
|
+
JWT without verifying its signature is not authentication.
|
|
66
|
+
|
|
67
|
+
### Any other server language
|
|
68
|
+
|
|
69
|
+
The same route can make one ordinary HTTPS request. No Node service is required:
|
|
70
|
+
|
|
71
|
+
```http
|
|
72
|
+
POST https://api.robono.com/v1/messenger/sessions
|
|
73
|
+
Authorization: Bearer YOUR_PRIVATE_ROBONO_KEY
|
|
74
|
+
Content-Type: application/json
|
|
75
|
+
|
|
76
|
+
{"operation":"create","userId":"VERIFIED_ACCOUNT_ID","origins":["https://your-company.com"],"expiresInSeconds":900}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Return the response only to that signed-in user with `Cache-Control: no-store`.
|
|
80
|
+
Reject unknown browser origins. Native clients may omit Origin but must still
|
|
81
|
+
present their existing login. Do not log the returned token. Requests use HTTPS,
|
|
82
|
+
a timeout and no redirects. Set CORS on a separate sign-in domain to your exact
|
|
83
|
+
website origin and allow `Authorization` and `Content-Type`; no cookies are
|
|
84
|
+
needed by the supplied bearer-token connection.
|
|
85
|
+
|
|
86
|
+
This Python helper is called **after your framework has authenticated the user**:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import json, os, urllib.request
|
|
90
|
+
|
|
91
|
+
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
92
|
+
def redirect_request(self, request, fp, code, message, headers, new_url):
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
def issue_robono_session(verified_user_id):
|
|
96
|
+
request = urllib.request.Request(
|
|
97
|
+
'https://api.robono.com/v1/messenger/sessions',
|
|
98
|
+
data=json.dumps({'operation': 'create', 'userId': verified_user_id,
|
|
99
|
+
'origins': ['https://your-company.com']}).encode(),
|
|
100
|
+
headers={'Authorization': 'Bearer ' + os.environ['ROBONO_API_KEY'],
|
|
101
|
+
'Content-Type': 'application/json'}, method='POST')
|
|
102
|
+
with urllib.request.build_opener(NoRedirect()).open(request, timeout=15) as response:
|
|
103
|
+
return json.load(response)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
PHP, inside an already authenticated controller:
|
|
107
|
+
|
|
108
|
+
```php
|
|
109
|
+
function issueRobonoSession(string $verifiedUserId): array {
|
|
110
|
+
$request = curl_init('https://api.robono.com/v1/messenger/sessions');
|
|
111
|
+
curl_setopt_array($request, [
|
|
112
|
+
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
|
|
113
|
+
CURLOPT_FOLLOWLOCATION => false, CURLOPT_TIMEOUT => 15,
|
|
114
|
+
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('ROBONO_API_KEY'),
|
|
115
|
+
'Content-Type: application/json'],
|
|
116
|
+
CURLOPT_POSTFIELDS => json_encode(['operation' => 'create',
|
|
117
|
+
'userId' => $verifiedUserId, 'origins' => ['https://your-company.com']], JSON_THROW_ON_ERROR),
|
|
118
|
+
]);
|
|
119
|
+
$body = curl_exec($request);
|
|
120
|
+
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
|
|
121
|
+
curl_close($request);
|
|
122
|
+
if ($body === false || $status !== 201) throw new RuntimeException('Messaging sign-in unavailable');
|
|
123
|
+
return json_decode($body, true, 512, JSON_THROW_ON_ERROR);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
.NET, inside an endpoint protected by your existing authentication middleware:
|
|
128
|
+
|
|
129
|
+
```csharp
|
|
130
|
+
using System.Net.Http.Headers;
|
|
131
|
+
using System.Net.Http.Json;
|
|
132
|
+
using System.Text.Json;
|
|
133
|
+
|
|
134
|
+
static async Task<JsonElement> IssueRobonoSession(string verifiedUserId, string key) {
|
|
135
|
+
using var client = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false }) {
|
|
136
|
+
Timeout = TimeSpan.FromSeconds(15)
|
|
137
|
+
};
|
|
138
|
+
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key);
|
|
139
|
+
using var response = await client.PostAsJsonAsync(
|
|
140
|
+
"https://api.robono.com/v1/messenger/sessions",
|
|
141
|
+
new { operation = "create", userId = verifiedUserId,
|
|
142
|
+
origins = new[] { "https://your-company.com" } });
|
|
143
|
+
response.EnsureSuccessStatusCode();
|
|
144
|
+
return await response.Content.ReadFromJsonAsync<JsonElement>();
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The examples deliberately receive `verifiedUserId`; it must come from your
|
|
149
|
+
framework's authenticated account, never form data or an unsigned header.
|
|
150
|
+
Handle provider errors with a generic failure response, not a private token or
|
|
151
|
+
provider response dump. Your regular login rate limit also applies to this route.
|
|
152
|
+
|
|
153
|
+
## 3. Synchronize your permissions
|
|
154
|
+
|
|
155
|
+
For contacts, call the server-only `RobonoMessengerConnections.synchronizeContact`
|
|
156
|
+
with the two authoritative user IDs, `allowed` and a monotonically increasing
|
|
157
|
+
revision. It updates texting and calling together. Group membership uses
|
|
158
|
+
`RobonoMessagingBackend.setGroup`. Location visibility uses
|
|
159
|
+
`RobonoLocationBackend.setGrant` and the recipient's sharing approval. Send
|
|
160
|
+
changes when your own records change; users cannot grant themselves membership.
|
|
161
|
+
An empty contact list is valid and means you have not connected anyone yet.
|
|
162
|
+
|
|
163
|
+
For suspension/deletion, synchronize the account's service permissions and call
|
|
164
|
+
`RobonoSessions.revokeUser(userId)` with the same server key that minted sessions.
|
|
165
|
+
Key revocation also invalidates sessions minted by that key. Sessions never
|
|
166
|
+
extend the original key's scope. A session lasts 15 minutes by default and may
|
|
167
|
+
be configured from one minute to one hour. The supplied client renews it through
|
|
168
|
+
your verified login; it never stores the token on disk.
|
|
169
|
+
|
|
170
|
+
## Notifications, Maps and calling
|
|
171
|
+
|
|
172
|
+
- Website: publish the included `public/robono-notifications/worker.js`. Enter
|
|
173
|
+
your website origin and chat-page path in Robono's notification setup. The
|
|
174
|
+
worker has its own scope and does not replace your existing website worker.
|
|
175
|
+
A user must allow notifications. iPhone websites must be installed on the
|
|
176
|
+
Home Screen and opened there to receive web notifications.
|
|
177
|
+
- iPhone: connect your Apple Developer Team ID, APNs Key ID, private `.p8` key
|
|
178
|
+
and Bundle ID privately in Robono. Enable push notifications, including the
|
|
179
|
+
incoming-call `.voip` topic. Use development credentials/environment for a
|
|
180
|
+
development build and production for TestFlight/App Store. Do not put the
|
|
181
|
+
`.p8` file in the app.
|
|
182
|
+
- Android: connect the Firebase project, app `google-services.json`, and private
|
|
183
|
+
FCM service account in the documented setup. Only app configuration goes in
|
|
184
|
+
the app; the service account stays at Robono. Full-screen call alerts depend
|
|
185
|
+
on Android permissions and Play eligibility; ordinary Answer/Decline alerts
|
|
186
|
+
are the fallback.
|
|
187
|
+
- Maps: register the website origin or both phone app identifiers in Robono's
|
|
188
|
+
Maps setup. Supply your own authoritative people and sharing grants. The
|
|
189
|
+
supplied interface manages location permission, sharing start/stop and expiry.
|
|
190
|
+
- Video: camera and microphone permissions are required. Apple screen sharing
|
|
191
|
+
additionally needs the Broadcast Upload extension configured by the calling
|
|
192
|
+
plugin; it cannot be added to an already-built app without rebuilding it.
|
|
193
|
+
|
|
194
|
+
These platform credentials belong to the customer's apps and cannot be replaced
|
|
195
|
+
with Robono Messenger's credentials. AWS screening and Robono service credentials
|
|
196
|
+
are managed by Robono; customers do not need their own AWS account.
|
|
197
|
+
|
|
198
|
+
## Offline behavior and sign-out
|
|
199
|
+
|
|
200
|
+
With the supplied persistent storage, interrupted outgoing messages remain on
|
|
201
|
+
that device and retry for up to 24 hours while the app/site is running. This is
|
|
202
|
+
not a promise that a force-closed app runs in the background. On reconnection,
|
|
203
|
+
Robono checks current permissions before sending. An attachment keeps its place
|
|
204
|
+
in the queue; later text does not overtake it. Stable IDs prevent a lost server
|
|
205
|
+
acknowledgement from creating a second message. Policy failures and expired
|
|
206
|
+
queue items require user action. Users can remove waiting messages.
|
|
207
|
+
|
|
208
|
+
Offline reading/composition requires a still-valid server-issued offline-history
|
|
209
|
+
window. If that window expires, reconnect before displaying private history.
|
|
210
|
+
Sign-out clears the local account state, stops sharing, unregisters notifications
|
|
211
|
+
and revokes the connection's Robono sessions. Notify Robono when your own account
|
|
212
|
+
is suspended; a cached screen is never a new authorization grant.
|
|
213
|
+
|
|
214
|
+
## Existing installations
|
|
215
|
+
|
|
216
|
+
`--upgrade` preserves customer-edited files and saves proposed changes for review.
|
|
217
|
+
Existing self-hosted installations retain their mode. Moving an existing app to
|
|
218
|
+
hosted mode is a deliberate migration: use the hosted connection and token route,
|
|
219
|
+
verify your existing permission synchronization, then retire the old forwarding
|
|
220
|
+
route. Never leave a private API key in the client as a shortcut. `--self-hosted`
|
|
221
|
+
remains available for customers deliberately maintaining their own adapter.
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
## Interface language and recovery
|
|
225
|
+
|
|
226
|
+
Set `interfaceLocale={{locale:'es'}}` on the supplied phone Messenger, or
|
|
227
|
+
`interfaceLocale:{locale:'es'}` when mounting the website Messenger. English is the
|
|
228
|
+
default. This also covers the supplied calling controls and Maps screens. It does
|
|
229
|
+
not change the language of anyone's messages or their translation preferences.
|
|
230
|
+
For custom wording or another language, see the interface language guide.
|
|
231
|
+
|
|
232
|
+
The supplied interface keeps unsent messages and waiting edits/reactions/deletions
|
|
233
|
+
on the device, scoped to the account and customer. It retries connection failures
|
|
234
|
+
in order, after checking current access. Policy failures stay visible for review;
|
|
235
|
+
users can retry or discard them. Automatic retries expire after 24 hours. Sign-out
|
|
236
|
+
clears this account's saved work. Offline storage is device-local, not a server
|
|
237
|
+
backup. A custom outbox can implement the optional action methods to use this flow.
|
|
238
|
+
|
|
239
|
+
To migrate an existing conversation, use `robono-history-import`; see
|
|
240
|
+
[History import](./HISTORY-IMPORT.md). Do this before allowing new sends in that
|
|
241
|
+
conversation. Existing user and membership records remain your responsibility.
|
package/README.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# `@robono/server`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Server SDK and project installer for Robono Messenger, Maps, calling and Bridge.
|
|
4
|
+
|
|
5
|
+
## Start with the hosted connection
|
|
6
|
+
|
|
7
|
+
Robono runs the messaging, media, calling and Maps services. Your server verifies your existing user login and returns a short-lived Robono session; it does not forward chat requests. Your company continues to supply its users, permitted contacts and group memberships.
|
|
8
|
+
|
|
9
|
+
Run `npx --package @robono/server robono-messenger-install` in your application project. The installer detects supported projects, creates the connection files and explains the remaining login and platform credentials. See [the hosted setup guide](https://robono.com/guides/hosted-messenger.html) for the recommended setup, supported frameworks and non-JavaScript server examples. The older self-hosted adapters below remain available with `--self-hosted`.
|
|
10
|
+
|
|
11
|
+
Never put your Robono API key or Apple/Google private keys in browser or phone code.
|
|
4
12
|
|
|
5
13
|
## License
|
|
6
14
|
|
|
@@ -11,10 +19,10 @@ Robono's own app and third-party connected apps appear as endpoints in the same
|
|
|
11
19
|
## Install
|
|
12
20
|
|
|
13
21
|
```bash
|
|
14
|
-
npm install @robono/server
|
|
22
|
+
npm install @robono/server
|
|
15
23
|
```
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
Use the current package for the hosted setup; older releases retain their original adapter API.
|
|
18
26
|
|
|
19
27
|
The package is ESM-only, supports Node.js 18+ and Deno 2.x through npm compatibility, and includes TypeScript declarations and `SBOM.spdx.json`.
|
|
20
28
|
|
package/SBOM.spdx.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"spdxVersion": "SPDX-2.3",
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
|
-
"name": "@robono/server@0.
|
|
6
|
-
"documentNamespace": "http://spdx.org/spdxdocs/@robono%2fserver-0.
|
|
5
|
+
"name": "@robono/server@0.25.0",
|
|
6
|
+
"documentNamespace": "http://spdx.org/spdxdocs/@robono%2fserver-0.25.0-***",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-11-
|
|
8
|
+
"created": "2026-11-04T19:16:12.352Z",
|
|
9
9
|
"creators": [
|
|
10
10
|
"Tool: npm/cli-11.6.2"
|
|
11
11
|
]
|
|
12
12
|
},
|
|
13
13
|
"documentDescribes": [
|
|
14
|
-
"SPDXRef-Package-robono.server-0.
|
|
14
|
+
"SPDXRef-Package-robono.server-0.25.0"
|
|
15
15
|
],
|
|
16
16
|
"packages": [
|
|
17
17
|
{
|
|
18
18
|
"name": "@robono/server",
|
|
19
|
-
"SPDXID": "SPDXRef-Package-robono.server-0.
|
|
20
|
-
"versionInfo": "0.
|
|
19
|
+
"SPDXID": "SPDXRef-Package-robono.server-0.25.0",
|
|
20
|
+
"versionInfo": "0.25.0",
|
|
21
21
|
"packageFileName": "",
|
|
22
22
|
"description": "Provider-neutral server SDK for the Robono Bridge API.",
|
|
23
23
|
"primaryPackagePurpose": "LIBRARY",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
{
|
|
30
30
|
"referenceCategory": "PACKAGE-MANAGER",
|
|
31
31
|
"referenceType": "purl",
|
|
32
|
-
"referenceLocator": "pkg:npm/%40robono/server@0.
|
|
32
|
+
"referenceLocator": "pkg:npm/%40robono/server@0.25.0"
|
|
33
33
|
}
|
|
34
34
|
]
|
|
35
35
|
},
|
|
@@ -175,12 +175,12 @@
|
|
|
175
175
|
"relationships": [
|
|
176
176
|
{
|
|
177
177
|
"spdxElementId": "SPDXRef-DOCUMENT",
|
|
178
|
-
"relatedSpdxElement": "SPDXRef-Package-robono.server-0.
|
|
178
|
+
"relatedSpdxElement": "SPDXRef-Package-robono.server-0.25.0",
|
|
179
179
|
"relationshipType": "DESCRIBES"
|
|
180
180
|
},
|
|
181
181
|
{
|
|
182
182
|
"spdxElementId": "SPDXRef-Package-cross-spawn-7.0.6",
|
|
183
|
-
"relatedSpdxElement": "SPDXRef-Package-robono.server-0.
|
|
183
|
+
"relatedSpdxElement": "SPDXRef-Package-robono.server-0.25.0",
|
|
184
184
|
"relationshipType": "DEPENDENCY_OF"
|
|
185
185
|
},
|
|
186
186
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calling-session-handle.d.ts","sourceRoot":"","sources":["../src/calling-session-handle.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"calling-session-handle.d.ts","sourceRoot":"","sources":["../src/calling-session-handle.ts"],"names":[],"mappings":"AAEA;iFACiF;AACjF,wBAAgB,2BAA2B,CAAC,SAAS,EAAC,MAAM,EAAC,UAAU,EAAC,MAAM;kBAK/D,MAAM,WAAS;QAAC,KAAK,EAAC,MAAM,CAAC;QAAA,SAAS,EAAC,MAAM,CAAA;KAAC;iBAK/C,OAAO,WAAS,MAAM,GAAE;QAAC,KAAK,EAAC,MAAM,CAAC;QAAA,SAAS,EAAC,MAAM,CAAA;KAAC;EAWpE"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
1
2
|
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
|
|
2
3
|
/** Full call credentials stay encrypted between requests to the customer's route.
|
|
3
4
|
* The browser receives a separate credential that can only hear change hints. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calling-session-handle.js","sourceRoot":"","sources":["../src/calling-session-handle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,cAAc,EAAC,gBAAgB,EAAC,UAAU,EAAC,WAAW,EAAC,MAAM,aAAa,CAAC;AACnF;iFACiF;AACjF,MAAM,UAAU,2BAA2B,CAAC,SAAgB,EAAC,UAAiB;IAC7E,IAAG,CAAC,SAAS,CAAC,IAAI,EAAE;QAAC,MAAM,KAAK,CAAC,+BAA+B,CAAC,CAAC;IAClE,MAAM,GAAG,GAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,+BAA+B,GAAC,SAAS,CAAC,CAAC,MAAM,EAAE,CAAC;IAC1F,MAAM,GAAG,GAAC,MAAM,CAAC,IAAI,CAAC,+BAA+B,GAAC,UAAU,CAAC,CAAC;IAClE,OAAO;QACN,IAAI,CAAC,OAAc,EAAC,OAAuC;YAC1D,MAAM,EAAE,GAAC,WAAW,CAAC,EAAE,CAAC,EAAC,MAAM,GAAC,cAAc,CAAC,aAAa,EAAC,GAAG,EAAC,EAAE,CAAC,CAAC;YAAA,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACxF,MAAM,IAAI,GAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAC,OAAO,EAAC,KAAK,EAAC,OAAO,CAAC,KAAK,EAAC,SAAS,EAAC,OAAO,CAAC,SAAS,EAAC,CAAC,EAAC,MAAM,CAAC,EAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YAC3I,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,EAAC,MAAM,CAAC,UAAU,EAAE,EAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QAC3E,CAAC;QACD,IAAI,CAAC,MAAc,EAAC,OAAc;YACjC,IAAG,CAAC;gBACH,IAAG,OAAO,MAAM,KAAG,QAAQ,IAAE,MAAM,CAAC,MAAM,GAAC,IAAI,IAAE,CAAC,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC;oBAAC,MAAM,KAAK,EAAE,CAAC;gBAChG,MAAM,KAAK,GAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAC,WAAW,CAAC,CAAC;gBAAA,IAAG,KAAK,CAAC,MAAM,GAAC,EAAE;oBAAC,MAAM,KAAK,EAAE,CAAC;gBAC7E,MAAM,QAAQ,GAAC,gBAAgB,CAAC,aAAa,EAAC,GAAG,EAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAC,EAAE,CAAC,CAAC,CAAC;gBAAA,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAAA,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAC,EAAE,CAAC,CAAC,CAAC;gBACxI,MAAM,KAAK,GAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;gBAC/G,IAAG,KAAK,CAAC,OAAO,KAAG,OAAO,IAAE,OAAO,KAAK,CAAC,KAAK,KAAG,QAAQ,IAAE,CAAC,KAAK,CAAC,KAAK,IAAE,KAAK,CAAC,KAAK,CAAC,MAAM,GAAC,GAAG,IAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,IAAE,KAAK,CAAC,SAAS,IAAE,IAAI,CAAC,GAAG,EAAE;oBAAC,MAAM,KAAK,EAAE,CAAC;gBAC9K,OAAO,EAAC,KAAK,EAAC,KAAK,CAAC,KAAK,EAAC,SAAS,EAAC,KAAK,CAAC,SAAS,EAAC,CAAC;YACtD,CAAC;YAAA,MAAK,CAAC;gBAAA,MAAM,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,CAAC,EAAC,EAAC,MAAM,EAAC,GAAG,EAAC,CAAC,CAAC;YAAA,CAAC;QAC5E,CAAC;KACD,CAAC;AACH,CAAC"}
|
|
1
|
+
{"version":3,"file":"calling-session-handle.js","sourceRoot":"","sources":["../src/calling-session-handle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AACnC,OAAO,EAAC,cAAc,EAAC,gBAAgB,EAAC,UAAU,EAAC,WAAW,EAAC,MAAM,aAAa,CAAC;AACnF;iFACiF;AACjF,MAAM,UAAU,2BAA2B,CAAC,SAAgB,EAAC,UAAiB;IAC7E,IAAG,CAAC,SAAS,CAAC,IAAI,EAAE;QAAC,MAAM,KAAK,CAAC,+BAA+B,CAAC,CAAC;IAClE,MAAM,GAAG,GAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,+BAA+B,GAAC,SAAS,CAAC,CAAC,MAAM,EAAE,CAAC;IAC1F,MAAM,GAAG,GAAC,MAAM,CAAC,IAAI,CAAC,+BAA+B,GAAC,UAAU,CAAC,CAAC;IAClE,OAAO;QACN,IAAI,CAAC,OAAc,EAAC,OAAuC;YAC1D,MAAM,EAAE,GAAC,WAAW,CAAC,EAAE,CAAC,EAAC,MAAM,GAAC,cAAc,CAAC,aAAa,EAAC,GAAG,EAAC,EAAE,CAAC,CAAC;YAAA,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACxF,MAAM,IAAI,GAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAC,OAAO,EAAC,KAAK,EAAC,OAAO,CAAC,KAAK,EAAC,SAAS,EAAC,OAAO,CAAC,SAAS,EAAC,CAAC,EAAC,MAAM,CAAC,EAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YAC3I,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,EAAC,MAAM,CAAC,UAAU,EAAE,EAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QAC3E,CAAC;QACD,IAAI,CAAC,MAAc,EAAC,OAAc;YACjC,IAAG,CAAC;gBACH,IAAG,OAAO,MAAM,KAAG,QAAQ,IAAE,MAAM,CAAC,MAAM,GAAC,IAAI,IAAE,CAAC,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC;oBAAC,MAAM,KAAK,EAAE,CAAC;gBAChG,MAAM,KAAK,GAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAC,WAAW,CAAC,CAAC;gBAAA,IAAG,KAAK,CAAC,MAAM,GAAC,EAAE;oBAAC,MAAM,KAAK,EAAE,CAAC;gBAC7E,MAAM,QAAQ,GAAC,gBAAgB,CAAC,aAAa,EAAC,GAAG,EAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAC,EAAE,CAAC,CAAC,CAAC;gBAAA,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAAA,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAC,EAAE,CAAC,CAAC,CAAC;gBACxI,MAAM,KAAK,GAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;gBAC/G,IAAG,KAAK,CAAC,OAAO,KAAG,OAAO,IAAE,OAAO,KAAK,CAAC,KAAK,KAAG,QAAQ,IAAE,CAAC,KAAK,CAAC,KAAK,IAAE,KAAK,CAAC,KAAK,CAAC,MAAM,GAAC,GAAG,IAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,IAAE,KAAK,CAAC,SAAS,IAAE,IAAI,CAAC,GAAG,EAAE;oBAAC,MAAM,KAAK,EAAE,CAAC;gBAC9K,OAAO,EAAC,KAAK,EAAC,KAAK,CAAC,KAAK,EAAC,SAAS,EAAC,KAAK,CAAC,SAAS,EAAC,CAAC;YACtD,CAAC;YAAA,MAAK,CAAC;gBAAA,MAAM,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,CAAC,EAAC,EAAC,MAAM,EAAC,GAAG,EAAC,CAAC,CAAC;YAAA,CAAC;QAC5E,CAAC;KACD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { type HistoryImportMessage } from './history-import.js';
|
|
3
|
+
/** Private JSON export: never print message bodies or keys. Validation does not contact Robono. */
|
|
4
|
+
export declare function runHistoryImport(args: string[], env?: NodeJS.ProcessEnv, log?: (line: string) => void): Promise<void>;
|
|
5
|
+
export declare function validateHistoryImportFile(value: any): asserts value is {
|
|
6
|
+
jobId: string;
|
|
7
|
+
conversationId: string;
|
|
8
|
+
authors: string[];
|
|
9
|
+
messages: HistoryImportMessage[];
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=history-import-cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history-import-cli.d.ts","sourceRoot":"","sources":["../src/history-import-cli.ts"],"names":[],"mappings":";AAGA,OAAO,EAAqB,KAAK,oBAAoB,EAAC,MAAM,qBAAqB,CAAC;AAElF,mGAAmG;AACnG,wBAAsB,gBAAgB,CAAC,IAAI,EAAC,MAAM,EAAE,EAAC,GAAG,GAAC,MAAM,CAAC,UAAsB,EAAC,GAAG,GAAC,CAAC,IAAI,EAAC,MAAM,KAAG,IAAgB,iBAkBzH;AACD,wBAAgB,yBAAyB,CAAC,KAAK,EAAC,GAAG,GAAE,OAAO,CAAC,KAAK,IAAI;IAAC,KAAK,EAAC,MAAM,CAAC;IAAA,cAAc,EAAC,MAAM,CAAC;IAAA,OAAO,EAAC,MAAM,EAAE,CAAC;IAAA,QAAQ,EAAC,oBAAoB,EAAE,CAAA;CAAC,CAiB1J"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { RobonoHistoryImport } from './history-import.js';
|
|
5
|
+
import { RobonoError } from './errors.js';
|
|
6
|
+
/** Private JSON export: never print message bodies or keys. Validation does not contact Robono. */
|
|
7
|
+
export async function runHistoryImport(args, env = process.env, log = console.log) {
|
|
8
|
+
const flags = new Set(args.filter(a => a === '--apply' || a === '--help'));
|
|
9
|
+
if (flags.has('--help')) {
|
|
10
|
+
log('robono-history-import --file history.json [--apply]\nWithout --apply, validates locally. To import, set ROBONO_API_KEY on your server. Keep jobId and sourceId unchanged when retrying.');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const rest = args.filter(a => !flags.has(a));
|
|
14
|
+
if (rest.length !== 2 || rest[0] !== '--file' || !rest[1])
|
|
15
|
+
throw Error('Use --file history.json, with --apply when ready.');
|
|
16
|
+
const info = await stat(rest[1]);
|
|
17
|
+
if (!info.isFile() || info.size > 33554432)
|
|
18
|
+
throw Error('Use a JSON file no larger than 32 MiB.');
|
|
19
|
+
let input;
|
|
20
|
+
try {
|
|
21
|
+
input = JSON.parse(await readFile(rest[1], 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw Error('The import file must contain valid JSON.');
|
|
25
|
+
}
|
|
26
|
+
validateHistoryImportFile(input);
|
|
27
|
+
log(`Validated ${input.messages.length} messages from ${input.authors.length} authors.`);
|
|
28
|
+
if (!flags.has('--apply')) {
|
|
29
|
+
log('Nothing sent. Add --apply to import into the authorized empty conversation.');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const apiKey = env.ROBONO_API_KEY;
|
|
33
|
+
if (!apiKey)
|
|
34
|
+
throw Error('Set ROBONO_API_KEY in your server environment. Do not put it in the JSON file.');
|
|
35
|
+
const importer = new RobonoHistoryImport({ apiKey });
|
|
36
|
+
for (let attempt = 0;; attempt++) {
|
|
37
|
+
try {
|
|
38
|
+
const result = await importer.importMessages({ ...input, onProgress: (done, total) => log(`Prepared ${done}/${total}.`) });
|
|
39
|
+
log(`Import ${result.state}: ${result.importedCount ?? input.messages.length} messages.`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (attempt >= 4 || !(error instanceof TypeError || error instanceof RobonoError && error.retryable))
|
|
44
|
+
throw error;
|
|
45
|
+
log('Connection interrupted. Retrying the same import safely.');
|
|
46
|
+
await new Promise(r => setTimeout(r, Math.min(15000, 2000 * 2 ** attempt)));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function validateHistoryImportFile(value) {
|
|
51
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
52
|
+
const id = (v) => typeof v === 'string' && v.length > 0 && v.length <= 200 && v === v.trim() && !/[\u0000-\u001f\u007f]/.test(v);
|
|
53
|
+
if (!value || !uuid.test(value.jobId) || !uuid.test(value.conversationId) || !Array.isArray(value.authors) || !value.authors.length || value.authors.length > 10000 || value.authors.some((a) => !id(a)) || new Set(value.authors).size !== value.authors.length)
|
|
54
|
+
throw Error('Provide UUID jobId and conversationId and a unique authors list.');
|
|
55
|
+
if (!Array.isArray(value.messages) || !value.messages.length || value.messages.length > 10000)
|
|
56
|
+
throw Error('Include 1–10,000 messages.');
|
|
57
|
+
const ids = new Map(), order = new Map();
|
|
58
|
+
for (const [index, message] of value.messages.entries()) {
|
|
59
|
+
const fail = (reason) => { throw Error(`Message ${index + 1}: ${reason}`); };
|
|
60
|
+
if (!message || !id(message.sourceId) || ids.has(message.sourceId) || !value.authors.includes(message.authorId))
|
|
61
|
+
fail('use a unique sourceId and an author from the authors list.');
|
|
62
|
+
const sent = Date.parse(message.sentAt);
|
|
63
|
+
if (typeof message.sentAt !== 'string' || !Number.isFinite(sent) || sent < 0 || sent > Date.now())
|
|
64
|
+
fail('sentAt must be a valid past date.');
|
|
65
|
+
const c = message.content;
|
|
66
|
+
if (!c || !['text', 'image', 'video', 'voice', 'document'].includes(c.kind) || Object.keys(c).some(k => !['kind', 'text', 'media'].includes(k)))
|
|
67
|
+
fail('provide supported content only.');
|
|
68
|
+
if (c.kind === 'text' && (typeof c.text !== 'string' || !c.text.trim() || c.text.length > 10000 || c.media))
|
|
69
|
+
fail('text must contain 1–10,000 characters.');
|
|
70
|
+
if (c.kind !== 'text' && (!c.media || typeof c.media.assetId !== 'string' || !c.media.assetId.startsWith('robono-r2:')))
|
|
71
|
+
fail('upload attachments privately through importer.upload first. Public source URLs are not accepted.');
|
|
72
|
+
if (message.replyToSourceId !== undefined && !id(message.replyToSourceId))
|
|
73
|
+
fail('replyToSourceId must be an original source ID.');
|
|
74
|
+
ids.set(message.sourceId, sent);
|
|
75
|
+
order.set(message.sourceId, index);
|
|
76
|
+
}
|
|
77
|
+
for (const [index, message] of value.messages.entries())
|
|
78
|
+
if (message.replyToSourceId !== undefined && (!ids.has(message.replyToSourceId) || ids.get(message.replyToSourceId) > Date.parse(message.sentAt) || message.replyToSourceId === message.sourceId || ids.get(message.replyToSourceId) === Date.parse(message.sentAt) && order.get(message.replyToSourceId) >= index))
|
|
79
|
+
throw Error(`Message ${index + 1}: the original reply target must exist and precede this message.`);
|
|
80
|
+
}
|
|
81
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
82
|
+
runHistoryImport(process.argv.slice(2)).catch(error => { console.error(error instanceof Error ? error.message : 'History import failed.'); process.exitCode = 1; });
|
|
83
|
+
//# sourceMappingURL=history-import-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history-import-cli.js","sourceRoot":"","sources":["../src/history-import-cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAC,QAAQ,EAAC,IAAI,EAAC,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAC,aAAa,EAAC,MAAM,UAAU,CAAC;AACvC,OAAO,EAAC,mBAAmB,EAA2B,MAAM,qBAAqB,CAAC;AAClF,OAAO,EAAC,WAAW,EAAC,MAAM,aAAa,CAAC;AACxC,mGAAmG;AACnG,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAa,EAAC,MAAsB,OAAO,CAAC,GAAG,EAAC,MAAwB,OAAO,CAAC,GAAG;IACzH,MAAM,KAAK,GAAC,IAAI,GAAG,CAAS,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA,EAAE,CAAA,CAAC,KAAG,SAAS,IAAE,CAAC,KAAG,QAAQ,CAAC,CAAC,CAAC;IACzE,IAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAC,CAAC;QAAA,GAAG,CAAC,yLAAyL,CAAC,CAAC;QAAA,OAAO;IAAA,CAAC;IAC/N,MAAM,IAAI,GAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA,EAAE,CAAA,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAAA,IAAG,IAAI,CAAC,MAAM,KAAG,CAAC,IAAE,IAAI,CAAC,CAAC,CAAC,KAAG,QAAQ,IAAE,CAAC,IAAI,CAAC,CAAC,CAAC;QAAC,MAAM,KAAK,CAAC,mDAAmD,CAAC,CAAC;IAC3J,MAAM,IAAI,GAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAAA,IAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAE,IAAI,CAAC,IAAI,GAAC,QAAQ;QAAC,MAAM,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC3H,IAAI,KAAS,CAAC;IAAA,IAAG,CAAC;QAAA,KAAK,GAAC,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAC,MAAM,CAAC,CAAC,CAAC;IAAA,CAAC;IAAA,MAAK,CAAC;QAAA,MAAM,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAAA,CAAC;IACnI,yBAAyB,CAAC,KAAK,CAAC,CAAC;IACjC,GAAG,CAAC,aAAa,KAAK,CAAC,QAAQ,CAAC,MAAM,kBAAkB,KAAK,CAAC,OAAO,CAAC,MAAM,WAAW,CAAC,CAAC;IACzF,IAAG,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,EAAC,CAAC;QAAA,GAAG,CAAC,6EAA6E,CAAC,CAAC;QAAA,OAAO;IAAA,CAAC;IACrH,MAAM,MAAM,GAAC,GAAG,CAAC,cAAc,CAAC;IAAA,IAAG,CAAC,MAAM;QAAC,MAAM,KAAK,CAAC,gFAAgF,CAAC,CAAC;IACzI,MAAM,QAAQ,GAAC,IAAI,mBAAmB,CAAC,EAAC,MAAM,EAAC,CAAC,CAAC;IACjD,KAAI,IAAI,OAAO,GAAC,CAAC,GAAE,OAAO,EAAE,EAAC,CAAC;QAC7B,IAAG,CAAC;YAAA,MAAM,MAAM,GAAC,MAAM,QAAQ,CAAC,cAAc,CAAC,EAAC,GAAG,KAAK,EAAC,UAAU,EAAC,CAAC,IAAW,EAAC,KAAY,EAAC,EAAE,CAAA,GAAG,CAAC,YAAY,IAAI,IAAI,KAAK,GAAG,CAAC,EAAC,CAAC,CAAC;YAAA,GAAG,CAAC,UAAU,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,aAAa,IAAE,KAAK,CAAC,QAAQ,CAAC,MAAM,YAAY,CAAC,CAAC;YAAA,OAAO;QAAA,CAAC;QACpO,OAAM,KAAK,EAAC,CAAC;YACZ,IAAG,OAAO,IAAE,CAAC,IAAE,CAAC,CAAC,KAAK,YAAY,SAAS,IAAE,KAAK,YAAY,WAAW,IAAE,KAAK,CAAC,SAAS,CAAC;gBAAC,MAAM,KAAK,CAAC;YACxG,GAAG,CAAC,0DAA0D,CAAC,CAAC;YAAA,MAAM,IAAI,OAAO,CAAC,CAAC,CAAA,EAAE,CAAA,UAAU,CAAC,CAAC,EAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAC,IAAI,GAAC,CAAC,IAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QACrI,CAAC;IACF,CAAC;AACF,CAAC;AACD,MAAM,UAAU,yBAAyB,CAAC,KAAS;IAClD,MAAM,IAAI,GAAC,4EAA4E,CAAC;IACxF,MAAM,EAAE,GAAC,CAAC,CAAS,EAAC,EAAE,CAAA,OAAO,CAAC,KAAG,QAAQ,IAAE,CAAC,CAAC,MAAM,GAAC,CAAC,IAAE,CAAC,CAAC,MAAM,IAAE,GAAG,IAAE,CAAC,KAAG,CAAC,CAAC,IAAI,EAAE,IAAE,CAAC,uBAAuB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACrH,IAAG,CAAC,KAAK,IAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,IAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,IAAE,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,IAAE,KAAK,CAAC,OAAO,CAAC,MAAM,GAAC,KAAK,IAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAS,EAAC,EAAE,CAAA,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAE,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,KAAG,KAAK,CAAC,OAAO,CAAC,MAAM;QAAC,MAAM,KAAK,CAAC,kEAAkE,CAAC,CAAC;IACpU,IAAG,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,IAAE,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAC,KAAK;QAAC,MAAM,KAAK,CAAC,4BAA4B,CAAC,CAAC;IACjI,MAAM,GAAG,GAAC,IAAI,GAAG,EAAiB,EAAC,KAAK,GAAC,IAAI,GAAG,EAAiB,CAAC;IAClE,KAAI,MAAM,CAAC,KAAK,EAAC,OAAO,CAAC,IAAG,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAC,CAAC;QACrD,MAAM,IAAI,GAAC,CAAC,MAAa,EAAC,EAAE,GAAC,MAAM,KAAK,CAAC,WAAW,KAAK,GAAC,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC,CAAA,CAAC,CAAC;QAC5E,IAAG,CAAC,OAAO,IAAE,CAAC,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAE,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAE,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC;YAAC,IAAI,CAAC,4DAA4D,CAAC,CAAC;QAC5K,MAAM,IAAI,GAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAAA,IAAG,OAAO,OAAO,CAAC,MAAM,KAAG,QAAQ,IAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAE,IAAI,GAAC,CAAC,IAAE,IAAI,GAAC,IAAI,CAAC,GAAG,EAAE;YAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;QACrK,MAAM,CAAC,GAAC,OAAO,CAAC,OAAO,CAAC;QAAA,IAAG,CAAC,CAAC,IAAE,CAAC,CAAC,MAAM,EAAC,OAAO,EAAC,OAAO,EAAC,OAAO,EAAC,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA,EAAE,CAAA,CAAC,CAAC,MAAM,EAAC,MAAM,EAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;QACnM,IAAG,CAAC,CAAC,IAAI,KAAG,MAAM,IAAE,CAAC,OAAO,CAAC,CAAC,IAAI,KAAG,QAAQ,IAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAE,CAAC,CAAC,IAAI,CAAC,MAAM,GAAC,KAAK,IAAE,CAAC,CAAC,KAAK,CAAC;YAAC,IAAI,CAAC,wCAAwC,CAAC,CAAC;QAC5I,IAAG,CAAC,CAAC,IAAI,KAAG,MAAM,IAAE,CAAC,CAAC,CAAC,CAAC,KAAK,IAAE,OAAO,CAAC,CAAC,KAAK,CAAC,OAAO,KAAG,QAAQ,IAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;YAAC,IAAI,CAAC,kGAAkG,CAAC,CAAC;QACtN,IAAG,OAAO,CAAC,eAAe,KAAG,SAAS,IAAE,CAAC,EAAE,CAAC,OAAO,CAAC,eAAe,CAAC;YAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC;QAC5H,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAC,IAAI,CAAC,CAAC;QAAA,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAC,KAAK,CAAC,CAAC;IAClE,CAAC;IACD,KAAI,MAAM,CAAC,KAAK,EAAC,OAAO,CAAC,IAAG,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE;QAAC,IAAG,OAAO,CAAC,eAAe,KAAG,SAAS,IAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,IAAE,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,CAAE,GAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAE,OAAO,CAAC,eAAe,KAAG,OAAO,CAAC,QAAQ,IAAE,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,KAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAE,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,CAAE,IAAE,KAAK,CAAC;YAAC,MAAM,KAAK,CAAC,WAAW,KAAK,GAAC,CAAC,kEAAkE,CAAC,CAAC;AACzb,CAAC;AACD,IAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAE,MAAM,CAAC,IAAI,CAAC,GAAG,KAAG,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;IAAC,gBAAgB,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAA,EAAE,GAAC,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAA,CAAC,CAAA,KAAK,CAAC,OAAO,CAAA,CAAC,CAAA,wBAAwB,CAAC,CAAC,CAAA,OAAO,CAAC,QAAQ,GAAC,CAAC,CAAC,CAAA,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { RobonoManagedMedia } from './managed-media.js';
|
|
2
|
+
import type { MessagingStoredMedia, MessagingUploadFile, MessagingMediaKind } from './messaging-media.js';
|
|
3
|
+
export interface HistoryImportMessage {
|
|
4
|
+
sourceId: string;
|
|
5
|
+
authorId: string;
|
|
6
|
+
sentAt: string;
|
|
7
|
+
content: {
|
|
8
|
+
kind: 'text' | 'voice' | 'image' | 'video' | 'document';
|
|
9
|
+
text?: string;
|
|
10
|
+
media?: MessagingStoredMedia;
|
|
11
|
+
};
|
|
12
|
+
replyToSourceId?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface HistoryImportStatus {
|
|
15
|
+
jobId: string;
|
|
16
|
+
state: 'staging' | 'committing' | 'complete' | 'cancelled';
|
|
17
|
+
expiresAt?: string;
|
|
18
|
+
stagedCount?: number;
|
|
19
|
+
importedCount?: number;
|
|
20
|
+
}
|
|
21
|
+
/** Server-only. A manage-scoped key cannot be exposed in an app or browser. */
|
|
22
|
+
export declare class RobonoHistoryImport {
|
|
23
|
+
private options;
|
|
24
|
+
private endpoint;
|
|
25
|
+
constructor(options: {
|
|
26
|
+
apiKey: string;
|
|
27
|
+
baseUrl?: string;
|
|
28
|
+
mediaUrl?: string;
|
|
29
|
+
fetch?: typeof fetch;
|
|
30
|
+
});
|
|
31
|
+
private request;
|
|
32
|
+
start(input: {
|
|
33
|
+
jobId: string;
|
|
34
|
+
conversationId: string;
|
|
35
|
+
authors: string[];
|
|
36
|
+
count: number;
|
|
37
|
+
}): Promise<HistoryImportStatus>;
|
|
38
|
+
append(jobId: string, messages: Array<HistoryImportMessage & {
|
|
39
|
+
ordinal: number;
|
|
40
|
+
}>): Promise<HistoryImportStatus>;
|
|
41
|
+
status(jobId: string): Promise<HistoryImportStatus>;
|
|
42
|
+
commit(jobId: string): Promise<HistoryImportStatus>;
|
|
43
|
+
cancel(jobId: string): Promise<HistoryImportStatus>;
|
|
44
|
+
/** Upload historic files privately, with normal limits, verification and screening. */
|
|
45
|
+
media(jobId: string): RobonoManagedMedia;
|
|
46
|
+
upload(input: {
|
|
47
|
+
jobId: string;
|
|
48
|
+
conversationId: string;
|
|
49
|
+
authorId: string;
|
|
50
|
+
uploadId: string;
|
|
51
|
+
kind: MessagingMediaKind;
|
|
52
|
+
file: MessagingUploadFile;
|
|
53
|
+
bytes: Uint8Array;
|
|
54
|
+
}): Promise<MessagingStoredMedia>;
|
|
55
|
+
/** Retry this whole operation with the same job/source IDs after interruption. */
|
|
56
|
+
importMessages(input: {
|
|
57
|
+
jobId: string;
|
|
58
|
+
conversationId: string;
|
|
59
|
+
authors: string[];
|
|
60
|
+
messages: HistoryImportMessage[];
|
|
61
|
+
onProgress?: (accepted: number, total: number) => void;
|
|
62
|
+
}): Promise<HistoryImportStatus>;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=history-import.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history-import.d.ts","sourceRoot":"","sources":["../src/history-import.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,kBAAkB,EAAC,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAC,oBAAoB,EAAC,mBAAmB,EAAC,kBAAkB,EAAC,MAAM,sBAAsB,CAAC;AACtG,MAAM,WAAW,oBAAoB;IACpC,QAAQ,EAAC,MAAM,CAAC;IAAA,QAAQ,EAAC,MAAM,CAAC;IAAA,MAAM,EAAC,MAAM,CAAC;IAC9C,OAAO,EAAC;QAAC,IAAI,EAAC,MAAM,GAAC,OAAO,GAAC,OAAO,GAAC,OAAO,GAAC,UAAU,CAAC;QAAA,IAAI,CAAC,EAAC,MAAM,CAAC;QAAA,KAAK,CAAC,EAAC,oBAAoB,CAAA;KAAC,CAAC;IAClG,eAAe,CAAC,EAAC,MAAM,CAAC;CACxB;AACD,MAAM,WAAW,mBAAmB;IAAE,KAAK,EAAC,MAAM,CAAC;IAAA,KAAK,EAAC,SAAS,GAAC,YAAY,GAAC,UAAU,GAAC,WAAW,CAAC;IAAA,SAAS,CAAC,EAAC,MAAM,CAAC;IAAA,WAAW,CAAC,EAAC,MAAM,CAAC;IAAA,aAAa,CAAC,EAAC,MAAM,CAAA;CAAC;AAEnK,+EAA+E;AAC/E,qBAAa,mBAAmB;IAEnB,OAAO,CAAC,OAAO;IAD3B,OAAO,CAAC,QAAQ,CAAQ;gBACJ,OAAO,EAAC;QAAC,MAAM,EAAC,MAAM,CAAC;QAAA,OAAO,CAAC,EAAC,MAAM,CAAC;QAAA,QAAQ,CAAC,EAAC,MAAM,CAAC;QAAA,KAAK,CAAC,EAAC,OAAO,KAAK,CAAA;KAAC;YAIlF,OAAO;IAOrB,KAAK,CAAC,KAAK,EAAC;QAAC,KAAK,EAAC,MAAM,CAAC;QAAA,cAAc,EAAC,MAAM,CAAC;QAAA,OAAO,EAAC,MAAM,EAAE,CAAC;QAAA,KAAK,EAAC,MAAM,CAAA;KAAC;IAC9E,MAAM,CAAC,KAAK,EAAC,MAAM,EAAC,QAAQ,EAAC,KAAK,CAAC,oBAAoB,GAAC;QAAC,OAAO,EAAC,MAAM,CAAA;KAAC,CAAC;IACzE,MAAM,CAAC,KAAK,EAAC,MAAM;IACnB,MAAM,CAAC,KAAK,EAAC,MAAM;IACnB,MAAM,CAAC,KAAK,EAAC,MAAM;IACnB,uFAAuF;IACvF,KAAK,CAAC,KAAK,EAAC,MAAM;IAIZ,MAAM,CAAC,KAAK,EAAC;QAAC,KAAK,EAAC,MAAM,CAAC;QAAA,cAAc,EAAC,MAAM,CAAC;QAAA,QAAQ,EAAC,MAAM,CAAC;QAAA,QAAQ,EAAC,MAAM,CAAC;QAAA,IAAI,EAAC,kBAAkB,CAAC;QAAA,IAAI,EAAC,mBAAmB,CAAC;QAAA,KAAK,EAAC,UAAU,CAAA;KAAC,GAAE,OAAO,CAAC,oBAAoB,CAAC;IAUxL,kFAAkF;IAC5E,cAAc,CAAC,KAAK,EAAC;QAAC,KAAK,EAAC,MAAM,CAAC;QAAA,cAAc,EAAC,MAAM,CAAC;QAAA,OAAO,EAAC,MAAM,EAAE,CAAC;QAAA,QAAQ,EAAC,oBAAoB,EAAE,CAAC;QAAA,UAAU,CAAC,EAAC,CAAC,QAAQ,EAAC,MAAM,EAAC,KAAK,EAAC,MAAM,KAAG,IAAI,CAAA;KAAC,GAAE,OAAO,CAAC,mBAAmB,CAAC;CAW/L"}
|