Converted go-roots-ws to typescript.
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 nostr:npub10mtatsat7ph6rsq0w8u8npt8d86x4jfr2nqjnvld2439q6f8ugqq0x27hf
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
344
README.md
Normal file
344
README.md
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
# TS-Roots-WS - Nostr WebSocket Transport for TypeScript
|
||||||
|
|
||||||
|
Source: https://git.wisehodl.dev/jay/ts-roots-ws
|
||||||
|
|
||||||
|
Mirror: https://github.com/wisehodl/ts-roots-ws
|
||||||
|
|
||||||
|
## What this library does
|
||||||
|
|
||||||
|
`ts-roots-ws` is a consensus-layer Nostr protocol websocket transport library for TypeScript. It only provides primitives for working with Nostr protocol websocket connection states and messages:
|
||||||
|
|
||||||
|
- WebSocket Connection States
|
||||||
|
- Envelope Structure
|
||||||
|
- Message Validation
|
||||||
|
- Protocol Message Creation
|
||||||
|
- Protocol Message Parsing
|
||||||
|
- Standard Label Handling
|
||||||
|
|
||||||
|
## What this library does not do
|
||||||
|
|
||||||
|
`ts-roots-ws` serves as a foundation for other libraries and applications to implement higher level transport abstractions on top of it, including:
|
||||||
|
|
||||||
|
- Connection Management
|
||||||
|
- Event Loops
|
||||||
|
- Subscription Handling
|
||||||
|
- State Management
|
||||||
|
- Reconnection Logic
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Add `ts-roots-ws` to your project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @wisehodl/roots-ws
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Import the packages:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
encloseEvent,
|
||||||
|
encloseSubscriptionEvent,
|
||||||
|
findEvent,
|
||||||
|
findSubscriptionEvent,
|
||||||
|
getLabel,
|
||||||
|
isStandardLabel
|
||||||
|
} from "@wisehodl/roots-ws/envelope";
|
||||||
|
|
||||||
|
import { ConnectionStatus } from "@wisehodl/roots-ws";
|
||||||
|
import { InvalidJSONError, WrongEnvelopeLabelError } from "@wisehodl/roots-ws/errors";
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Access functions with appropriate namespaces.
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Envelope Creation
|
||||||
|
|
||||||
|
#### Create EVENT envelope
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create an event using ts-roots
|
||||||
|
import { Event } from "@wisehodl/roots/events";
|
||||||
|
|
||||||
|
const event: Event = {
|
||||||
|
id: "abc123",
|
||||||
|
pubkey: "def456",
|
||||||
|
kind: 1,
|
||||||
|
content: "Hello Nostr!",
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [],
|
||||||
|
sig: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert to JSON
|
||||||
|
const eventJSON = JSON.stringify(event);
|
||||||
|
|
||||||
|
// Create envelope
|
||||||
|
const env = encloseEvent(eventJSON);
|
||||||
|
// Result: ["EVENT",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!","created_at":1636394097}]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create subscription EVENT envelope
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create an event using ts-roots
|
||||||
|
import { Event } from "@wisehodl/roots/events";
|
||||||
|
|
||||||
|
const event: Event = {
|
||||||
|
id: "abc123",
|
||||||
|
pubkey: "def456",
|
||||||
|
kind: 1,
|
||||||
|
content: "Hello Nostr!",
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [],
|
||||||
|
sig: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert to JSON
|
||||||
|
const eventJSON = JSON.stringify(event);
|
||||||
|
|
||||||
|
// Create envelope with subscription ID
|
||||||
|
const subID = "sub1";
|
||||||
|
const env = encloseSubscriptionEvent(subID, eventJSON);
|
||||||
|
// Result: ["EVENT","sub1",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!","created_at":1636394097}]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create REQ envelope
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create filters using ts-roots
|
||||||
|
import { Filter } from "@wisehodl/roots/filters";
|
||||||
|
|
||||||
|
const since = Math.floor(Date.now() / 1000) - (24 * 60 * 60);
|
||||||
|
const limit = 50;
|
||||||
|
|
||||||
|
const filter1: Filter = {
|
||||||
|
kinds: [1],
|
||||||
|
limit: limit,
|
||||||
|
since: since
|
||||||
|
};
|
||||||
|
|
||||||
|
const filter2: Filter = {
|
||||||
|
authors: ["def456"]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert to JSON
|
||||||
|
const filter1JSON = JSON.stringify(filter1);
|
||||||
|
const filter2JSON = JSON.stringify(filter2);
|
||||||
|
|
||||||
|
// Create envelope
|
||||||
|
const subID = "sub1";
|
||||||
|
const filtersJSON = [filter1JSON, filter2JSON];
|
||||||
|
const env = encloseReq(subID, filtersJSON);
|
||||||
|
// Result: ["REQ","sub1",{"kinds":[1],"limit":50,"since":1636307697},{"authors":["def456"]}]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create other envelope types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create CLOSE envelope
|
||||||
|
const env1 = encloseClose("sub1");
|
||||||
|
// Result: ["CLOSE","sub1"]
|
||||||
|
|
||||||
|
// Create EOSE envelope
|
||||||
|
const env2 = encloseEOSE("sub1");
|
||||||
|
// Result: ["EOSE","sub1"]
|
||||||
|
|
||||||
|
// Create NOTICE envelope
|
||||||
|
const env3 = encloseNotice("This is a notice");
|
||||||
|
// Result: ["NOTICE","This is a notice"]
|
||||||
|
|
||||||
|
// Create OK envelope
|
||||||
|
const env4 = encloseOK("abc123", true, "Event accepted");
|
||||||
|
// Result: ["OK","abc123",true,"Event accepted"]
|
||||||
|
|
||||||
|
// Create AUTH challenge
|
||||||
|
const env5 = encloseAuthChallenge("random-challenge-string");
|
||||||
|
// Result: ["AUTH","random-challenge-string"]
|
||||||
|
|
||||||
|
// Create AUTH response
|
||||||
|
import { Event } from "@wisehodl/roots/events";
|
||||||
|
|
||||||
|
const authEvent: Event = {
|
||||||
|
id: "abc123",
|
||||||
|
pubkey: "def456",
|
||||||
|
kind: 22242,
|
||||||
|
content: "",
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [],
|
||||||
|
sig: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert to JSON
|
||||||
|
const authEventJSON = JSON.stringify(authEvent);
|
||||||
|
|
||||||
|
// Create envelope
|
||||||
|
const env6 = encloseAuthResponse(authEventJSON);
|
||||||
|
// Result: ["AUTH",{"id":"abc123","pubkey":"def456","kind":22242,"content":"","created_at":1636394097}]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Envelope Parsing
|
||||||
|
|
||||||
|
#### Extract label from envelope
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const env = `["EVENT",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!"}]`;
|
||||||
|
try {
|
||||||
|
const label = getLabel(env);
|
||||||
|
// label: "EVENT"
|
||||||
|
|
||||||
|
// Check if label is standard
|
||||||
|
const isStandard = isStandardLabel(label);
|
||||||
|
// isStandard: true
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Extract event from EVENT envelope
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const env = `["EVENT",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!"}]`;
|
||||||
|
try {
|
||||||
|
const eventObj = findEvent(env);
|
||||||
|
|
||||||
|
// Parse into ts-roots Event if needed
|
||||||
|
import { Event, validate } from "@wisehodl/roots/events";
|
||||||
|
|
||||||
|
// Validate the event
|
||||||
|
try {
|
||||||
|
validate(eventObj as Event);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Invalid event: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now you can access event properties
|
||||||
|
console.log(eventObj.id, eventObj.kind, eventObj.content);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Extract subscription event
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const env = `["EVENT","sub1",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!"}]`;
|
||||||
|
try {
|
||||||
|
const [subID, eventObj] = findSubscriptionEvent(env);
|
||||||
|
|
||||||
|
// Parse into ts-roots Event if needed
|
||||||
|
import { Event } from "@wisehodl/roots/events";
|
||||||
|
|
||||||
|
console.log(`Subscription: ${subID}, Event ID: ${eventObj.id}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Extract subscription request
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const env = `["REQ","sub1",{"kinds":[1],"limit":50},{"authors":["def456"]}]`;
|
||||||
|
try {
|
||||||
|
const [subID, filtersObj] = findReq(env);
|
||||||
|
|
||||||
|
// Parse each filter
|
||||||
|
import { Filter } from "@wisehodl/roots/filters";
|
||||||
|
|
||||||
|
// Now you can use the filter objects
|
||||||
|
filtersObj.forEach((filter, i) => {
|
||||||
|
console.log(`Filter ${i}: `, filter);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Extract other envelope types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Extract OK response
|
||||||
|
const env1 = `["OK","abc123",true,"Event accepted"]`;
|
||||||
|
try {
|
||||||
|
const [eventID, status, message] = findOK(env1);
|
||||||
|
// eventID: "abc123"
|
||||||
|
// status: true
|
||||||
|
// message: "Event accepted"
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract EOSE message
|
||||||
|
const env2 = `["EOSE","sub1"]`;
|
||||||
|
try {
|
||||||
|
const subID = findEOSE(env2);
|
||||||
|
// subID: "sub1"
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract CLOSE message
|
||||||
|
const env3 = `["CLOSE","sub1"]`;
|
||||||
|
try {
|
||||||
|
const subID = findClose(env3);
|
||||||
|
// subID: "sub1"
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract CLOSED message
|
||||||
|
const env4 = `["CLOSED","sub1","Subscription complete"]`;
|
||||||
|
try {
|
||||||
|
const [subID, message] = findClosed(env4);
|
||||||
|
// subID: "sub1"
|
||||||
|
// message: "Subscription complete"
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract NOTICE message
|
||||||
|
const env5 = `["NOTICE","This is a notice"]`;
|
||||||
|
try {
|
||||||
|
const message = findNotice(env5);
|
||||||
|
// message: "This is a notice"
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract AUTH challenge
|
||||||
|
const env6 = `["AUTH","random-challenge-string"]`;
|
||||||
|
try {
|
||||||
|
const challenge = findAuthChallenge(env6);
|
||||||
|
// challenge: "random-challenge-string"
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract AUTH response
|
||||||
|
const env7 = `["AUTH",{"id":"abc123","pubkey":"def456","kind":22242,"content":""}]`;
|
||||||
|
try {
|
||||||
|
const authEvent = findAuthResponse(env7);
|
||||||
|
|
||||||
|
// Parse into ts-roots Event if needed
|
||||||
|
import { Event } from "@wisehodl/roots/events";
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
This library contains a comprehensive suite of unit tests. Run them with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Or for a single run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:run
|
||||||
|
```
|
||||||
1711
package-lock.json
generated
Normal file
1711
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
50
package.json
Normal file
50
package.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "@wisehodl/roots-ws",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Nostr WebSocket transport primitives for TypeScript",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
},
|
||||||
|
"./envelope": {
|
||||||
|
"import": "./dist/envelope/index.js",
|
||||||
|
"types": "./dist/envelope/index.d.ts"
|
||||||
|
},
|
||||||
|
"./errors": {
|
||||||
|
"import": "./dist/errors/index.js",
|
||||||
|
"types": "./dist/errors/index.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"lint": "tsc --noEmit",
|
||||||
|
"format": "prettier -w src",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:run": "vitest run"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
|
"@types/node": "^24.9.1",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^7.1.12",
|
||||||
|
"vitest": "^4.0.2"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"importOrder": [
|
||||||
|
"^@(/)(.*)",
|
||||||
|
"^[./]"
|
||||||
|
],
|
||||||
|
"importOrderSeparation": true,
|
||||||
|
"importOrderSortSpecifiers": true,
|
||||||
|
"plugins": [
|
||||||
|
"@trivago/prettier-plugin-sort-imports"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
262
src/envelope/enclose.test.ts
Normal file
262
src/envelope/enclose.test.ts
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
encloseAuthChallenge,
|
||||||
|
encloseAuthResponse,
|
||||||
|
encloseClose,
|
||||||
|
encloseClosed,
|
||||||
|
encloseEOSE,
|
||||||
|
encloseEvent,
|
||||||
|
encloseNotice,
|
||||||
|
encloseOK,
|
||||||
|
encloseReq,
|
||||||
|
encloseSubscriptionEvent,
|
||||||
|
} from "./enclose";
|
||||||
|
|
||||||
|
describe("encloseEvent", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "empty event",
|
||||||
|
event: "{}",
|
||||||
|
want: '["EVENT",{}]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid json",
|
||||||
|
event: "in[valid,]",
|
||||||
|
want: '["EVENT",in[valid,]]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "populated event",
|
||||||
|
event: '{"id":"abc123","kind":1,"sig":"abc123"}',
|
||||||
|
want: '["EVENT",{"id":"abc123","kind":1,"sig":"abc123"}]',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
const got = encloseEvent(tc.event);
|
||||||
|
expect(got).toEqual(tc.want);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encloseOK", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "successful event",
|
||||||
|
eventID: "abc123",
|
||||||
|
status: true,
|
||||||
|
message: "Event accepted",
|
||||||
|
want: '["OK","abc123",true,"Event accepted"]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rejected event",
|
||||||
|
eventID: "xyz789",
|
||||||
|
status: false,
|
||||||
|
message: "Invalid signature",
|
||||||
|
want: '["OK","xyz789",false,"Invalid signature"]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty message",
|
||||||
|
eventID: "def456",
|
||||||
|
status: true,
|
||||||
|
message: "",
|
||||||
|
want: '["OK","def456",true,""]',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
const got = encloseOK(tc.eventID, tc.status, tc.message);
|
||||||
|
expect(got).toEqual(tc.want);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encloseReq", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "single filter",
|
||||||
|
subID: "sub1",
|
||||||
|
filters: ['{"kinds":[1],"limit":10}'],
|
||||||
|
want: '["REQ","sub1",{"kinds":[1],"limit":10}]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple filters",
|
||||||
|
subID: "sub2",
|
||||||
|
filters: ['{"kinds":[1]}', '{"authors":["abc"]}'],
|
||||||
|
want: '["REQ","sub2",{"kinds":[1]},{"authors":["abc"]}]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no filters",
|
||||||
|
subID: "sub3",
|
||||||
|
filters: [],
|
||||||
|
want: '["REQ","sub3"]',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
const got = encloseReq(tc.subID, tc.filters);
|
||||||
|
expect(got).toEqual(tc.want);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encloseSubscriptionEvent", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "basic event",
|
||||||
|
subID: "sub1",
|
||||||
|
event: '{"id":"abc123","kind":1}',
|
||||||
|
want: '["EVENT","sub1",{"id":"abc123","kind":1}]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty event",
|
||||||
|
subID: "sub2",
|
||||||
|
event: "{}",
|
||||||
|
want: '["EVENT","sub2",{}]',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
const got = encloseSubscriptionEvent(tc.subID, tc.event);
|
||||||
|
expect(got).toEqual(tc.want);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encloseEOSE", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "valid subscription ID",
|
||||||
|
subID: "sub1",
|
||||||
|
want: '["EOSE","sub1"]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty subscription ID",
|
||||||
|
subID: "",
|
||||||
|
want: '["EOSE",""]',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
const got = encloseEOSE(tc.subID);
|
||||||
|
expect(got).toEqual(tc.want);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encloseClose", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "valid subscription ID",
|
||||||
|
subID: "sub1",
|
||||||
|
want: '["CLOSE","sub1"]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty subscription ID",
|
||||||
|
subID: "",
|
||||||
|
want: '["CLOSE",""]',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
const got = encloseClose(tc.subID);
|
||||||
|
expect(got).toEqual(tc.want);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encloseClosed", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "with message",
|
||||||
|
subID: "sub1",
|
||||||
|
message: "Subscription complete",
|
||||||
|
want: '["CLOSED","sub1","Subscription complete"]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty message",
|
||||||
|
subID: "sub2",
|
||||||
|
message: "",
|
||||||
|
want: '["CLOSED","sub2",""]',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
const got = encloseClosed(tc.subID, tc.message);
|
||||||
|
expect(got).toEqual(tc.want);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encloseNotice", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "valid message",
|
||||||
|
message: "This is a notice",
|
||||||
|
want: '["NOTICE","This is a notice"]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty message",
|
||||||
|
message: "",
|
||||||
|
want: '["NOTICE",""]',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
const got = encloseNotice(tc.message);
|
||||||
|
expect(got).toEqual(tc.want);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encloseAuthChallenge", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "valid challenge",
|
||||||
|
challenge: "random-challenge-string",
|
||||||
|
want: '["AUTH","random-challenge-string"]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty challenge",
|
||||||
|
challenge: "",
|
||||||
|
want: '["AUTH",""]',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
const got = encloseAuthChallenge(tc.challenge);
|
||||||
|
expect(got).toEqual(tc.want);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encloseAuthResponse", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "valid event",
|
||||||
|
event: '{"id":"abc123","kind":22242}',
|
||||||
|
want: '["AUTH",{"id":"abc123","kind":22242}]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty event",
|
||||||
|
event: "{}",
|
||||||
|
want: '["AUTH",{}]',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
const got = encloseAuthResponse(tc.event);
|
||||||
|
expect(got).toEqual(tc.want);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
95
src/envelope/enclose.ts
Normal file
95
src/envelope/enclose.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { Envelope } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an EVENT envelope for publishing events.
|
||||||
|
* It wraps the provided event JSON in the format ["EVENT", event].
|
||||||
|
*/
|
||||||
|
export function encloseEvent(event: string): Envelope {
|
||||||
|
return `["EVENT",${event}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an OK envelope acknowledging receipt of an event.
|
||||||
|
* Format: ["OK", eventID, status, message]
|
||||||
|
*/
|
||||||
|
export function encloseOK(
|
||||||
|
eventID: string,
|
||||||
|
status: boolean,
|
||||||
|
message: string,
|
||||||
|
): Envelope {
|
||||||
|
return `["OK","${eventID}",${status},"${message}"]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a REQ envelope for subscription requests.
|
||||||
|
* Format: ["REQ", subID, filter1, filter2, ...]
|
||||||
|
*/
|
||||||
|
export function encloseReq(subID: string, filters: string[]): Envelope {
|
||||||
|
let envelope = `["REQ","${subID}"`;
|
||||||
|
|
||||||
|
for (const filter of filters) {
|
||||||
|
envelope += `,${filter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
envelope += "]";
|
||||||
|
return envelope;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an EVENT envelope for delivering subscription events.
|
||||||
|
* Format: ["EVENT", subID, event]
|
||||||
|
*/
|
||||||
|
export function encloseSubscriptionEvent(
|
||||||
|
subID: string,
|
||||||
|
event: string,
|
||||||
|
): Envelope {
|
||||||
|
return `["EVENT","${subID}",${event}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an EOSE (End of Stored Events) envelope.
|
||||||
|
* Format: ["EOSE", subID]
|
||||||
|
*/
|
||||||
|
export function encloseEOSE(subID: string): Envelope {
|
||||||
|
return `["EOSE","${subID}"]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a CLOSE envelope for ending a subscription.
|
||||||
|
* Format: ["CLOSE", subID]
|
||||||
|
*/
|
||||||
|
export function encloseClose(subID: string): Envelope {
|
||||||
|
return `["CLOSE","${subID}"]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a CLOSED envelope for indicating a terminated subscription.
|
||||||
|
* Format: ["CLOSED", subID, message]
|
||||||
|
*/
|
||||||
|
export function encloseClosed(subID: string, message: string): Envelope {
|
||||||
|
return `["CLOSED","${subID}","${message}"]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a NOTICE envelope for responder messages.
|
||||||
|
* Format: ["NOTICE", message]
|
||||||
|
*/
|
||||||
|
export function encloseNotice(message: string): Envelope {
|
||||||
|
return `["NOTICE","${message}"]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an AUTH challenge envelope.
|
||||||
|
* Format: ["AUTH", challenge]
|
||||||
|
*/
|
||||||
|
export function encloseAuthChallenge(challenge: string): Envelope {
|
||||||
|
return `["AUTH","${challenge}"]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an AUTH response envelope.
|
||||||
|
* Format: ["AUTH", event]
|
||||||
|
*/
|
||||||
|
export function encloseAuthResponse(event: string): Envelope {
|
||||||
|
return `["AUTH",${event}]`;
|
||||||
|
}
|
||||||
101
src/envelope/envelope.test.ts
Normal file
101
src/envelope/envelope.test.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { getLabel, getStandardLabels, isStandardLabel } from "./envelope";
|
||||||
|
|
||||||
|
describe("getLabel", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "valid envelope with EVENT label",
|
||||||
|
env: '["EVENT",{"id":"abc123"}]',
|
||||||
|
wantLabel: "EVENT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid envelope with custom label",
|
||||||
|
env: '["TEST",{"data":"value"}]',
|
||||||
|
wantLabel: "TEST",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid json",
|
||||||
|
env: "invalid",
|
||||||
|
wantErrText: "Unexpected token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty array",
|
||||||
|
env: "[]",
|
||||||
|
wantErrText: "empty envelope",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "label not a string",
|
||||||
|
env: '[123,{"id":"abc123"}]',
|
||||||
|
wantErrText: "label is not a string",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
if (tc.wantErrText) {
|
||||||
|
try {
|
||||||
|
getLabel(tc.env);
|
||||||
|
expect.fail("Expected function to throw an error");
|
||||||
|
} catch (err) {
|
||||||
|
expect((err as Error).message).toContain(tc.wantErrText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const got = getLabel(tc.env);
|
||||||
|
expect(got).toEqual(tc.wantLabel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getStandardLabels", () => {
|
||||||
|
it("returns the correct standard labels", () => {
|
||||||
|
const expected = new Set([
|
||||||
|
"EVENT",
|
||||||
|
"REQ",
|
||||||
|
"CLOSE",
|
||||||
|
"CLOSED",
|
||||||
|
"EOSE",
|
||||||
|
"NOTICE",
|
||||||
|
"OK",
|
||||||
|
"AUTH",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const labels = getStandardLabels();
|
||||||
|
|
||||||
|
// Check that we have the exact same number of labels
|
||||||
|
expect(labels.size).toBe(expected.size);
|
||||||
|
|
||||||
|
// Check that all expected labels are present
|
||||||
|
expected.forEach((label) => {
|
||||||
|
expect(labels.has(label)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isStandardLabel", () => {
|
||||||
|
const standardCases = [
|
||||||
|
"EVENT",
|
||||||
|
"REQ",
|
||||||
|
"CLOSE",
|
||||||
|
"CLOSED",
|
||||||
|
"EOSE",
|
||||||
|
"NOTICE",
|
||||||
|
"OK",
|
||||||
|
"AUTH",
|
||||||
|
];
|
||||||
|
|
||||||
|
const nonStandardCases = ["TEST", "CUSTOM", "event", "REQ1", ""];
|
||||||
|
|
||||||
|
standardCases.forEach((label) => {
|
||||||
|
it(`${label} should be standard`, () => {
|
||||||
|
expect(isStandardLabel(label)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
nonStandardCases.forEach((label) => {
|
||||||
|
it(`${label} should not be standard`, () => {
|
||||||
|
expect(isStandardLabel(label)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
52
src/envelope/envelope.ts
Normal file
52
src/envelope/envelope.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
InvalidEnvelopeError,
|
||||||
|
InvalidJSONError,
|
||||||
|
WrongFieldTypeError,
|
||||||
|
} from "../errors";
|
||||||
|
import { Envelope } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the label from an envelope.
|
||||||
|
*/
|
||||||
|
export function getLabel(env: Envelope): string {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(env);
|
||||||
|
if (!Array.isArray(arr) || arr.length < 1) {
|
||||||
|
throw new InvalidEnvelopeError("empty envelope");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof arr[0] !== "string") {
|
||||||
|
throw new WrongFieldTypeError("label is not a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr[0];
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error && !(err instanceof InvalidJSONError)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw new InvalidJSONError(`${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a set of standard Nostr WebSocket message labels.
|
||||||
|
*/
|
||||||
|
export function getStandardLabels(): Set<string> {
|
||||||
|
return new Set([
|
||||||
|
"EVENT",
|
||||||
|
"REQ",
|
||||||
|
"CLOSE",
|
||||||
|
"CLOSED",
|
||||||
|
"EOSE",
|
||||||
|
"NOTICE",
|
||||||
|
"OK",
|
||||||
|
"AUTH",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given label is a standard Nostr WebSocket message label.
|
||||||
|
*/
|
||||||
|
export function isStandardLabel(label: string): boolean {
|
||||||
|
return getStandardLabels().has(label);
|
||||||
|
}
|
||||||
509
src/envelope/find.test.ts
Normal file
509
src/envelope/find.test.ts
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
findAuthChallenge,
|
||||||
|
findAuthResponse,
|
||||||
|
findClose,
|
||||||
|
findClosed,
|
||||||
|
findEOSE,
|
||||||
|
findEvent,
|
||||||
|
findNotice,
|
||||||
|
findOK,
|
||||||
|
findReq,
|
||||||
|
findSubscriptionEvent,
|
||||||
|
} from "./find";
|
||||||
|
|
||||||
|
describe("findEvent", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "valid event",
|
||||||
|
env: '["EVENT",{"id":"abc123","kind":1}]',
|
||||||
|
wantEvent: '{"id":"abc123","kind":1}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong label",
|
||||||
|
env: '["REQ",{"id":"abc123","kind":1}]',
|
||||||
|
wantErrText: "expected EVENT, got REQ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid json",
|
||||||
|
env: "invalid",
|
||||||
|
wantErrText: "Unexpected token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing elements",
|
||||||
|
env: '["EVENT"]',
|
||||||
|
wantErrText: "expected 2 elements, got 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "extraneous elements",
|
||||||
|
env: '["EVENT",{"id":"abc123"},"extra"]',
|
||||||
|
wantEvent: '{"id":"abc123"}',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
if (tc.wantErrText) {
|
||||||
|
try {
|
||||||
|
findEvent(tc.env);
|
||||||
|
expect.fail("Expected function to throw an error");
|
||||||
|
} catch (err) {
|
||||||
|
expect((err as Error).message).toContain(tc.wantErrText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const got = findEvent(tc.env);
|
||||||
|
expect(JSON.stringify(got)).toEqual(tc.wantEvent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findSubscriptionEvent", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "valid event",
|
||||||
|
env: '["EVENT","sub1",{"id":"abc123","kind":1}]',
|
||||||
|
wantSubID: "sub1",
|
||||||
|
wantEvent: '{"id":"abc123","kind":1}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong label",
|
||||||
|
env: '["REQ","sub1",{"id":"abc123","kind":1}]',
|
||||||
|
wantErrText: "expected EVENT, got REQ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid json",
|
||||||
|
env: "invalid",
|
||||||
|
wantErrText: "Unexpected token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing elements",
|
||||||
|
env: '["EVENT","sub1"]',
|
||||||
|
wantErrText: "expected 3 elements, got 2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "extraneous elements",
|
||||||
|
env: '["EVENT","sub1",{"id":"abc123"},"extra"]',
|
||||||
|
wantSubID: "sub1",
|
||||||
|
wantEvent: '{"id":"abc123"}',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
if (tc.wantErrText) {
|
||||||
|
try {
|
||||||
|
findSubscriptionEvent(tc.env);
|
||||||
|
expect.fail("Expected function to throw an error");
|
||||||
|
} catch (err) {
|
||||||
|
expect((err as Error).message).toContain(tc.wantErrText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const [gotSubID, gotEvent] = findSubscriptionEvent(tc.env);
|
||||||
|
expect(gotSubID).toEqual(tc.wantSubID);
|
||||||
|
expect(JSON.stringify(gotEvent)).toEqual(tc.wantEvent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findOK", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "accepted event",
|
||||||
|
env: '["OK","abc123",true,"Event accepted"]',
|
||||||
|
wantEventID: "abc123",
|
||||||
|
wantStatus: true,
|
||||||
|
wantMessage: "Event accepted",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rejected event",
|
||||||
|
env: '["OK","xyz789",false,"Invalid signature"]',
|
||||||
|
wantEventID: "xyz789",
|
||||||
|
wantStatus: false,
|
||||||
|
wantMessage: "Invalid signature",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong status type",
|
||||||
|
env: '["OK","abc123","ok","Event accepted"]',
|
||||||
|
wantErrText: "status is not the expected type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong label",
|
||||||
|
env: '["EVENT","abc123",true,"Event accepted"]',
|
||||||
|
wantErrText: "expected OK, got EVENT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid json",
|
||||||
|
env: "invalid",
|
||||||
|
wantErrText: "Unexpected token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing elements",
|
||||||
|
env: '["OK","abc123",true]',
|
||||||
|
wantErrText: "expected 4 elements, got 3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "extraneous elements",
|
||||||
|
env: '["OK","abc123",true,"Event accepted","extra"]',
|
||||||
|
wantEventID: "abc123",
|
||||||
|
wantStatus: true,
|
||||||
|
wantMessage: "Event accepted",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
if (tc.wantErrText) {
|
||||||
|
try {
|
||||||
|
findOK(tc.env);
|
||||||
|
expect.fail("Expected function to throw an error");
|
||||||
|
} catch (err) {
|
||||||
|
expect((err as Error).message).toContain(tc.wantErrText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const [gotEventID, gotStatus, gotMessage] = findOK(tc.env);
|
||||||
|
expect(gotEventID).toEqual(tc.wantEventID);
|
||||||
|
expect(gotStatus).toEqual(tc.wantStatus);
|
||||||
|
expect(gotMessage).toEqual(tc.wantMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findReq", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "single filter",
|
||||||
|
env: '["REQ","sub1",{"kinds":[1],"limit":10}]',
|
||||||
|
wantSubID: "sub1",
|
||||||
|
wantFilters: ['{"kinds":[1],"limit":10}'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple filters",
|
||||||
|
env: '["REQ","sub2",{"kinds":[1]},{"authors":["abc"]}]',
|
||||||
|
wantSubID: "sub2",
|
||||||
|
wantFilters: ['{"kinds":[1]}', '{"authors":["abc"]}'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no filters",
|
||||||
|
env: '["REQ","sub3"]',
|
||||||
|
wantSubID: "sub3",
|
||||||
|
wantFilters: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong label",
|
||||||
|
env: '["EVENT","sub1",{"kinds":[1],"limit":10}]',
|
||||||
|
wantErrText: "expected REQ, got EVENT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid json",
|
||||||
|
env: "invalid",
|
||||||
|
wantErrText: "Unexpected token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing elements",
|
||||||
|
env: '["REQ"]',
|
||||||
|
wantErrText: "expected 2 elements, got 1",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
if (tc.wantErrText) {
|
||||||
|
try {
|
||||||
|
findReq(tc.env);
|
||||||
|
expect.fail("Expected function to throw an error");
|
||||||
|
} catch (err) {
|
||||||
|
expect((err as Error).message).toContain(tc.wantErrText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const [gotSubID, gotFilters] = findReq(tc.env);
|
||||||
|
expect(gotSubID).toEqual(tc.wantSubID);
|
||||||
|
expect(gotFilters.map((f) => JSON.stringify(f))).toEqual(
|
||||||
|
tc.wantFilters,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findEOSE", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "valid EOSE",
|
||||||
|
env: '["EOSE","sub1"]',
|
||||||
|
wantSubID: "sub1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong label",
|
||||||
|
env: '["EVENT","sub1"]',
|
||||||
|
wantErrText: "expected EOSE, got EVENT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid json",
|
||||||
|
env: "invalid",
|
||||||
|
wantErrText: "Unexpected token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing elements",
|
||||||
|
env: '["EOSE"]',
|
||||||
|
wantErrText: "expected 2 elements, got 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "extraneous elements",
|
||||||
|
env: '["EOSE","sub1","extra"]',
|
||||||
|
wantSubID: "sub1",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
if (tc.wantErrText) {
|
||||||
|
try {
|
||||||
|
findEOSE(tc.env);
|
||||||
|
expect.fail("Expected function to throw an error");
|
||||||
|
} catch (err) {
|
||||||
|
expect((err as Error).message).toContain(tc.wantErrText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const gotSubID = findEOSE(tc.env);
|
||||||
|
expect(gotSubID).toEqual(tc.wantSubID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findClose", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "valid CLOSE",
|
||||||
|
env: '["CLOSE","sub1"]',
|
||||||
|
wantSubID: "sub1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong label",
|
||||||
|
env: '["EVENT","sub1"]',
|
||||||
|
wantErrText: "expected CLOSE, got EVENT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid json",
|
||||||
|
env: "invalid",
|
||||||
|
wantErrText: "Unexpected token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing elements",
|
||||||
|
env: '["CLOSE"]',
|
||||||
|
wantErrText: "expected 2 elements, got 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "extraneous elements",
|
||||||
|
env: '["CLOSE","sub1","extra"]',
|
||||||
|
wantSubID: "sub1",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
if (tc.wantErrText) {
|
||||||
|
try {
|
||||||
|
findClose(tc.env);
|
||||||
|
expect.fail("Expected function to throw an error");
|
||||||
|
} catch (err) {
|
||||||
|
expect((err as Error).message).toContain(tc.wantErrText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const gotSubID = findClose(tc.env);
|
||||||
|
expect(gotSubID).toEqual(tc.wantSubID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findClosed", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "valid CLOSED",
|
||||||
|
env: '["CLOSED","sub1","Subscription complete"]',
|
||||||
|
wantSubID: "sub1",
|
||||||
|
wantMessage: "Subscription complete",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong label",
|
||||||
|
env: '["EVENT","sub1","Subscription complete"]',
|
||||||
|
wantErrText: "expected CLOSED, got EVENT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid json",
|
||||||
|
env: "invalid",
|
||||||
|
wantErrText: "Unexpected token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing elements",
|
||||||
|
env: '["CLOSED","sub1"]',
|
||||||
|
wantErrText: "expected 3 elements, got 2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "extraneous elements",
|
||||||
|
env: '["CLOSED","sub1","Subscription complete","extra"]',
|
||||||
|
wantSubID: "sub1",
|
||||||
|
wantMessage: "Subscription complete",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
if (tc.wantErrText) {
|
||||||
|
try {
|
||||||
|
findClosed(tc.env);
|
||||||
|
expect.fail("Expected function to throw an error");
|
||||||
|
} catch (err) {
|
||||||
|
expect((err as Error).message).toContain(tc.wantErrText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const [gotSubID, gotMessage] = findClosed(tc.env);
|
||||||
|
expect(gotSubID).toEqual(tc.wantSubID);
|
||||||
|
expect(gotMessage).toEqual(tc.wantMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findNotice", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "valid NOTICE",
|
||||||
|
env: '["NOTICE","This is a notice"]',
|
||||||
|
wantMessage: "This is a notice",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong label",
|
||||||
|
env: '["EVENT","This is a notice"]',
|
||||||
|
wantErrText: "expected NOTICE, got EVENT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid json",
|
||||||
|
env: "invalid",
|
||||||
|
wantErrText: "Unexpected token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing elements",
|
||||||
|
env: '["NOTICE"]',
|
||||||
|
wantErrText: "expected 2 elements, got 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "extraneous elements",
|
||||||
|
env: '["NOTICE","This is a notice","extra"]',
|
||||||
|
wantMessage: "This is a notice",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
if (tc.wantErrText) {
|
||||||
|
try {
|
||||||
|
findNotice(tc.env);
|
||||||
|
expect.fail("Expected function to throw an error");
|
||||||
|
} catch (err) {
|
||||||
|
expect((err as Error).message).toContain(tc.wantErrText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const gotMessage = findNotice(tc.env);
|
||||||
|
expect(gotMessage).toEqual(tc.wantMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findAuthChallenge", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "valid AUTH challenge",
|
||||||
|
env: '["AUTH","random-challenge-string"]',
|
||||||
|
wantChallenge: "random-challenge-string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong label",
|
||||||
|
env: '["EVENT","random-challenge-string"]',
|
||||||
|
wantErrText: "expected AUTH, got EVENT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid json",
|
||||||
|
env: "invalid",
|
||||||
|
wantErrText: "Unexpected token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing elements",
|
||||||
|
env: '["AUTH"]',
|
||||||
|
wantErrText: "expected 2 elements, got 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "extraneous elements",
|
||||||
|
env: '["AUTH","random-challenge-string","extra"]',
|
||||||
|
wantChallenge: "random-challenge-string",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
if (tc.wantErrText) {
|
||||||
|
try {
|
||||||
|
findAuthChallenge(tc.env);
|
||||||
|
expect.fail("Expected function to throw an error");
|
||||||
|
} catch (err) {
|
||||||
|
expect((err as Error).message).toContain(tc.wantErrText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const gotChallenge = findAuthChallenge(tc.env);
|
||||||
|
expect(gotChallenge).toEqual(tc.wantChallenge);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findAuthResponse", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "valid AUTH response",
|
||||||
|
env: '["AUTH",{"id":"abc123","kind":22242}]',
|
||||||
|
wantEvent: '{"id":"abc123","kind":22242}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong label",
|
||||||
|
env: '["EVENT",{"id":"abc123","kind":22242}]',
|
||||||
|
wantErrText: "expected AUTH, got EVENT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid json",
|
||||||
|
env: "invalid",
|
||||||
|
wantErrText: "Unexpected token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing elements",
|
||||||
|
env: '["AUTH"]',
|
||||||
|
wantErrText: "expected 2 elements, got 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "extraneous elements",
|
||||||
|
env: '["AUTH",{"id":"abc123","kind":22242},"extra"]',
|
||||||
|
wantEvent: '{"id":"abc123","kind":22242}',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
cases.forEach((tc) => {
|
||||||
|
it(tc.name, () => {
|
||||||
|
if (tc.wantErrText) {
|
||||||
|
try {
|
||||||
|
findAuthResponse(tc.env);
|
||||||
|
expect.fail("Expected function to throw an error");
|
||||||
|
} catch (err) {
|
||||||
|
expect((err as Error).message).toContain(tc.wantErrText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const gotEvent = findAuthResponse(tc.env);
|
||||||
|
expect(JSON.stringify(gotEvent)).toEqual(tc.wantEvent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
332
src/envelope/find.ts
Normal file
332
src/envelope/find.ts
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
import {
|
||||||
|
InvalidEnvelopeError,
|
||||||
|
InvalidJSONError,
|
||||||
|
WrongEnvelopeLabelError,
|
||||||
|
WrongFieldTypeError,
|
||||||
|
} from "../errors";
|
||||||
|
import { Envelope } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that ensures the JSON array has at least the minimum length required.
|
||||||
|
*/
|
||||||
|
export function checkArrayLength(arr: any[], minLen: number): void {
|
||||||
|
if (arr.length < minLen) {
|
||||||
|
throw new InvalidEnvelopeError(
|
||||||
|
`expected ${minLen} elements, got ${arr.length}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that verifies that the envelope label matches the expected one.
|
||||||
|
*/
|
||||||
|
export function checkLabel(got: string, want: string): void {
|
||||||
|
if (got !== want) {
|
||||||
|
throw new WrongEnvelopeLabelError(`expected ${want}, got ${got}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts an event from an EVENT envelope with no subscription ID.
|
||||||
|
* Expected Format: ["EVENT", event]
|
||||||
|
*/
|
||||||
|
export function findEvent(env: Envelope): any {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(env);
|
||||||
|
checkArrayLength(arr, 2);
|
||||||
|
|
||||||
|
const label = arr[0];
|
||||||
|
if (typeof label !== "string") {
|
||||||
|
throw new WrongFieldTypeError("envelope label is not a string");
|
||||||
|
}
|
||||||
|
checkLabel(label, "EVENT");
|
||||||
|
|
||||||
|
return arr[1];
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw new InvalidJSONError(`${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts an event and subscription ID from an EVENT envelope.
|
||||||
|
* Expected Format: ["EVENT", subID, event]
|
||||||
|
*/
|
||||||
|
export function findSubscriptionEvent(
|
||||||
|
env: Envelope,
|
||||||
|
): [subID: string, event: any] {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(env);
|
||||||
|
checkArrayLength(arr, 3);
|
||||||
|
|
||||||
|
const label = arr[0];
|
||||||
|
if (typeof label !== "string") {
|
||||||
|
throw new WrongFieldTypeError("envelope label is not a string");
|
||||||
|
}
|
||||||
|
checkLabel(label, "EVENT");
|
||||||
|
|
||||||
|
const subID = arr[1];
|
||||||
|
if (typeof subID !== "string") {
|
||||||
|
throw new WrongFieldTypeError("subscription ID is not a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
return [subID, arr[2]];
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw new InvalidJSONError(`${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts eventID, status, and message from an OK envelope.
|
||||||
|
* Expected Format: ["OK", eventID, status, message]
|
||||||
|
*/
|
||||||
|
export function findOK(
|
||||||
|
env: Envelope,
|
||||||
|
): [eventID: string, status: boolean, message: string] {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(env);
|
||||||
|
checkArrayLength(arr, 4);
|
||||||
|
|
||||||
|
const label = arr[0];
|
||||||
|
if (typeof label !== "string") {
|
||||||
|
throw new WrongFieldTypeError("envelope label is not a string");
|
||||||
|
}
|
||||||
|
checkLabel(label, "OK");
|
||||||
|
|
||||||
|
const eventID = arr[1];
|
||||||
|
if (typeof eventID !== "string") {
|
||||||
|
throw new WrongFieldTypeError("event ID is not a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof arr[2] !== "boolean") {
|
||||||
|
throw new WrongFieldTypeError("status is not the expected type");
|
||||||
|
}
|
||||||
|
const status = arr[2];
|
||||||
|
|
||||||
|
const message = arr[3];
|
||||||
|
if (typeof message !== "string") {
|
||||||
|
throw new WrongFieldTypeError("message is not a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
return [eventID, status, message];
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw new InvalidJSONError(`${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts subscription ID and filters from a REQ envelope.
|
||||||
|
* Expected Format: ["REQ", subID, filter1, filter2, ...]
|
||||||
|
*/
|
||||||
|
export function findReq(env: Envelope): [subID: string, filters: any[]] {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(env);
|
||||||
|
checkArrayLength(arr, 2);
|
||||||
|
|
||||||
|
const label = arr[0];
|
||||||
|
if (typeof label !== "string") {
|
||||||
|
throw new WrongFieldTypeError("envelope label is not a string");
|
||||||
|
}
|
||||||
|
checkLabel(label, "REQ");
|
||||||
|
|
||||||
|
const subID = arr[1];
|
||||||
|
if (typeof subID !== "string") {
|
||||||
|
throw new WrongFieldTypeError("subscription ID is not a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters: any[] = [];
|
||||||
|
for (let i = 2; i < arr.length; i++) {
|
||||||
|
filters.push(arr[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [subID, filters];
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw new InvalidJSONError(`${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts subscription ID from an EOSE envelope.
|
||||||
|
* Expected Format: ["EOSE", subID]
|
||||||
|
*/
|
||||||
|
export function findEOSE(env: Envelope): string {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(env);
|
||||||
|
checkArrayLength(arr, 2);
|
||||||
|
|
||||||
|
const label = arr[0];
|
||||||
|
if (typeof label !== "string") {
|
||||||
|
throw new WrongFieldTypeError("envelope label is not a string");
|
||||||
|
}
|
||||||
|
checkLabel(label, "EOSE");
|
||||||
|
|
||||||
|
const subID = arr[1];
|
||||||
|
if (typeof subID !== "string") {
|
||||||
|
throw new WrongFieldTypeError("subscription ID is not a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
return subID;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw new InvalidJSONError(`${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts subscription ID from a CLOSE envelope.
|
||||||
|
* Expected Format: ["CLOSE", subID]
|
||||||
|
*/
|
||||||
|
export function findClose(env: Envelope): string {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(env);
|
||||||
|
checkArrayLength(arr, 2);
|
||||||
|
|
||||||
|
const label = arr[0];
|
||||||
|
if (typeof label !== "string") {
|
||||||
|
throw new WrongFieldTypeError("envelope label is not a string");
|
||||||
|
}
|
||||||
|
checkLabel(label, "CLOSE");
|
||||||
|
|
||||||
|
const subID = arr[1];
|
||||||
|
if (typeof subID !== "string") {
|
||||||
|
throw new WrongFieldTypeError("subscription ID is not a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
return subID;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw new InvalidJSONError(`${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts subscription ID and message from a CLOSED envelope.
|
||||||
|
* Expected Format: ["CLOSED", subID, message]
|
||||||
|
*/
|
||||||
|
export function findClosed(env: Envelope): [subID: string, message: string] {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(env);
|
||||||
|
checkArrayLength(arr, 3);
|
||||||
|
|
||||||
|
const label = arr[0];
|
||||||
|
if (typeof label !== "string") {
|
||||||
|
throw new WrongFieldTypeError("envelope label is not a string");
|
||||||
|
}
|
||||||
|
checkLabel(label, "CLOSED");
|
||||||
|
|
||||||
|
const subID = arr[1];
|
||||||
|
if (typeof subID !== "string") {
|
||||||
|
throw new WrongFieldTypeError("subscription ID is not a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = arr[2];
|
||||||
|
if (typeof message !== "string") {
|
||||||
|
throw new WrongFieldTypeError("message is not a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
return [subID, message];
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw new InvalidJSONError(`${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts message from a NOTICE envelope.
|
||||||
|
* Expected Format: ["NOTICE", message]
|
||||||
|
*/
|
||||||
|
export function findNotice(env: Envelope): string {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(env);
|
||||||
|
checkArrayLength(arr, 2);
|
||||||
|
|
||||||
|
const label = arr[0];
|
||||||
|
if (typeof label !== "string") {
|
||||||
|
throw new WrongFieldTypeError("envelope label is not a string");
|
||||||
|
}
|
||||||
|
checkLabel(label, "NOTICE");
|
||||||
|
|
||||||
|
const message = arr[1];
|
||||||
|
if (typeof message !== "string") {
|
||||||
|
throw new WrongFieldTypeError("message is not a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw new InvalidJSONError(`${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts challenge from an AUTH challenge envelope.
|
||||||
|
* Expected Format: ["AUTH", challenge]
|
||||||
|
*/
|
||||||
|
export function findAuthChallenge(env: Envelope): string {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(env);
|
||||||
|
checkArrayLength(arr, 2);
|
||||||
|
|
||||||
|
const label = arr[0];
|
||||||
|
if (typeof label !== "string") {
|
||||||
|
throw new WrongFieldTypeError("envelope label is not a string");
|
||||||
|
}
|
||||||
|
checkLabel(label, "AUTH");
|
||||||
|
|
||||||
|
const challenge = arr[1];
|
||||||
|
if (typeof challenge !== "string") {
|
||||||
|
throw new WrongFieldTypeError("challenge is not a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
return challenge;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw new InvalidJSONError(`${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts event from an AUTH response envelope.
|
||||||
|
* Expected Format: ["AUTH", event]
|
||||||
|
*/
|
||||||
|
export function findAuthResponse(env: Envelope): any {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(env);
|
||||||
|
checkArrayLength(arr, 2);
|
||||||
|
|
||||||
|
const label = arr[0];
|
||||||
|
if (typeof label !== "string") {
|
||||||
|
throw new WrongFieldTypeError("envelope label is not a string");
|
||||||
|
}
|
||||||
|
checkLabel(label, "AUTH");
|
||||||
|
|
||||||
|
// The second element should be an object (the event)
|
||||||
|
return arr[1];
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw new InvalidJSONError(`${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/envelope/index.ts
Normal file
3
src/envelope/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./envelope";
|
||||||
|
export * from "./enclose";
|
||||||
|
export * from "./find";
|
||||||
70
src/errors/index.ts
Normal file
70
src/errors/index.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Base error class for all roots-ws errors.
|
||||||
|
*/
|
||||||
|
export class RootsWSError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
// This sets the prototype correctly for instanceof checks
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data Structure Errors
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that a byte sequence could not be parsed as valid JSON.
|
||||||
|
* This is typically returned when unmarshaling fails during envelope processing.
|
||||||
|
*/
|
||||||
|
export class InvalidJSONError extends RootsWSError {
|
||||||
|
constructor(message: string = "invalid JSON") {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that a required field is absent from a data structure.
|
||||||
|
* This is returned when validating that all mandatory components are present.
|
||||||
|
*/
|
||||||
|
export class MissingFieldError extends RootsWSError {
|
||||||
|
constructor(message: string = "missing required field") {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that a field's type does not match the expected type.
|
||||||
|
* This is returned when unmarshaling a specific value fails due to type mismatch.
|
||||||
|
*/
|
||||||
|
export class WrongFieldTypeError extends RootsWSError {
|
||||||
|
constructor(message: string = "wrong field type") {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envelope Errors
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that a message does not conform to the Nostr envelope structure.
|
||||||
|
* This typically occurs when an array has incorrect number of elements for its message type.
|
||||||
|
*/
|
||||||
|
export class InvalidEnvelopeError extends RootsWSError {
|
||||||
|
constructor(message: string = "invalid envelope format") {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that an envelope's label does not match the expected type.
|
||||||
|
* This is returned when attempting to parse an envelope using a Find function that
|
||||||
|
* expects a different label than what was provided.
|
||||||
|
*/
|
||||||
|
export class WrongEnvelopeLabelError extends RootsWSError {
|
||||||
|
constructor(message: string = "wrong envelope label") {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/index.ts
Normal file
1
src/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./types";
|
||||||
29
src/types.ts
Normal file
29
src/types.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Represents a Nostr websocket message.
|
||||||
|
*/
|
||||||
|
export type Envelope = string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the current state of a WebSocket connection.
|
||||||
|
*/
|
||||||
|
export enum ConnectionStatus {
|
||||||
|
/**
|
||||||
|
* Indicates the connection is not active and no connection attempt is in progress.
|
||||||
|
*/
|
||||||
|
Disconnected = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates a connection attempt is currently in progress but not yet established.
|
||||||
|
*/
|
||||||
|
Connecting = 1,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates the connection is active and ready for message exchange.
|
||||||
|
*/
|
||||||
|
Connected = 2,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates the connection is in the process of shutting down gracefully.
|
||||||
|
*/
|
||||||
|
Closing = 3,
|
||||||
|
}
|
||||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||||
|
}
|
||||||
7
vitest.config.ts
Normal file
7
vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user