@solarpunkltd/swarm-chat-js 6.2.7 → 7.0.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/README.md +175 -196
- package/dist/chat.d.ts +106 -0
- package/dist/events.d.ts +80 -0
- package/dist/follower.d.ts +122 -0
- package/dist/history.d.ts +51 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +7 -8
- package/dist/index.js +576 -11
- package/dist/message/entry.d.ts +97 -0
- package/dist/message/index.d.ts +2 -0
- package/dist/message/message.d.ts +90 -0
- package/dist/message.cjs +1 -0
- package/dist/message.js +264 -0
- package/dist/sender.d.ts +62 -0
- package/dist/swarm.d.ts +74 -0
- package/package.json +48 -54
- package/dist/index.d.ts.map +0 -1
- package/dist/index.esm.js +0 -6581
- package/dist/interfaces/chat.d.ts +0 -41
- package/dist/interfaces/chat.d.ts.map +0 -1
- package/dist/interfaces/index.d.ts +0 -3
- package/dist/interfaces/index.d.ts.map +0 -1
- package/dist/interfaces/message.d.ts +0 -31
- package/dist/interfaces/message.d.ts.map +0 -1
- package/dist/lib/constants.d.ts +0 -10
- package/dist/lib/constants.d.ts.map +0 -1
- package/dist/lib/core.d.ts +0 -31
- package/dist/lib/core.d.ts.map +0 -1
- package/dist/lib/history.d.ts +0 -27
- package/dist/lib/history.d.ts.map +0 -1
- package/dist/lib/utils.d.ts +0 -32
- package/dist/lib/utils.d.ts.map +0 -1
- package/dist/scripts/mine.d.ts +0 -3
- package/dist/scripts/mine.d.ts.map +0 -1
- package/dist/swarm-chat-umd.js +0 -11
- package/dist/transports/MessageTransport.d.ts +0 -7
- package/dist/transports/MessageTransport.d.ts.map +0 -1
- package/dist/transports/PollingTransport.d.ts +0 -20
- package/dist/transports/PollingTransport.d.ts.map +0 -1
- package/dist/utils/bee.d.ts +0 -18
- package/dist/utils/bee.d.ts.map +0 -1
- package/dist/utils/common.d.ts +0 -5
- package/dist/utils/common.d.ts.map +0 -1
- package/dist/utils/error.d.ts +0 -8
- package/dist/utils/error.d.ts.map +0 -1
- package/dist/utils/eventEmitter.d.ts +0 -11
- package/dist/utils/eventEmitter.d.ts.map +0 -1
- package/dist/utils/logger.d.ts +0 -12
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/validation.d.ts +0 -18
- package/dist/utils/validation.d.ts.map +0 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* What the server writes: one feed entry per message at indices 0, 1, 2 and on, and one history file per chat that
|
|
4
|
+
* the entries point to. The server builds these and the reader checks their shape. The reader does not check each
|
|
5
|
+
* message's signature, because the server checked it before publishing.
|
|
6
|
+
*/
|
|
7
|
+
/** A feed entry is always one chunk, so bee-js never wraps it. */
|
|
8
|
+
export declare const MAX_ENTRY_BYTES = 4096;
|
|
9
|
+
export declare const historyLinkSchema: z.ZodObject<{
|
|
10
|
+
ref: z.ZodString;
|
|
11
|
+
toSeq: z.ZodNumber;
|
|
12
|
+
}, z.core.$strict>;
|
|
13
|
+
export type HistoryLink = z.infer<typeof historyLinkSchema>;
|
|
14
|
+
export declare const feedEntrySchema: z.ZodObject<{
|
|
15
|
+
v: z.ZodLiteral<7>;
|
|
16
|
+
seq: z.ZodNumber;
|
|
17
|
+
at: z.ZodNumber;
|
|
18
|
+
msg: z.ZodObject<{
|
|
19
|
+
v: z.ZodLiteral<7>;
|
|
20
|
+
topic: z.ZodString;
|
|
21
|
+
id: z.ZodString;
|
|
22
|
+
type: z.ZodEnum<{
|
|
23
|
+
reaction: "reaction";
|
|
24
|
+
text: "text";
|
|
25
|
+
thread: "thread";
|
|
26
|
+
}>;
|
|
27
|
+
target: z.ZodUnion<readonly [z.ZodLiteral<"">, z.ZodString]>;
|
|
28
|
+
text: z.ZodString;
|
|
29
|
+
name: z.ZodString;
|
|
30
|
+
addr: z.ZodString;
|
|
31
|
+
ts: z.ZodNumber;
|
|
32
|
+
sig: z.ZodString;
|
|
33
|
+
}, z.core.$strict>;
|
|
34
|
+
history: z.ZodNullable<z.ZodObject<{
|
|
35
|
+
ref: z.ZodString;
|
|
36
|
+
toSeq: z.ZodNumber;
|
|
37
|
+
}, z.core.$strict>>;
|
|
38
|
+
}, z.core.$strict>;
|
|
39
|
+
export type FeedEntry = z.infer<typeof feedEntrySchema>;
|
|
40
|
+
export declare const historyRowSchema: z.ZodObject<{
|
|
41
|
+
seq: z.ZodNumber;
|
|
42
|
+
at: z.ZodNumber;
|
|
43
|
+
msg: z.ZodObject<{
|
|
44
|
+
v: z.ZodLiteral<7>;
|
|
45
|
+
topic: z.ZodString;
|
|
46
|
+
id: z.ZodString;
|
|
47
|
+
type: z.ZodEnum<{
|
|
48
|
+
reaction: "reaction";
|
|
49
|
+
text: "text";
|
|
50
|
+
thread: "thread";
|
|
51
|
+
}>;
|
|
52
|
+
target: z.ZodUnion<readonly [z.ZodLiteral<"">, z.ZodString]>;
|
|
53
|
+
text: z.ZodString;
|
|
54
|
+
name: z.ZodString;
|
|
55
|
+
addr: z.ZodString;
|
|
56
|
+
ts: z.ZodNumber;
|
|
57
|
+
sig: z.ZodString;
|
|
58
|
+
}, z.core.$strict>;
|
|
59
|
+
}, z.core.$strict>;
|
|
60
|
+
export type HistoryRow = z.infer<typeof historyRowSchema>;
|
|
61
|
+
/** A row that fails its shape is left out and counted, so one bad row never hides the rest of the file. */
|
|
62
|
+
export declare const historyFileSchema: z.ZodObject<{
|
|
63
|
+
v: z.ZodLiteral<7>;
|
|
64
|
+
topic: z.ZodString;
|
|
65
|
+
fromSeq: z.ZodNumber;
|
|
66
|
+
toSeq: z.ZodNumber;
|
|
67
|
+
messages: z.ZodArray<z.ZodUnknown>;
|
|
68
|
+
prev: z.ZodNullable<z.ZodObject<{
|
|
69
|
+
ref: z.ZodString;
|
|
70
|
+
toSeq: z.ZodNumber;
|
|
71
|
+
}, z.core.$strict>>;
|
|
72
|
+
}, z.core.$strict>;
|
|
73
|
+
export interface HistoryFile {
|
|
74
|
+
topic: string;
|
|
75
|
+
fromSeq: number;
|
|
76
|
+
toSeq: number;
|
|
77
|
+
rows: HistoryRow[];
|
|
78
|
+
prev: HistoryLink | null;
|
|
79
|
+
/** Rows left out because they failed their shape, lay outside fromSeq to toSeq, or belonged to another chat. */
|
|
80
|
+
skipped: number;
|
|
81
|
+
}
|
|
82
|
+
export type ReadRefusal = 'not-utf8' | 'not-json' | 'shape' | 'mismatch';
|
|
83
|
+
export type ReadCheck<T> = {
|
|
84
|
+
ok: true;
|
|
85
|
+
value: T;
|
|
86
|
+
} | {
|
|
87
|
+
ok: false;
|
|
88
|
+
reason: ReadRefusal;
|
|
89
|
+
detail: string;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Reads the entry at feed index `index` of chat `topic`. An entry is refused when its `seq` is not that index or its
|
|
93
|
+
* message belongs to another chat, since either would put a message in the wrong place.
|
|
94
|
+
*/
|
|
95
|
+
export declare function parseFeedEntry(payload: Uint8Array, index: number, topic: string): ReadCheck<FeedEntry>;
|
|
96
|
+
/** Reads a history file of chat `topic` that `link` pointed to. */
|
|
97
|
+
export declare function parseHistoryFile(payload: Uint8Array, topic: string, link: HistoryLink): ReadCheck<HistoryFile>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { PrivateKey } from '@ethersphere/bee-js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* The chat message of version 7: what a browser sends as a GSOC payload, what the server verifies before it
|
|
5
|
+
* publishes, and what every feed entry carries back as `msg`. This module is the only place its bytes are built,
|
|
6
|
+
* signed, checked and read, and the server imports it from here rather than keeping a copy.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MESSAGE_VERSION = 7;
|
|
9
|
+
/** Measured on the UTF-8 of the whole JSON object, on top of the per-field caps. */
|
|
10
|
+
export declare const MAX_MESSAGE_BYTES = 2048;
|
|
11
|
+
export declare const MAX_TOPIC_CHARACTERS = 128;
|
|
12
|
+
export declare const MAX_TEXT_CHARACTERS = 500;
|
|
13
|
+
export declare const MAX_NAME_CHARACTERS = 20;
|
|
14
|
+
export declare const MessageType: {
|
|
15
|
+
readonly TEXT: 'text';
|
|
16
|
+
readonly THREAD: 'thread';
|
|
17
|
+
readonly REACTION: 'reaction';
|
|
18
|
+
};
|
|
19
|
+
export type MessageType = (typeof MessageType)[keyof typeof MessageType];
|
|
20
|
+
/**
|
|
21
|
+
* Characters are counted as Unicode code points, never as UTF-16 units, so an emoji outside the basic plane counts
|
|
22
|
+
* once. A composer that shows a count uses this function, so it and the check agree.
|
|
23
|
+
*/
|
|
24
|
+
export declare function countCharacters(value: string): number;
|
|
25
|
+
export declare const chatMessageSchema: z.ZodObject<{
|
|
26
|
+
v: z.ZodLiteral<7>;
|
|
27
|
+
topic: z.ZodString;
|
|
28
|
+
id: z.ZodString;
|
|
29
|
+
type: z.ZodEnum<{
|
|
30
|
+
reaction: "reaction";
|
|
31
|
+
text: "text";
|
|
32
|
+
thread: "thread";
|
|
33
|
+
}>;
|
|
34
|
+
target: z.ZodUnion<readonly [z.ZodLiteral<"">, z.ZodString]>;
|
|
35
|
+
text: z.ZodString;
|
|
36
|
+
name: z.ZodString;
|
|
37
|
+
addr: z.ZodString;
|
|
38
|
+
ts: z.ZodNumber;
|
|
39
|
+
sig: z.ZodString;
|
|
40
|
+
}, z.core.$strict>;
|
|
41
|
+
export type ChatMessage = z.infer<typeof chatMessageSchema>;
|
|
42
|
+
export type UnsignedChatMessage = Omit<ChatMessage, 'sig'>;
|
|
43
|
+
export type ChatMessageRefusal = 'too-large' | 'not-utf8' | 'not-json' | 'shape' | 'signature';
|
|
44
|
+
export type ChatMessageCheck = {
|
|
45
|
+
ok: true;
|
|
46
|
+
message: ChatMessage;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
reason: ChatMessageRefusal;
|
|
50
|
+
detail: string;
|
|
51
|
+
};
|
|
52
|
+
export declare class ChatMessageError extends Error {
|
|
53
|
+
readonly reason: ChatMessageRefusal;
|
|
54
|
+
constructor(reason: ChatMessageRefusal, detail: string);
|
|
55
|
+
}
|
|
56
|
+
/** The exact bytes a sender signs and a verifier recovers the signer from. Their order is the contract. */
|
|
57
|
+
export declare function signedBytes(message: UnsignedChatMessage): Uint8Array;
|
|
58
|
+
/** The payload bytes, with the fields in the contract's order so two senders of one message send one payload. */
|
|
59
|
+
export declare function encodeChatMessage(message: ChatMessage): Uint8Array;
|
|
60
|
+
export declare function newMessageId(): string;
|
|
61
|
+
export interface ChatMessageDraft {
|
|
62
|
+
topic: string;
|
|
63
|
+
type: MessageType;
|
|
64
|
+
/** The id a thread reply or a reaction refers to. Left out for a text message. */
|
|
65
|
+
target?: string;
|
|
66
|
+
text: string;
|
|
67
|
+
name: string;
|
|
68
|
+
/** Made fresh when left out. Given only to rebuild a message whose bytes must not change. */
|
|
69
|
+
id?: string;
|
|
70
|
+
/** The sender's clock in milliseconds, `Date.now()` when left out. */
|
|
71
|
+
ts?: number;
|
|
72
|
+
}
|
|
73
|
+
export interface SignedChatMessage {
|
|
74
|
+
message: ChatMessage;
|
|
75
|
+
bytes: Uint8Array;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Builds and signs a message, and throws a ChatMessageError before signing anything that the server would refuse:
|
|
79
|
+
* a field outside its cap, or a payload over MAX_MESSAGE_BYTES.
|
|
80
|
+
*/
|
|
81
|
+
export declare function createChatMessage(privateKey: PrivateKey | Uint8Array | string, draft: ChatMessageDraft): SignedChatMessage;
|
|
82
|
+
/** Whether `sig` was made over this message's signed bytes by the key behind `addr`. */
|
|
83
|
+
export declare function hasValidSignature(message: ChatMessage): boolean;
|
|
84
|
+
/** Checks a message that arrived as an object, its shape and its signature. */
|
|
85
|
+
export declare function checkChatMessage(value: unknown): ChatMessageCheck;
|
|
86
|
+
/**
|
|
87
|
+
* Checks a message as it arrived on the wire: the byte cap on the raw payload, then UTF-8, JSON, the shape and the
|
|
88
|
+
* signature. Never throws, since the server calls it on whatever anybody writes to the chat's GSOC address.
|
|
89
|
+
*/
|
|
90
|
+
export declare function parseChatMessage(payload: Uint8Array): ChatMessageCheck;
|
package/dist/message.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@ethersphere/bee-js"),t=require("zod");var n=7,r=2048,i=128,a=500,o=20,s={TEXT:`text`,THREAD:`thread`,REACTION:`reaction`};function c(e){let t=0;for(let n of e)t++;return t}function l(e){return t.z.string().regex(RegExp(`^[0-9a-f]{${e}}$`),`expected ${e} lowercase hex characters`)}function u(e,n){return t.z.string().refine(t=>{let r=c(t);return r>=e&&r<=n},`expected ${e} to ${n} characters`)}var d=l(32),f=t.z.strictObject({v:t.z.literal(7),topic:u(1,128),id:d,type:t.z.enum([s.TEXT,s.THREAD,s.REACTION]),target:t.z.union([t.z.literal(``),d]),text:u(1,500),name:u(1,20),addr:l(40),ts:t.z.number().int().nonnegative().max(2**53-1),sig:l(130)}).refine(e=>e.type===s.TEXT==(e.target===``),{message:`a text message has no target, a thread reply or a reaction names one`,path:[`target`]}),p=class extends Error{constructor(e,t){super(`chat message refused (${e}): ${t}`),this.reason=e,this.name=`ChatMessageError`}},m=new TextEncoder,h=new TextDecoder(`utf-8`,{fatal:!0});function g(e){return m.encode(JSON.stringify([e.v,e.topic,e.id,e.type,e.target,e.text,e.name,e.addr,e.ts]))}function _(e){let t={v:e.v,topic:e.topic,id:e.id,type:e.type,target:e.target,text:e.text,name:e.name,addr:e.addr,ts:e.ts,sig:e.sig};return m.encode(JSON.stringify(t))}function v(){let e=crypto.getRandomValues(new Uint8Array(16));return Array.from(e,e=>e.toString(16).padStart(2,`0`)).join(``)}function y(e){return e.issues.map(e=>`${e.path.join(`.`)||`message`}: ${e.message}`).join(`, `)}function b(t,n){let i=t instanceof e.PrivateKey?t:new e.PrivateKey(t),a={v:7,topic:n.topic,id:n.id??v(),type:n.type,target:n.target??``,text:n.text,name:n.name,addr:i.publicKey().address().toHex(),ts:n.ts??Date.now()},o={...a,sig:`0`.repeat(130)},s=f.safeParse(o);if(!s.success)throw new p(`shape`,y(s.error));let c=_(o).byteLength;if(c>2048)throw new p(`too-large`,`${c} bytes, the cap is ${r}`);let l={...a,sig:i.sign(g(a)).toHex()};return{message:l,bytes:_(l)}}function x(t){try{return new e.Signature(t.sig).recoverPublicKey(g(t)).address().toHex()===t.addr}catch{return!1}}function S(e){let t=f.safeParse(e);return t.success?x(t.data)?{ok:!0,message:t.data}:{ok:!1,reason:`signature`,detail:`the signature was not made by addr over these fields`}:{ok:!1,reason:`shape`,detail:y(t.error)}}function C(e){if(e.byteLength>2048)return{ok:!1,reason:`too-large`,detail:`${e.byteLength} bytes, the cap is ${r}`};let t;try{t=h.decode(e)}catch{return{ok:!1,reason:`not-utf8`,detail:`the payload is not valid UTF-8`}}let n;try{n=JSON.parse(t)}catch{return{ok:!1,reason:`not-json`,detail:`the payload is not JSON`}}return S(n)}var w=4096,T=t.z.string().regex(/^[0-9a-f]{64}([0-9a-f]{64})?$/,`expected a 64 or 128 hex character reference`),E=t.z.number().int().nonnegative().max(2**53-1),D=t.z.strictObject({ref:T,toSeq:E}),O=t.z.strictObject({v:t.z.literal(7),seq:E,at:E,msg:f,history:D.nullable()}),k=t.z.strictObject({seq:E,at:E,msg:f}),A=t.z.strictObject({v:t.z.literal(7),topic:t.z.string().min(1),fromSeq:E,toSeq:E,messages:t.z.array(t.z.unknown()),prev:D.nullable()}).refine(e=>e.fromSeq<=e.toSeq,{message:`fromSeq is after toSeq`,path:[`fromSeq`]}),j=new TextDecoder(`utf-8`,{fatal:!0});function M(e){let t;try{t=j.decode(e)}catch{return{ok:!1,reason:`not-utf8`,detail:`the payload is not valid UTF-8`}}try{return{ok:!0,value:JSON.parse(t)}}catch{return{ok:!1,reason:`not-json`,detail:`the payload is not JSON`}}}function N(e){return e.issues.map(e=>`${e.path.join(`.`)||`value`}: ${e.message}`).join(`, `)}function P(e,t,n){let r=M(e);if(!r.ok)return r;let i=O.safeParse(r.value);return i.success?i.data.seq===t?i.data.msg.topic===n?{ok:!0,value:i.data}:{ok:!1,reason:`mismatch`,detail:`the message belongs to another chat`}:{ok:!1,reason:`mismatch`,detail:`seq ${i.data.seq} at index ${t}`}:{ok:!1,reason:`shape`,detail:N(i.error)}}function F(e,t,n){let r=M(e);if(!r.ok)return r;let i=A.safeParse(r.value);if(!i.success)return{ok:!1,reason:`shape`,detail:N(i.error)};if(i.data.topic!==t||i.data.toSeq!==n.toSeq)return{ok:!1,reason:`mismatch`,detail:`the file is not the one its link names`};let{fromSeq:a,toSeq:o,prev:s}=i.data,c=[],l=0;for(let e of i.data.messages){let n=k.safeParse(e);n.success&&n.data.seq>=a&&n.data.seq<=o&&n.data.msg.topic===t?c.push(n.data):l++}return{ok:!0,value:{topic:t,fromSeq:a,toSeq:o,rows:c,prev:s,skipped:l}}}exports.ChatMessageError=p,exports.MAX_ENTRY_BYTES=w,exports.MAX_MESSAGE_BYTES=r,exports.MAX_NAME_CHARACTERS=o,exports.MAX_TEXT_CHARACTERS=a,exports.MAX_TOPIC_CHARACTERS=i,exports.MESSAGE_VERSION=n,exports.MessageType=s,exports.chatMessageSchema=f,exports.checkChatMessage=S,exports.countCharacters=c,exports.createChatMessage=b,exports.encodeChatMessage=_,exports.feedEntrySchema=O,exports.hasValidSignature=x,exports.historyFileSchema=A,exports.historyLinkSchema=D,exports.historyRowSchema=k,exports.newMessageId=v,exports.parseChatMessage=C,exports.parseFeedEntry=P,exports.parseHistoryFile=F,exports.signedBytes=g;
|
package/dist/message.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { PrivateKey as e, Signature as t } from "@ethersphere/bee-js";
|
|
2
|
+
import { z as n } from "zod";
|
|
3
|
+
//#region src/message/message.ts
|
|
4
|
+
var r = 7, i = 2048, a = 128, o = 500, s = 20, c = {
|
|
5
|
+
TEXT: "text",
|
|
6
|
+
THREAD: "thread",
|
|
7
|
+
REACTION: "reaction"
|
|
8
|
+
};
|
|
9
|
+
function l(e) {
|
|
10
|
+
let t = 0;
|
|
11
|
+
for (let n of e) t++;
|
|
12
|
+
return t;
|
|
13
|
+
}
|
|
14
|
+
function u(e) {
|
|
15
|
+
return n.string().regex(RegExp(`^[0-9a-f]{${e}}$`), `expected ${e} lowercase hex characters`);
|
|
16
|
+
}
|
|
17
|
+
function d(e, t) {
|
|
18
|
+
return n.string().refine((n) => {
|
|
19
|
+
let r = l(n);
|
|
20
|
+
return r >= e && r <= t;
|
|
21
|
+
}, `expected ${e} to ${t} characters`);
|
|
22
|
+
}
|
|
23
|
+
var f = u(32), p = n.strictObject({
|
|
24
|
+
v: n.literal(7),
|
|
25
|
+
topic: d(1, 128),
|
|
26
|
+
id: f,
|
|
27
|
+
type: n.enum([
|
|
28
|
+
c.TEXT,
|
|
29
|
+
c.THREAD,
|
|
30
|
+
c.REACTION
|
|
31
|
+
]),
|
|
32
|
+
target: n.union([n.literal(""), f]),
|
|
33
|
+
text: d(1, 500),
|
|
34
|
+
name: d(1, 20),
|
|
35
|
+
addr: u(40),
|
|
36
|
+
ts: n.number().int().nonnegative().max(2 ** 53 - 1),
|
|
37
|
+
sig: u(130)
|
|
38
|
+
}).refine((e) => e.type === c.TEXT == (e.target === ""), {
|
|
39
|
+
message: "a text message has no target, a thread reply or a reaction names one",
|
|
40
|
+
path: ["target"]
|
|
41
|
+
}), m = class extends Error {
|
|
42
|
+
constructor(e, t) {
|
|
43
|
+
super(`chat message refused (${e}): ${t}`), this.reason = e, this.name = "ChatMessageError";
|
|
44
|
+
}
|
|
45
|
+
}, h = new TextEncoder(), g = new TextDecoder("utf-8", { fatal: !0 });
|
|
46
|
+
function _(e) {
|
|
47
|
+
return h.encode(JSON.stringify([
|
|
48
|
+
e.v,
|
|
49
|
+
e.topic,
|
|
50
|
+
e.id,
|
|
51
|
+
e.type,
|
|
52
|
+
e.target,
|
|
53
|
+
e.text,
|
|
54
|
+
e.name,
|
|
55
|
+
e.addr,
|
|
56
|
+
e.ts
|
|
57
|
+
]));
|
|
58
|
+
}
|
|
59
|
+
function v(e) {
|
|
60
|
+
let t = {
|
|
61
|
+
v: e.v,
|
|
62
|
+
topic: e.topic,
|
|
63
|
+
id: e.id,
|
|
64
|
+
type: e.type,
|
|
65
|
+
target: e.target,
|
|
66
|
+
text: e.text,
|
|
67
|
+
name: e.name,
|
|
68
|
+
addr: e.addr,
|
|
69
|
+
ts: e.ts,
|
|
70
|
+
sig: e.sig
|
|
71
|
+
};
|
|
72
|
+
return h.encode(JSON.stringify(t));
|
|
73
|
+
}
|
|
74
|
+
function y() {
|
|
75
|
+
let e = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
76
|
+
return Array.from(e, (e) => e.toString(16).padStart(2, "0")).join("");
|
|
77
|
+
}
|
|
78
|
+
function b(e) {
|
|
79
|
+
return e.issues.map((e) => `${e.path.join(".") || "message"}: ${e.message}`).join(", ");
|
|
80
|
+
}
|
|
81
|
+
function x(t, n) {
|
|
82
|
+
let r = t instanceof e ? t : new e(t), a = {
|
|
83
|
+
v: 7,
|
|
84
|
+
topic: n.topic,
|
|
85
|
+
id: n.id ?? y(),
|
|
86
|
+
type: n.type,
|
|
87
|
+
target: n.target ?? "",
|
|
88
|
+
text: n.text,
|
|
89
|
+
name: n.name,
|
|
90
|
+
addr: r.publicKey().address().toHex(),
|
|
91
|
+
ts: n.ts ?? Date.now()
|
|
92
|
+
}, o = {
|
|
93
|
+
...a,
|
|
94
|
+
sig: "0".repeat(130)
|
|
95
|
+
}, s = p.safeParse(o);
|
|
96
|
+
if (!s.success) throw new m("shape", b(s.error));
|
|
97
|
+
let c = v(o).byteLength;
|
|
98
|
+
if (c > 2048) throw new m("too-large", `${c} bytes, the cap is ${i}`);
|
|
99
|
+
let l = {
|
|
100
|
+
...a,
|
|
101
|
+
sig: r.sign(_(a)).toHex()
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
message: l,
|
|
105
|
+
bytes: v(l)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function S(e) {
|
|
109
|
+
try {
|
|
110
|
+
return new t(e.sig).recoverPublicKey(_(e)).address().toHex() === e.addr;
|
|
111
|
+
} catch {
|
|
112
|
+
return !1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function C(e) {
|
|
116
|
+
let t = p.safeParse(e);
|
|
117
|
+
return t.success ? S(t.data) ? {
|
|
118
|
+
ok: !0,
|
|
119
|
+
message: t.data
|
|
120
|
+
} : {
|
|
121
|
+
ok: !1,
|
|
122
|
+
reason: "signature",
|
|
123
|
+
detail: "the signature was not made by addr over these fields"
|
|
124
|
+
} : {
|
|
125
|
+
ok: !1,
|
|
126
|
+
reason: "shape",
|
|
127
|
+
detail: b(t.error)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function w(e) {
|
|
131
|
+
if (e.byteLength > 2048) return {
|
|
132
|
+
ok: !1,
|
|
133
|
+
reason: "too-large",
|
|
134
|
+
detail: `${e.byteLength} bytes, the cap is ${i}`
|
|
135
|
+
};
|
|
136
|
+
let t;
|
|
137
|
+
try {
|
|
138
|
+
t = g.decode(e);
|
|
139
|
+
} catch {
|
|
140
|
+
return {
|
|
141
|
+
ok: !1,
|
|
142
|
+
reason: "not-utf8",
|
|
143
|
+
detail: "the payload is not valid UTF-8"
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
let n;
|
|
147
|
+
try {
|
|
148
|
+
n = JSON.parse(t);
|
|
149
|
+
} catch {
|
|
150
|
+
return {
|
|
151
|
+
ok: !1,
|
|
152
|
+
reason: "not-json",
|
|
153
|
+
detail: "the payload is not JSON"
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return C(n);
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/message/entry.ts
|
|
160
|
+
var T = 4096, E = n.string().regex(/^[0-9a-f]{64}([0-9a-f]{64})?$/, "expected a 64 or 128 hex character reference"), D = n.number().int().nonnegative().max(2 ** 53 - 1), O = n.strictObject({
|
|
161
|
+
ref: E,
|
|
162
|
+
toSeq: D
|
|
163
|
+
}), k = n.strictObject({
|
|
164
|
+
v: n.literal(7),
|
|
165
|
+
seq: D,
|
|
166
|
+
at: D,
|
|
167
|
+
msg: p,
|
|
168
|
+
history: O.nullable()
|
|
169
|
+
}), A = n.strictObject({
|
|
170
|
+
seq: D,
|
|
171
|
+
at: D,
|
|
172
|
+
msg: p
|
|
173
|
+
}), j = n.strictObject({
|
|
174
|
+
v: n.literal(7),
|
|
175
|
+
topic: n.string().min(1),
|
|
176
|
+
fromSeq: D,
|
|
177
|
+
toSeq: D,
|
|
178
|
+
messages: n.array(n.unknown()),
|
|
179
|
+
prev: O.nullable()
|
|
180
|
+
}).refine((e) => e.fromSeq <= e.toSeq, {
|
|
181
|
+
message: "fromSeq is after toSeq",
|
|
182
|
+
path: ["fromSeq"]
|
|
183
|
+
}), M = new TextDecoder("utf-8", { fatal: !0 });
|
|
184
|
+
function N(e) {
|
|
185
|
+
let t;
|
|
186
|
+
try {
|
|
187
|
+
t = M.decode(e);
|
|
188
|
+
} catch {
|
|
189
|
+
return {
|
|
190
|
+
ok: !1,
|
|
191
|
+
reason: "not-utf8",
|
|
192
|
+
detail: "the payload is not valid UTF-8"
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
return {
|
|
197
|
+
ok: !0,
|
|
198
|
+
value: JSON.parse(t)
|
|
199
|
+
};
|
|
200
|
+
} catch {
|
|
201
|
+
return {
|
|
202
|
+
ok: !1,
|
|
203
|
+
reason: "not-json",
|
|
204
|
+
detail: "the payload is not JSON"
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function P(e) {
|
|
209
|
+
return e.issues.map((e) => `${e.path.join(".") || "value"}: ${e.message}`).join(", ");
|
|
210
|
+
}
|
|
211
|
+
function F(e, t, n) {
|
|
212
|
+
let r = N(e);
|
|
213
|
+
if (!r.ok) return r;
|
|
214
|
+
let i = k.safeParse(r.value);
|
|
215
|
+
return i.success ? i.data.seq === t ? i.data.msg.topic === n ? {
|
|
216
|
+
ok: !0,
|
|
217
|
+
value: i.data
|
|
218
|
+
} : {
|
|
219
|
+
ok: !1,
|
|
220
|
+
reason: "mismatch",
|
|
221
|
+
detail: "the message belongs to another chat"
|
|
222
|
+
} : {
|
|
223
|
+
ok: !1,
|
|
224
|
+
reason: "mismatch",
|
|
225
|
+
detail: `seq ${i.data.seq} at index ${t}`
|
|
226
|
+
} : {
|
|
227
|
+
ok: !1,
|
|
228
|
+
reason: "shape",
|
|
229
|
+
detail: P(i.error)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function I(e, t, n) {
|
|
233
|
+
let r = N(e);
|
|
234
|
+
if (!r.ok) return r;
|
|
235
|
+
let i = j.safeParse(r.value);
|
|
236
|
+
if (!i.success) return {
|
|
237
|
+
ok: !1,
|
|
238
|
+
reason: "shape",
|
|
239
|
+
detail: P(i.error)
|
|
240
|
+
};
|
|
241
|
+
if (i.data.topic !== t || i.data.toSeq !== n.toSeq) return {
|
|
242
|
+
ok: !1,
|
|
243
|
+
reason: "mismatch",
|
|
244
|
+
detail: "the file is not the one its link names"
|
|
245
|
+
};
|
|
246
|
+
let { fromSeq: a, toSeq: o, prev: s } = i.data, c = [], l = 0;
|
|
247
|
+
for (let e of i.data.messages) {
|
|
248
|
+
let n = A.safeParse(e);
|
|
249
|
+
n.success && n.data.seq >= a && n.data.seq <= o && n.data.msg.topic === t ? c.push(n.data) : l++;
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
ok: !0,
|
|
253
|
+
value: {
|
|
254
|
+
topic: t,
|
|
255
|
+
fromSeq: a,
|
|
256
|
+
toSeq: o,
|
|
257
|
+
rows: c,
|
|
258
|
+
prev: s,
|
|
259
|
+
skipped: l
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
//#endregion
|
|
264
|
+
export { m as ChatMessageError, T as MAX_ENTRY_BYTES, i as MAX_MESSAGE_BYTES, s as MAX_NAME_CHARACTERS, o as MAX_TEXT_CHARACTERS, a as MAX_TOPIC_CHARACTERS, r as MESSAGE_VERSION, c as MessageType, p as chatMessageSchema, C as checkChatMessage, l as countCharacters, x as createChatMessage, v as encodeChatMessage, k as feedEntrySchema, S as hasValidSignature, j as historyFileSchema, O as historyLinkSchema, A as historyRowSchema, y as newMessageId, w as parseChatMessage, F as parseFeedEntry, I as parseHistoryFile, _ as signedBytes };
|
package/dist/sender.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { PrivateKey } from '@ethersphere/bee-js';
|
|
2
|
+
import { type ChatMessage, type ChatMessageDraft } from './message/index.js';
|
|
3
|
+
/** Writes one payload to the chat's GSOC address. Rejects when the write did not reach the node. */
|
|
4
|
+
export type GsocWrite = (payload: Uint8Array) => Promise<void>;
|
|
5
|
+
export interface SenderSettings {
|
|
6
|
+
/** How long a written message may go unseen on the chat feed before its identical bytes are written again. */
|
|
7
|
+
resendAfterMs: number;
|
|
8
|
+
/** Resends after the first write, before the message is failed. */
|
|
9
|
+
maxResends: number;
|
|
10
|
+
/** Taps on one reaction inside this window become one net toggle. */
|
|
11
|
+
reactionWindowMs: number;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the reader can see the chat feed now. While it cannot, the resends and the give-up wait, since a message
|
|
14
|
+
* the server published would be failed only because this viewer could not see it.
|
|
15
|
+
*/
|
|
16
|
+
isLive: () => boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare const DEFAULT_SENDER_SETTINGS: SenderSettings;
|
|
19
|
+
export interface SenderEvents {
|
|
20
|
+
/** Built and signed, and about to be written. */
|
|
21
|
+
pending(message: ChatMessage): void;
|
|
22
|
+
/** The first write that the node accepted. */
|
|
23
|
+
written(message: ChatMessage): void;
|
|
24
|
+
/** Every resend used up, or a write refused in a way a resend cannot mend. `retry` sends it again. */
|
|
25
|
+
failed(message: ChatMessage, error: unknown): void;
|
|
26
|
+
/** Read back from the chat feed. */
|
|
27
|
+
confirmed(message: ChatMessage): void;
|
|
28
|
+
}
|
|
29
|
+
export type SendDraft = Omit<ChatMessageDraft, 'name' | 'ts' | 'id'>;
|
|
30
|
+
/**
|
|
31
|
+
* Sends signed messages as GSOC writes and keeps each one pending until the chat feed shows it. A pending message is
|
|
32
|
+
* written again with the identical bytes, so the server can tell a resend from a new message by its id.
|
|
33
|
+
*/
|
|
34
|
+
export declare class Sender {
|
|
35
|
+
private readonly key;
|
|
36
|
+
private readonly name;
|
|
37
|
+
private readonly write;
|
|
38
|
+
private readonly events;
|
|
39
|
+
private readonly settings;
|
|
40
|
+
private readonly outgoing;
|
|
41
|
+
private readonly reactions;
|
|
42
|
+
private stopped;
|
|
43
|
+
constructor(key: PrivateKey, name: string, write: GsocWrite, events: SenderEvents, settings?: SenderSettings);
|
|
44
|
+
get address(): string;
|
|
45
|
+
/** Builds, signs and writes a message. Throws a ChatMessageError, and writes nothing, for one the server would refuse. */
|
|
46
|
+
send(draft: SendDraft): ChatMessage;
|
|
47
|
+
/**
|
|
48
|
+
* One tap on a reaction. A reaction sent again takes it back, so taps inside one window cancel in pairs and only an
|
|
49
|
+
* odd count sends anything, once, when the window closes.
|
|
50
|
+
*/
|
|
51
|
+
react(topic: string, target: string, emoji: string): void;
|
|
52
|
+
/** Called for every message the chat feed shows. Ends the wait for one this sender wrote. */
|
|
53
|
+
confirm(message: ChatMessage): void;
|
|
54
|
+
/** Writes a failed or still pending message again, with the same bytes, and starts its resends over. */
|
|
55
|
+
retry(id: string): boolean;
|
|
56
|
+
isPending(id: string): boolean;
|
|
57
|
+
/** Cancels every resend and every open reaction window. Messages still pending stay unconfirmed. */
|
|
58
|
+
stop(): void;
|
|
59
|
+
private attempt;
|
|
60
|
+
private scheduleNext;
|
|
61
|
+
private clearTimer;
|
|
62
|
+
}
|
package/dist/swarm.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type Bee } from '@ethersphere/bee-js';
|
|
2
|
+
import type { GsocWrite } from './sender.js';
|
|
3
|
+
/** Everything the reader asks of Swarm, so the reader can be driven by a fake in tests. */
|
|
4
|
+
export interface ChatSource {
|
|
5
|
+
/**
|
|
6
|
+
* The payload at one feed index, or null when Bee answered 404. Rejects with an UnreadableSlotError when a chunk was
|
|
7
|
+
* served that fails its own check, and with anything else when the gateway failed.
|
|
8
|
+
*/
|
|
9
|
+
readSlot(index: number): Promise<Uint8Array | null>;
|
|
10
|
+
/**
|
|
11
|
+
* Bee's head lookup: the newest index it found and its payload, or null when it answered that nothing is there.
|
|
12
|
+
* Rejects with a HeadLookupTimeoutError when it ran out of time, and with anything else when the gateway failed.
|
|
13
|
+
*/
|
|
14
|
+
readHead(): Promise<{
|
|
15
|
+
index: number;
|
|
16
|
+
payload: Uint8Array;
|
|
17
|
+
} | null>;
|
|
18
|
+
/** A history file's bytes. Rejects when it cannot be read. */
|
|
19
|
+
readFile(reference: string): Promise<Uint8Array>;
|
|
20
|
+
}
|
|
21
|
+
/** Bee's head lookup did not answer within its timeout, which on a long chat or a busy gateway is ordinary. */
|
|
22
|
+
export declare class HeadLookupTimeoutError extends Error {
|
|
23
|
+
constructor(options: {
|
|
24
|
+
cause: unknown;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A slot the gateway served whose chunk failed its own check, such as a signature that is not the owner's. A fact
|
|
29
|
+
* about that one slot and not about the gateway, so it never holds the reader back from the slots after it.
|
|
30
|
+
*/
|
|
31
|
+
export declare class UnreadableSlotError extends Error {
|
|
32
|
+
readonly index: number;
|
|
33
|
+
constructor(index: number, options: {
|
|
34
|
+
cause: unknown;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export interface SwarmTimeouts {
|
|
38
|
+
/** A read of one feed slot. */
|
|
39
|
+
slotReadMs: number;
|
|
40
|
+
/** The head lookup and a history file download, which are slower by nature. */
|
|
41
|
+
feedReadMs: number;
|
|
42
|
+
/** One GSOC write. */
|
|
43
|
+
writeMs: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether Bee said the chunk is not there. A 404 means no peer gave it within Bee's retry budget, which at the live
|
|
47
|
+
* edge is the ordinary "not written yet" and elsewhere a slow read. A Bee 2.6 cluster answers 500 on `GET /chunks` for
|
|
48
|
+
* a slot never written, measured on the test bed, so a 500 counts too, as bee-js's own `isRetrievable` counts it.
|
|
49
|
+
* Neither ever means the chat has ended, and a read that is wrong about "not there" is asked again on the next poll.
|
|
50
|
+
* Every other failure, a timeout, an abort, a refused connection or a 502, 503 or 504, is the gateway failing.
|
|
51
|
+
*/
|
|
52
|
+
export declare function isAbsent(error: unknown): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* One slot read, sorted into what it means: the payload, null for a slot that is not there, an UnreadableSlotError for
|
|
55
|
+
* a chunk served that fails its own check, and a rejection for the gateway failing. Shared by the Bee reader and the
|
|
56
|
+
* tests' fake, so both sort failures the same way.
|
|
57
|
+
*/
|
|
58
|
+
export declare function readSlotThrough(index: number, read: () => Promise<Uint8Array>): Promise<Uint8Array | null>;
|
|
59
|
+
/**
|
|
60
|
+
* Reads a chat feed through bee-js's feed reader with an explicit index, which reads `GET /chunks/{address}` and
|
|
61
|
+
* answers 404 only when the chunk could not be found, where `/soc/{owner}/{id}` answers 404 for any failure at all.
|
|
62
|
+
*/
|
|
63
|
+
export declare function beeChatSource(bee: Bee, owner: string, chatTopic: string, timeouts: SwarmTimeouts): ChatSource;
|
|
64
|
+
/**
|
|
65
|
+
* A batch id for a gateway that stamps what is written through it and ignores the one it is sent. Bee refuses a
|
|
66
|
+
* write with no batch id at all, so something well formed has to go. The value 6.x sent, kept so a gateway that
|
|
67
|
+
* recognises it keeps doing so.
|
|
68
|
+
*/
|
|
69
|
+
export declare const GATEWAY_STAMPS_ITSELF: string;
|
|
70
|
+
/**
|
|
71
|
+
* Writes to the chat's inbox: one GSOC address shared by every chat, signed with the mined key every sender holds.
|
|
72
|
+
* Never deferred and never tagged, so each write is pushed at once and two different messages never collide.
|
|
73
|
+
*/
|
|
74
|
+
export declare function beeGsocWrite(bee: Bee, stamp: string, gsocKey: string, gsocTopic: string, timeouts: SwarmTimeouts): GsocWrite;
|