@robono/server 0.15.0 → 0.17.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 +11 -1
- package/NOTIFICATIONS.md +149 -0
- package/README.md +8 -1
- package/SBOM.spdx.json +8 -8
- package/SCREENING.md +73 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/managed-media.d.ts.map +1 -1
- package/dist/managed-media.js +2 -0
- package/dist/managed-media.js.map +1 -1
- package/dist/messaging-notification-postgres.d.ts +7 -0
- package/dist/messaging-notification-postgres.d.ts.map +1 -0
- package/dist/messaging-notification-postgres.js +32 -0
- package/dist/messaging-notification-postgres.js.map +1 -0
- package/dist/messaging-notification-providers.d.ts +26 -0
- package/dist/messaging-notification-providers.d.ts.map +1 -0
- package/dist/messaging-notification-providers.js +107 -0
- package/dist/messaging-notification-providers.js.map +1 -0
- package/dist/messaging-notifications.d.ts +66 -0
- package/dist/messaging-notifications.d.ts.map +1 -0
- package/dist/messaging-notifications.js +128 -0
- package/dist/messaging-notifications.js.map +1 -0
- package/dist/messaging.d.ts +7 -0
- package/dist/messaging.d.ts.map +1 -1
- package/dist/messaging.js +40 -2
- package/dist/messaging.js.map +1 -1
- package/dist/messenger.d.ts +53 -0
- package/dist/messenger.d.ts.map +1 -0
- package/dist/messenger.js +89 -0
- package/dist/messenger.js.map +1 -0
- package/dist/screening.d.ts +53 -0
- package/dist/screening.d.ts.map +1 -0
- package/dist/screening.js +52 -0
- package/dist/screening.js.map +1 -0
- package/package.json +5 -2
- package/sql/messaging-notifications.sql +41 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { RobonoError } from './errors.js';
|
|
3
|
+
/** Server-only. Existing connected networks use their Robono Processing API key. */
|
|
4
|
+
export class RobonoScreening {
|
|
5
|
+
options;
|
|
6
|
+
url;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
const u = new URL(options.baseUrl ?? 'https://api.robono.com/v1/screening');
|
|
10
|
+
if (u.protocol !== 'https:' || u.username || u.password || !options.apiKey.trim())
|
|
11
|
+
throw Error('Use HTTPS and a server API key.');
|
|
12
|
+
this.url = u.href;
|
|
13
|
+
}
|
|
14
|
+
async request(operation, body) {
|
|
15
|
+
const r = await (this.options.fetch ?? fetch)(this.url, { method: 'POST', redirect: 'error', signal: AbortSignal.timeout(30000), headers: { authorization: 'Bearer ' + this.options.apiKey, 'content-type': 'application/json' }, body: JSON.stringify({ ...body, operation }) });
|
|
16
|
+
const data = await r.json().catch(() => null);
|
|
17
|
+
if (!r.ok)
|
|
18
|
+
throw new RobonoError(data?.error?.message ?? 'Screening request failed.', { status: r.status, code: data?.error?.code ?? 'screening_unavailable', retryable: r.status === 429 || r.status >= 500 });
|
|
19
|
+
if (!data?.id || !['uploading', 'queued', 'processing', 'allowed', 'blocked', 'failed'].includes(data.state))
|
|
20
|
+
throw Error('Invalid screening response');
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
23
|
+
prepare(requestId, file) { return this.request('prepare', { requestId, file }); }
|
|
24
|
+
submit(id) { return this.request('submit', { id }); }
|
|
25
|
+
status(id) { return this.request('status', { id }); }
|
|
26
|
+
/** Upload exactly the bytes to be published. Returns a job; never treats pending as approved. */
|
|
27
|
+
async upload(requestId, file, bytes) {
|
|
28
|
+
if (file.byteSize !== bytes.byteLength)
|
|
29
|
+
throw Error('File size does not match the supplied bytes.');
|
|
30
|
+
const digest = createHash('sha256').update(bytes).digest('hex');
|
|
31
|
+
const prepared = await this.prepare(requestId, file);
|
|
32
|
+
if (prepared.sha256 && prepared.sha256 !== digest)
|
|
33
|
+
throw Error('This request ID belongs to different file contents.');
|
|
34
|
+
if (prepared.upload) {
|
|
35
|
+
const destination = new URL(prepared.upload.url);
|
|
36
|
+
if (destination.protocol !== 'https:' || destination.username || destination.password)
|
|
37
|
+
throw Error('Invalid private upload URL');
|
|
38
|
+
const r = await (this.options.fetch ?? fetch)(destination, { method: 'PUT', redirect: 'error', headers: prepared.upload.headers, body: bytes, signal: AbortSignal.timeout(120000) });
|
|
39
|
+
if (!r.ok && r.status !== 412)
|
|
40
|
+
throw new RobonoError('Screening upload failed.', { status: r.status, code: 'screening_upload_failed', retryable: true });
|
|
41
|
+
}
|
|
42
|
+
return this.submit(prepared.id);
|
|
43
|
+
}
|
|
44
|
+
/** Call this before releasing bytes stored by your own application. */
|
|
45
|
+
async assertAllowed(id, bytes) {
|
|
46
|
+
const result = await this.status(id);
|
|
47
|
+
if (result.state !== 'allowed' || !result.sha256 || createHash('sha256').update(bytes).digest('hex') !== result.sha256)
|
|
48
|
+
throw new RobonoError('This file has not been approved for delivery.', { status: 409, code: result.state === 'blocked' ? 'content_blocked' : 'screening_not_approved', retryable: result.state === 'queued' || result.state === 'processing' });
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=screening.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screening.js","sourceRoot":"","sources":["../src/screening.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robono/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Provider-neutral server SDK for the Robono Bridge API.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"robono",
|
|
@@ -46,7 +46,10 @@
|
|
|
46
46
|
"README.md",
|
|
47
47
|
"CHANGELOG.md",
|
|
48
48
|
"LICENSE",
|
|
49
|
-
"SBOM.spdx.json"
|
|
49
|
+
"SBOM.spdx.json",
|
|
50
|
+
"sql",
|
|
51
|
+
"NOTIFICATIONS.md",
|
|
52
|
+
"SCREENING.md"
|
|
50
53
|
],
|
|
51
54
|
"scripts": {
|
|
52
55
|
"build": "tsc -p tsconfig.json",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
-- Apply to the CUSTOMER'S PostgreSQL database with the server's database role.
|
|
2
|
+
-- Do not expose this schema through a public/client database API.
|
|
3
|
+
begin;
|
|
4
|
+
create schema if not exists robono_notifications;
|
|
5
|
+
revoke all on schema robono_notifications from public;
|
|
6
|
+
create table if not exists robono_notifications.devices (
|
|
7
|
+
scope text not null,device_id text not null,user_id text not null,
|
|
8
|
+
provider text not null check(provider in ('apns','fcm')),token text not null,
|
|
9
|
+
updated_at timestamptz not null default now(),
|
|
10
|
+
token_hash bytea not null check(octet_length(token_hash)=32),
|
|
11
|
+
primary key(scope,device_id,user_id),unique(scope,provider,token_hash)
|
|
12
|
+
);
|
|
13
|
+
create table if not exists robono_notifications.jobs (
|
|
14
|
+
id bigint generated always as identity primary key,scope text not null,event_id text not null,
|
|
15
|
+
device_id text not null,user_id text not null,provider text not null,token text not null,event jsonb not null,
|
|
16
|
+
state text not null default 'pending' check(state in ('pending','running','sent','skipped','failed')),
|
|
17
|
+
attempt integer not null default 0,next_attempt_at timestamptz not null default now(),
|
|
18
|
+
lease text,lease_until timestamptz,last_code text,finished_at timestamptz,
|
|
19
|
+
unique(scope,event_id,device_id,user_id)
|
|
20
|
+
);
|
|
21
|
+
create index if not exists jobs_ready on robono_notifications.jobs(scope,state,next_attempt_at);
|
|
22
|
+
alter table robono_notifications.devices enable row level security;
|
|
23
|
+
alter table robono_notifications.jobs enable row level security;
|
|
24
|
+
revoke all on all tables in schema robono_notifications from public;
|
|
25
|
+
revoke all on all sequences in schema robono_notifications from public;
|
|
26
|
+
|
|
27
|
+
create or replace function robono_notifications.register_device(s text,d text,u text,p text,t text)
|
|
28
|
+
returns void language plpgsql set search_path=pg_catalog,robono_notifications as $$
|
|
29
|
+
begin
|
|
30
|
+
perform pg_advisory_xact_lock(hashtextextended('robono-notification:'||s,0));
|
|
31
|
+
delete from robono_notifications.devices where scope=s and provider=p and token=t and (device_id<>d or user_id<>u);
|
|
32
|
+
insert into robono_notifications.devices(scope,device_id,user_id,provider,token,token_hash)
|
|
33
|
+
values(s,d,u,p,t,sha256(convert_to(t,'UTF8'))) on conflict(scope,device_id,user_id) do update set provider=p,token=t,token_hash=excluded.token_hash,updated_at=now();
|
|
34
|
+
end $$;
|
|
35
|
+
revoke all on function robono_notifications.register_device(text,text,text,text,text) from public;
|
|
36
|
+
commit;
|
|
37
|
+
|
|
38
|
+
-- Retain completed job identities for seven days to deduplicate delivery retries.
|
|
39
|
+
-- Schedule this cleanup daily with the same private server role:
|
|
40
|
+
-- delete from robono_notifications.jobs where finished_at < now()-interval '7 days';
|
|
41
|
+
-- delete from robono_notifications.devices where updated_at < now()-interval '90 days';
|