@robono/server 0.21.0 → 0.23.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 +12 -0
- package/README.md +30 -2
- package/SBOM.spdx.json +9 -9
- package/dist/calling-session-handle.d.ts +13 -0
- package/dist/calling-session-handle.d.ts.map +1 -0
- package/dist/calling-session-handle.js +37 -0
- package/dist/calling-session-handle.js.map +1 -0
- package/dist/calling.d.ts +1 -0
- package/dist/calling.d.ts.map +1 -1
- package/dist/calling.js +1 -1
- package/dist/calling.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/location.d.ts.map +1 -1
- package/dist/location.js +3 -0
- package/dist/location.js.map +1 -1
- package/dist/messaging-extras-contract.d.ts +14 -0
- package/dist/messaging-extras-contract.d.ts.map +1 -0
- package/dist/messaging-extras-contract.js +94 -0
- package/dist/messaging-extras-contract.js.map +1 -0
- package/dist/messaging-extras.d.ts +10 -0
- package/dist/messaging-extras.d.ts.map +1 -0
- package/dist/messaging-extras.js +42 -0
- package/dist/messaging-extras.js.map +1 -0
- package/dist/messaging-notification-providers.d.ts +20 -1
- package/dist/messaging-notification-providers.d.ts.map +1 -1
- package/dist/messaging-notification-providers.js +16 -5
- 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 +22 -2
- package/dist/messaging-push-worker.js.map +1 -1
- package/dist/messenger-calling.d.ts +14 -0
- package/dist/messenger-calling.d.ts.map +1 -0
- package/dist/messenger-calling.js +173 -0
- package/dist/messenger-calling.js.map +1 -0
- package/dist/messenger-check.d.ts.map +1 -1
- package/dist/messenger-check.js +3 -1
- package/dist/messenger-check.js.map +1 -1
- package/dist/messenger-connected-templates.d.ts.map +1 -1
- package/dist/messenger-connected-templates.js +5 -2
- package/dist/messenger-connected-templates.js.map +1 -1
- package/dist/messenger-connections.d.ts +31 -0
- package/dist/messenger-connections.d.ts.map +1 -0
- package/dist/messenger-connections.js +18 -0
- package/dist/messenger-connections.js.map +1 -0
- package/dist/messenger-customer-templates.d.ts.map +1 -1
- package/dist/messenger-customer-templates.js +2 -0
- package/dist/messenger-customer-templates.js.map +1 -1
- package/dist/messenger-init.d.ts.map +1 -1
- package/dist/messenger-init.js +9 -5
- package/dist/messenger-init.js.map +1 -1
- package/dist/messenger-install-cli.js +23 -6
- package/dist/messenger-install-cli.js.map +1 -1
- package/dist/messenger-install.d.ts +11 -1
- package/dist/messenger-install.d.ts.map +1 -1
- package/dist/messenger-install.js +260 -62
- package/dist/messenger-install.js.map +1 -1
- package/dist/messenger-node.d.ts +14 -0
- package/dist/messenger-node.d.ts.map +1 -0
- package/dist/messenger-node.js +127 -0
- package/dist/messenger-node.js.map +1 -0
- package/dist/messenger-upgrade.d.ts +21 -0
- package/dist/messenger-upgrade.d.ts.map +1 -0
- package/dist/messenger-upgrade.js +136 -0
- package/dist/messenger-upgrade.js.map +1 -0
- package/dist/messenger-vite-templates.d.ts +12 -0
- package/dist/messenger-vite-templates.d.ts.map +1 -0
- package/dist/messenger-vite-templates.js +89 -0
- package/dist/messenger-vite-templates.js.map +1 -0
- package/dist/messenger.d.ts +16 -0
- package/dist/messenger.d.ts.map +1 -1
- package/dist/messenger.js +28 -1
- package/dist/messenger.js.map +1 -1
- package/legacy/0.21.0/messenger-connected-templates.js +138 -0
- package/legacy/0.21.0/messenger-customer-templates.js +111 -0
- package/legacy/0.21.0/messenger-init.js +121 -0
- package/legacy/0.21.0/messenger-install.js +243 -0
- package/legacy/0.21.0/messenger-preview-template.js +43 -0
- package/legacy/0.21.0/package.json +1 -0
- package/package.json +11 -2
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { messengerPreviewTemplates } from './messenger-preview-template.js';
|
|
2
|
+
import { readFile, writeFile, mkdir, lstat, realpath, unlink } from 'node:fs/promises';
|
|
3
|
+
import { resolve, join, dirname, relative } from 'node:path';
|
|
4
|
+
import spawn from 'cross-spawn';
|
|
5
|
+
import { messengerStarterFiles } from './messenger-init.js';
|
|
6
|
+
async function exists(file) { try {
|
|
7
|
+
await lstat(file);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch (e) {
|
|
11
|
+
if (e.code === 'ENOENT')
|
|
12
|
+
return false;
|
|
13
|
+
throw e;
|
|
14
|
+
} }
|
|
15
|
+
async function project(directory) {
|
|
16
|
+
const root = resolve(directory);
|
|
17
|
+
if (await realpath(root) !== root)
|
|
18
|
+
throw Error('Use a project path without symbolic links.');
|
|
19
|
+
const stat = await lstat(join(root, 'package.json'));
|
|
20
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
21
|
+
throw Error('Use a regular package.json.');
|
|
22
|
+
const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
23
|
+
return { root, pkg, deps: { ...pkg.devDependencies, ...pkg.dependencies } };
|
|
24
|
+
}
|
|
25
|
+
async function safePath(file) {
|
|
26
|
+
let part = resolve(file);
|
|
27
|
+
while (true) {
|
|
28
|
+
if (await exists(part)) {
|
|
29
|
+
const s = await lstat(part);
|
|
30
|
+
if (s.isSymbolicLink())
|
|
31
|
+
throw Error('Installation paths cannot contain symbolic links.');
|
|
32
|
+
}
|
|
33
|
+
const parent = dirname(part);
|
|
34
|
+
if (parent === part)
|
|
35
|
+
break;
|
|
36
|
+
part = parent;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function appRoot(root) {
|
|
40
|
+
for (const base of ['src', ''])
|
|
41
|
+
if (await exists(join(root, base, 'app'))) {
|
|
42
|
+
await safePath(join(root, base, 'app'));
|
|
43
|
+
return base;
|
|
44
|
+
}
|
|
45
|
+
throw Error('Automatic website installation supports Next.js App Router. Create its app directory first, or use robono-messenger-init for another server.');
|
|
46
|
+
}
|
|
47
|
+
function installCommand(root, manager, packages) {
|
|
48
|
+
if (!['npm', 'pnpm', 'yarn'].includes(manager))
|
|
49
|
+
throw Error('Supported package managers: npm, pnpm, yarn.');
|
|
50
|
+
return { cwd: root, command: manager, args: manager === 'npm' ? ['install', '--ignore-scripts', '--', ...packages] : manager === 'pnpm' ? ['add', '--ignore-scripts', '--', ...packages] : ['add', '--ignore-scripts', ...packages] };
|
|
51
|
+
}
|
|
52
|
+
async function manager(root, pkg) {
|
|
53
|
+
const locks = [];
|
|
54
|
+
for (const [file, name] of [['package-lock.json', 'npm'], ['pnpm-lock.yaml', 'pnpm'], ['yarn.lock', 'yarn']])
|
|
55
|
+
if (await exists(join(root, file)))
|
|
56
|
+
locks.push(name);
|
|
57
|
+
const named = pkg.packageManager?.split('@')[0];
|
|
58
|
+
if (locks.length > 1 || named && locks.length && locks[0] !== named)
|
|
59
|
+
throw Error('Conflicting package-manager files. Keep the lockfile used by this project before installing.');
|
|
60
|
+
const result = named ?? locks[0] ?? 'npm';
|
|
61
|
+
// Yarn Berry has different script controls; do not silently run hooks or rewrite its configuration.
|
|
62
|
+
if (result === 'yarn' && pkg.packageManager && !pkg.packageManager.startsWith('yarn@1.'))
|
|
63
|
+
throw Error('Use the generated-file command for Yarn 2+; automatic installation currently supports Yarn 1, npm and pnpm.');
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
export async function planMessengerInstallation(options) {
|
|
67
|
+
return createInstallationPlan(options, false);
|
|
68
|
+
}
|
|
69
|
+
async function createInstallationPlan(options, resume) {
|
|
70
|
+
if (Number(process.versions.node.split('.')[0]) < 22)
|
|
71
|
+
throw Error('Use Node.js 22 or later for automatic installation.');
|
|
72
|
+
const client = await project(options.project), platform = client.deps.expo ? 'expo' : client.deps.next ? 'web' : null;
|
|
73
|
+
if (!platform)
|
|
74
|
+
throw Error('Automatic installation supports Next.js App Router and Expo. For other projects use robono-messenger-init.');
|
|
75
|
+
if (platform === 'expo' && !/^[~^]?(55|56)\./.test(client.deps.expo))
|
|
76
|
+
throw Error('The supplied Expo messenger supports Expo 55–56. Keep your current app unchanged and check the mobile installation guide.');
|
|
77
|
+
if (platform === 'expo' && !options.serverProject)
|
|
78
|
+
throw Error('Pass --server-project with your existing Next.js server folder. A phone app must not contain the Robono server key.');
|
|
79
|
+
const server = platform === 'web' ? client : await project(options.serverProject);
|
|
80
|
+
if (!server.deps.next)
|
|
81
|
+
throw Error('The automatic server connection needs Next.js App Router. Use robono-messenger-init for other servers.');
|
|
82
|
+
if (platform === 'expo' && server.root === client.root)
|
|
83
|
+
throw Error('Use a separate server project; never install server credentials inside the phone app.');
|
|
84
|
+
const serverBase = await appRoot(server.root), clientBase = platform === 'web' ? serverBase : (await exists(join(client.root, 'src')) ? 'src' : '');
|
|
85
|
+
const detected = client.deps['@supabase/supabase-js'] ? 'supabase' : client.deps.firebase ? 'firebase' : 'custom';
|
|
86
|
+
if (!options.auth && client.deps['@supabase/supabase-js'] && client.deps.firebase)
|
|
87
|
+
throw Error('Both sign-in providers are installed. Choose --auth supabase, firebase or custom.');
|
|
88
|
+
const auth = options.auth ?? detected;
|
|
89
|
+
const providerName = auth === 'supabase' ? '@supabase/supabase-js' : auth === 'firebase' ? 'firebase' : null;
|
|
90
|
+
if (providerName && client.deps[providerName]) {
|
|
91
|
+
const installed = await readFile(join(client.root, 'node_modules', providerName, 'package.json'), 'utf8').then(value => JSON.parse(value).version).catch(() => client.deps[providerName]);
|
|
92
|
+
const major = Number(installed.match(/^[~^]?(\d+)/)?.[1]);
|
|
93
|
+
if (!major || major < (auth === 'supabase' ? 2 : 9))
|
|
94
|
+
throw Error('This sign-in package needs a newer API. Keep your current sign-in unchanged and use --auth custom, or update it separately.');
|
|
95
|
+
}
|
|
96
|
+
const templates = messengerStarterFiles({ platform, framework: 'next', origin: options.origin, companyName: options.companyName, auth });
|
|
97
|
+
const files = [], replacements = [];
|
|
98
|
+
for (const [name, content] of Object.entries(templates)) {
|
|
99
|
+
if (name.startsWith('client/'))
|
|
100
|
+
files.push({ path: join(client.root, clientBase, 'robono', name), content });
|
|
101
|
+
else if (name.startsWith('server/'))
|
|
102
|
+
files.push({ path: join(server.root, serverBase, 'robono', name), content });
|
|
103
|
+
}
|
|
104
|
+
files.push({ path: join(server.root, serverBase, 'app/api/robono/route.ts'), content: templates['app/api/robono/route.ts'].replace('../../../server/robono', '../../../robono/server/robono') });
|
|
105
|
+
const preview = messengerPreviewTemplates();
|
|
106
|
+
files.push({ path: join(server.root, serverBase, 'app/robono-preview/page.tsx'), content: preview.page }, { path: join(server.root, serverBase, 'robono/client/RobonoPreview.tsx'), content: preview.client });
|
|
107
|
+
const notes = [`Detected ${platform === 'web' ? 'Next.js website' : 'Expo phone app'} and ${auth} sign-in.`,
|
|
108
|
+
'Start your Next.js development server and open /robono-preview for the two-person local example. It returns 404 in production.',
|
|
109
|
+
'Connect your current messaging permission check in robono/server/robono-access.ts. Access stays blocked until then.',
|
|
110
|
+
'Keep provider credentials and the Robono key on the server. The installer never reads or edits your .env files.',
|
|
111
|
+
'Mount CustomerMessenger with your existing sign-in client and contacts. Account wiring is included.',
|
|
112
|
+
'Connect website alerts or Apple/Google credentials in Robono → Your app → Message notifications. Chat works before alerts are configured.'];
|
|
113
|
+
const setup = { schema: 2, platform, auth, companyName: options.companyName, origin: options.origin, serverProject: relative(client.root, server.root) || '.', dependencies: 'pending', checks: { files: 'installed', permissions: 'needs-connection', serverKey: 'needs-server-environment', contacts: 'needs-connection', notifications: 'needs-configuration', liveConnection: 'not-tested' } };
|
|
114
|
+
files.push({ path: join(client.root, '.robono/setup.json'), content: JSON.stringify(setup, null, 2) + '\n' });
|
|
115
|
+
files.push({ path: join(client.root, '.robono/SETUP.md'), content: '# Connect your messenger\n\n' + notes.map(x => '- ' + x).join('\n') + '\n\n## Connect your screen\n' + (auth === 'custom' ? 'Implement RobonoAccounts from client/ConnectedRobonoChat.tsx using your existing sign-in. Render <CustomerMessenger accounts={yourAccounts} contacts={yourContacts}/>.' : 'Import CustomerMessenger from the generated client/CustomerMessenger.tsx. Render <CustomerMessenger auth={yourExistingSignInClient} contacts={yourContacts}/>.') + ' Give the messenger an area with a height. Keep it mounted in your signed-in application. It follows account changes; names and avatars update without restarting chat. For Supabase use its client; for Firebase use its Auth instance.\n\n## Allow the right people\nConnect authorize in robono-access.ts to current account eligibility, suspension and conversation permissions. It denies access until implemented. When a friendship changes, call synchronizeFriendship(getMessenger(), friendship) on your server. Use revisions from your database; group membership uses getMessenger().robono.setGroup. Never use an always-allow function.\n\n## Sign-out\nThe messenger account menu clears local content and unregisters notifications before provider sign-out. If your own account menu signs out first, provider events still clear the visible chat. Keep account suspension and relationship revocation synchronized with Robono; an expired session cannot reliably unregister a remote device. Never substitute another account’s token.\n\n## Check the connection\nDuring development, add setupChecks to CustomerMessenger and press Check connection after signing in to your app. It uses your current login; no copied tokens are needed. It checks settings, available chats and notification configuration without sending a message. This optional panel is hidden in production.\n\nRun robono-messenger-install --check for the local checklist. For additional command-line security checks, set ROBONO_MESSENGER_URL, ROBONO_WEBSITE_ORIGIN and ROBONO_TEST_USER_TOKEN locally. Add --json for a shareable report without secrets. No local checker can prove your complete permission rules; test denied accounts and cross-account access too.\n\n## Interrupted installation\nRun robono-messenger-install --resume from the same project. It reinstalls the required packages without replacing your integration code, screens or environment files.\n\n## Server configuration\nSet ROBONO_API_KEY privately on the server. Use ROBONO_ALLOWED_ORIGINS for your production website addresses instead of editing generated code. Never use a wildcard. Existing Next.js Supabase public settings are reused; see server.env.example.\n\n## Notifications\nFor website alerts configure your HTTPS origin and chat path. For iPhone, upload your APNs .p8 key, key ID, Team ID and Bundle ID in Robono. For Android, configure google-services.json in your app and upload the Firebase messaging service account privately in Robono. Never put private keys in client code. Build both apps after native dependency changes.\n\n## Local preview\nOpen /robono-preview on your Next.js development server. It uses the actual web messenger with fictional Alice and Bob and local text only. It sends no customer requests and returns 404 in production.\n' });
|
|
116
|
+
files.push({ path: join(server.root, '.robono/server.env.example'), content: templates['.env.example'] });
|
|
117
|
+
const serverPackages = ['@robono/server@^0.21.0', ...(platform === 'expo' ? ['@robono/messaging@^0.4.1', '@robono/messaging-web@^0.6.0'] : []), ...(auth === 'supabase' ? ['@supabase/supabase-js@^2'] : auth === 'firebase' ? ['firebase-admin@^14'] : [])];
|
|
118
|
+
const clientPackages = ['@robono/messaging@^0.4.1', `@robono/messaging-${platform === 'web' ? 'web' : 'react-native'}@^0.6.0`, ...(auth === 'supabase' ? ['@supabase/supabase-js@^2'] : auth === 'firebase' ? ['firebase@^13'] : [])];
|
|
119
|
+
const packageName = (spec) => spec.slice(0, spec.lastIndexOf('@'));
|
|
120
|
+
const keepExistingProvider = (spec, deps) => !['firebase', 'firebase-admin', '@supabase/supabase-js'].includes(packageName(spec)) || !deps[packageName(spec)];
|
|
121
|
+
const serverInstall = serverPackages.filter(spec => keepExistingProvider(spec, server.deps)), clientInstall = clientPackages.filter(spec => keepExistingProvider(spec, client.deps));
|
|
122
|
+
const commands = [];
|
|
123
|
+
if (platform === 'web')
|
|
124
|
+
commands.push(installCommand(client.root, await manager(client.root, client.pkg), [...new Set([...serverInstall, ...clientInstall])]));
|
|
125
|
+
else {
|
|
126
|
+
commands.push(installCommand(server.root, await manager(server.root, server.pkg), serverInstall));
|
|
127
|
+
const cm = await manager(client.root, client.pkg);
|
|
128
|
+
commands.push(installCommand(client.root, cm, clientInstall));
|
|
129
|
+
// Expo selects versions compatible with the customer's installed SDK, not arbitrary latest peers.
|
|
130
|
+
commands.push({ cwd: client.root, command: process.execPath, args: [join(client.root, 'node_modules/expo/bin/cli'), 'install', 'expo-audio', 'expo-image-picker', 'expo-document-picker', 'expo-file-system', 'expo-sharing', 'expo-video', 'expo-crypto', 'react-native-safe-area-context', 'react-native-svg', 'expo-clipboard', 'expo-image-manipulator', 'expo-video-thumbnails', 'react-native-webview', 'expo-blur', 'expo-notifications', 'expo-secure-store', '--' + cm] });
|
|
131
|
+
if (await exists(join(client.root, 'app.config.js')) || await exists(join(client.root, 'app.config.ts')))
|
|
132
|
+
throw Error('Your Expo configuration is code. Use robono-messenger-init and the mobile guide so your existing configuration is preserved.');
|
|
133
|
+
const configPath = join(client.root, 'app.json');
|
|
134
|
+
await safePath(configPath);
|
|
135
|
+
const before = await readFile(configPath, 'utf8'), config = JSON.parse(before);
|
|
136
|
+
if (!config.expo || typeof config.expo !== 'object' || Array.isArray(config.expo))
|
|
137
|
+
throw Error('Expected an expo configuration object in app.json.');
|
|
138
|
+
const plugins = config.expo?.plugins ?? [];
|
|
139
|
+
if (!Array.isArray(plugins))
|
|
140
|
+
throw Error('Expected an Expo plugins array in app.json.');
|
|
141
|
+
const names = plugins.map(p => Array.isArray(p) ? p[0] : p);
|
|
142
|
+
const required = [['expo-audio', { microphonePermission: 'Allow microphone access to record voice messages.', enableBackgroundRecording: false }], ['expo-image-picker', { cameraPermission: 'Allow camera access to take photos and videos for messages.' }], 'expo-notifications', '@robono/messaging-react-native'];
|
|
143
|
+
const added = required.filter(p => !names.includes(Array.isArray(p) ? p[0] : p));
|
|
144
|
+
if (added.length) {
|
|
145
|
+
config.expo.plugins = [...plugins, ...added];
|
|
146
|
+
replacements.push({ path: configPath, before, after: JSON.stringify(config, null, 2) + '\n' });
|
|
147
|
+
files.push({ path: join(client.root, '.robono/app.json.before-install'), content: before });
|
|
148
|
+
}
|
|
149
|
+
notes.push('Added missing Expo plugins; existing permission wording is preserved. Rebuild both iPhone and Android after native dependency changes.');
|
|
150
|
+
}
|
|
151
|
+
if (!resume)
|
|
152
|
+
for (const file of files) {
|
|
153
|
+
await safePath(file.path);
|
|
154
|
+
if (await exists(file.path))
|
|
155
|
+
throw Error(`Already exists: ${file.path}. Nothing was changed. Use --check to see the remaining steps, or --resume to finish package installation.`);
|
|
156
|
+
}
|
|
157
|
+
return { platform, auth, files: resume ? [] : files, replacements: resume ? [] : replacements, commands, notes: resume ? ['Resume package installation. Existing integration files, permission code, screens and environment files are preserved.'] : notes, setupFile: join(client.root, '.robono/setup.json') };
|
|
158
|
+
}
|
|
159
|
+
/** Reconstruct trusted package commands; never execute commands from saved metadata. */
|
|
160
|
+
export async function planMessengerInstallationResume(directory) {
|
|
161
|
+
const root = resolve(directory), setupFile = join(root, '.robono/setup.json');
|
|
162
|
+
await safePath(setupFile);
|
|
163
|
+
const setup = JSON.parse(await readFile(setupFile, 'utf8'));
|
|
164
|
+
if (![1, 2].includes(setup.schema) || !['web', 'expo'].includes(setup.platform) || !['custom', 'supabase', 'firebase'].includes(setup.auth) || typeof setup.origin !== 'string' || typeof setup.serverProject !== 'string' || setup.companyName !== undefined && typeof setup.companyName !== 'string')
|
|
165
|
+
throw Error('Invalid saved setup. Keep your integration files and run --check.');
|
|
166
|
+
const server = resolve(root, setup.serverProject), base = await appRoot(server);
|
|
167
|
+
const clientBase = setup.platform === 'web' ? base : (await exists(join(root, 'src')) ? 'src' : '');
|
|
168
|
+
for (const file of [join(root, clientBase, 'robono/client/ConnectedRobonoChat.tsx'), join(server, base, 'app/api/robono/route.ts')]) {
|
|
169
|
+
await safePath(file);
|
|
170
|
+
if (!await exists(file))
|
|
171
|
+
throw Error('An integration file is missing. Restore it before resuming; existing code was not changed.');
|
|
172
|
+
}
|
|
173
|
+
return createInstallationPlan({ project: root, ...(setup.platform === 'expo' ? { serverProject: server } : {}), origin: setup.origin, companyName: setup.companyName ?? 'Your company', auth: setup.auth }, true);
|
|
174
|
+
}
|
|
175
|
+
/** Preflight all files; exclusive writes and rollback avoid overwriting customer work. */
|
|
176
|
+
export async function applyMessengerInstallation(plan, install = true) {
|
|
177
|
+
for (const file of plan.files) {
|
|
178
|
+
await safePath(file.path);
|
|
179
|
+
if (await exists(file.path))
|
|
180
|
+
throw Error(`Already exists: ${file.path}. Nothing was changed.`);
|
|
181
|
+
}
|
|
182
|
+
for (const file of plan.replacements) {
|
|
183
|
+
await safePath(file.path);
|
|
184
|
+
if (await readFile(file.path, 'utf8') !== file.before)
|
|
185
|
+
throw Error('Project configuration changed after planning. Run the installer again.');
|
|
186
|
+
}
|
|
187
|
+
const written = [], replaced = [];
|
|
188
|
+
try {
|
|
189
|
+
for (const file of plan.files) {
|
|
190
|
+
await mkdir(dirname(file.path), { recursive: true });
|
|
191
|
+
await writeFile(file.path, file.content, { flag: 'wx', mode: 0o600 });
|
|
192
|
+
written.push(file.path);
|
|
193
|
+
}
|
|
194
|
+
for (const file of plan.replacements) {
|
|
195
|
+
if (await readFile(file.path, 'utf8') !== file.before)
|
|
196
|
+
throw Error('Project configuration changed during installation. Existing configuration was preserved.');
|
|
197
|
+
await writeFile(file.path, file.after);
|
|
198
|
+
replaced.push(file);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
for (const file of replaced)
|
|
203
|
+
await writeFile(file.path, file.before);
|
|
204
|
+
for (const file of written)
|
|
205
|
+
await unlink(file);
|
|
206
|
+
throw e;
|
|
207
|
+
}
|
|
208
|
+
if (install)
|
|
209
|
+
for (const command of plan.commands)
|
|
210
|
+
await new Promise((resolve, reject) => {
|
|
211
|
+
const child = spawn(command.command, command.args, { cwd: command.cwd, stdio: 'inherit', shell: false, env: { ...process.env, npm_config_ignore_scripts: 'true', YARN_ENABLE_SCRIPTS: 'false' } });
|
|
212
|
+
child.on('error', () => reject(Error('Package installation could not start. Files are saved; fix the package-manager error and run robono-messenger-install --resume.')));
|
|
213
|
+
child.on('exit', code => code === 0 ? resolve() : reject(Error('Package installation did not finish. Files are saved; fix the package-manager error and run robono-messenger-install --resume.')));
|
|
214
|
+
});
|
|
215
|
+
if (install && plan.setupFile) {
|
|
216
|
+
await safePath(plan.setupFile);
|
|
217
|
+
const setup = JSON.parse(await readFile(plan.setupFile, 'utf8'));
|
|
218
|
+
setup.dependencies = 'installed';
|
|
219
|
+
await writeFile(plan.setupFile, JSON.stringify(setup, null, 2) + '\n');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
export async function messengerInstallationChecklist(directory) {
|
|
223
|
+
const root = resolve(directory), file = join(root, '.robono/setup.json');
|
|
224
|
+
if (!await exists(file))
|
|
225
|
+
return [{ name: 'Install files', status: 'needs-action', detail: 'Run robono-messenger-install in your Next.js or Expo project.' }];
|
|
226
|
+
const setup = JSON.parse(await readFile(file, 'utf8'));
|
|
227
|
+
const server = resolve(root, setup.serverProject), base = await appRoot(server);
|
|
228
|
+
const permissionFile = join(server, base, 'robono/server/robono-access.ts');
|
|
229
|
+
const permission = await readFile(permissionFile, 'utf8').catch(() => 'Connect your existing messaging permission');
|
|
230
|
+
const missingPermission = permission.includes('Connect your existing messaging permission');
|
|
231
|
+
const clientBase = setup.platform === 'web' ? base : (await exists(join(root, 'src')) ? 'src' : '');
|
|
232
|
+
const complete = await exists(join(root, clientBase, 'robono/client/ConnectedRobonoChat.tsx')) && await exists(join(server, base, 'app/api/robono/route.ts'));
|
|
233
|
+
return [
|
|
234
|
+
{ name: 'Prepared files', status: complete ? 'passed' : 'failed', detail: `${setup.platform === 'expo' ? 'Expo' : 'Next.js'}; ${setup.auth} sign-in.` },
|
|
235
|
+
{ name: 'Package installation', status: setup.dependencies === 'installed' ? 'configured' : 'needs-action', detail: setup.dependencies === 'installed' ? 'The package manager completed. Use the in-app connection check next.' : 'Run robono-messenger-install --resume to finish installing packages without replacing your integration files.' },
|
|
236
|
+
{ name: 'Account permissions', status: missingPermission ? 'needs-action' : 'configured', detail: missingPermission ? 'Connect robono-access.ts to your current permission rules. The live checker verifies a test account.' : 'Permission code was edited. Verify allowed and denied accounts; file inspection cannot prove your rules.' },
|
|
237
|
+
{ name: 'Server key', status: 'needs-action', detail: 'Set ROBONO_API_KEY on the server. This command does not read or print secrets.' },
|
|
238
|
+
{ name: 'Contacts', status: 'needs-action', detail: 'Synchronize permitted friendships and group membership; pass names/avatars to ConnectedRobonoChat.' },
|
|
239
|
+
{ name: 'Notifications', status: 'needs-action', detail: setup.platform === 'expo' ? 'Add Apple/Google credentials in Robono and rebuild both apps with the installed Expo plugins.' : 'Connect your website origin and chat path in Robono.' },
|
|
240
|
+
{ name: 'Live connection', status: 'needs-action', detail: 'Set ROBONO_MESSENGER_URL, ROBONO_WEBSITE_ORIGIN and ROBONO_TEST_USER_TOKEN locally, then run robono-messenger-check.' },
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=messenger-install.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Development-only, in-memory demonstration using the actual shipped messenger. */
|
|
2
|
+
export function messengerPreviewTemplates() {
|
|
3
|
+
return {
|
|
4
|
+
page: `import {notFound} from 'next/navigation';
|
|
5
|
+
import Preview from '../../robono/client/RobonoPreview';
|
|
6
|
+
export const dynamic='force-dynamic';
|
|
7
|
+
export default function RobonoPreviewPage(){if(process.env.NODE_ENV!=='development')notFound();return <Preview/>;}
|
|
8
|
+
`,
|
|
9
|
+
client: `'use client';
|
|
10
|
+
import React,{useEffect,useRef} from 'react';
|
|
11
|
+
import {mountRobonoMessenger} from '@robono/messaging-web';
|
|
12
|
+
import {defaultMessagingSettings} from '@robono/messaging';
|
|
13
|
+
export default function RobonoPreview(){
|
|
14
|
+
const alice=useRef<HTMLDivElement>(null),bob=useRef<HTMLDivElement>(null);
|
|
15
|
+
useEffect(()=>{
|
|
16
|
+
if(process.env.NODE_ENV!=='development'||!alice.current||!bob.current)return;
|
|
17
|
+
const settings=defaultMessagingSettings();settings.enabled=true;
|
|
18
|
+
const messages:any[]=[];
|
|
19
|
+
const transport=(userId:string):typeof fetch=>async(_request,init)=>{
|
|
20
|
+
const body=JSON.parse(String(init?.body??'{}')),other=userId==='alice'?'bob':'alice';let result:unknown;
|
|
21
|
+
switch(body.operation){
|
|
22
|
+
case 'settings':result={settings};break;
|
|
23
|
+
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;
|
|
24
|
+
case 'list':result={settings,messages:[...messages],hasMore:false,nextCursor:null,conversation:{kind:'direct',canSend:true}};break;
|
|
25
|
+
case 'send':{
|
|
26
|
+
let message=messages.find(m=>m.senderId===userId&&m.clientMessageId===body.clientMessageId);
|
|
27
|
+
if(!message){message={...body,id:String(messages.length+1),senderId:userId,createdAt:new Date().toISOString(),readAt:null};messages.push(message);}
|
|
28
|
+
result={message};break;
|
|
29
|
+
}
|
|
30
|
+
case 'read':for(const m of messages)if(m.senderId!==userId)m.readAt=new Date().toISOString();result={ok:true};break;
|
|
31
|
+
default:return Response.json({error:'This local preview demonstrates text only. Connect your test account to try other features.'},{status:400});
|
|
32
|
+
}
|
|
33
|
+
return Response.json(result);
|
|
34
|
+
};
|
|
35
|
+
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});
|
|
36
|
+
const a=mount(alice.current,'alice'),b=mount(bob.current,'bob');return()=>{a.unmount();b.unmount();};
|
|
37
|
+
},[]);
|
|
38
|
+
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>;
|
|
39
|
+
}
|
|
40
|
+
`
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=messenger-preview-template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module","private":true}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robono/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Provider-neutral server SDK for the Robono Bridge API.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"robono",
|
|
@@ -43,6 +43,14 @@
|
|
|
43
43
|
"./installer": {
|
|
44
44
|
"types": "./dist/messenger-install.d.ts",
|
|
45
45
|
"import": "./dist/messenger-install.js"
|
|
46
|
+
},
|
|
47
|
+
"./node": {
|
|
48
|
+
"types": "./dist/messenger-node.d.ts",
|
|
49
|
+
"import": "./dist/messenger-node.js"
|
|
50
|
+
},
|
|
51
|
+
"./upgrade": {
|
|
52
|
+
"types": "./dist/messenger-upgrade.d.ts",
|
|
53
|
+
"import": "./dist/messenger-upgrade.js"
|
|
46
54
|
}
|
|
47
55
|
},
|
|
48
56
|
"bin": {
|
|
@@ -60,7 +68,8 @@
|
|
|
60
68
|
"SBOM.spdx.json",
|
|
61
69
|
"sql",
|
|
62
70
|
"NOTIFICATIONS.md",
|
|
63
|
-
"SCREENING.md"
|
|
71
|
+
"SCREENING.md",
|
|
72
|
+
"legacy"
|
|
64
73
|
],
|
|
65
74
|
"scripts": {
|
|
66
75
|
"build": "tsc -p tsconfig.json",
|