@phuzle/relay 0.1.0 → 0.1.1
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 +11 -0
- package/cli.js +87 -8
- package/index.cjs +23 -0
- package/index.d.cts +44 -0
- package/index.d.ts +44 -0
- package/index.js +23 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,12 +31,23 @@ try {
|
|
|
31
31
|
- Buttons (`actions`), images, scheduling (`deliverAt`, `delaySeconds`), `repeat` until acknowledged and `escalate` to another topic are supported.
|
|
32
32
|
- Point `baseUrl` at a local, staging or self-hosted Relay (default `https://relay.phuzle.com`).
|
|
33
33
|
|
|
34
|
+
## Topics and devices
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const { topics, canCreate } = await relay.topics(); // topics this key may send to (fullName is what send() takes)
|
|
38
|
+
const t = await relay.createTopic({ slug: "deploys", name: "Deploys" }); // needs "Can create topics" on the key
|
|
39
|
+
const phones = await relay.devices("acme-alerts"); // nickname, model, appVersion, lastSeenAt…
|
|
40
|
+
```
|
|
41
|
+
|
|
34
42
|
## Command line
|
|
35
43
|
|
|
36
44
|
```bash
|
|
37
45
|
export RELAY_API_KEY=rly_...
|
|
38
46
|
npx @phuzle/relay send --topic acme-alerts --title "Backup finished" --body "412 GB in 6 min"
|
|
39
47
|
# installed globally: relay send …
|
|
48
|
+
relay topics # what this key can send to (add --json for scripts)
|
|
49
|
+
relay topics create deploys --name "Deploys" # if the key has "Can create topics"
|
|
50
|
+
relay devices --topic acme-alerts
|
|
40
51
|
```
|
|
41
52
|
|
|
42
53
|
Exit code `0` on success, `1` with a one-line reason on failure. `relay` with no arguments prints every option.
|
package/cli.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
2
|
// ../core/src/encoding.ts
|
|
4
3
|
function b64urlEncode(bytes) {
|
|
5
4
|
let s = "";
|
|
@@ -209,6 +208,29 @@ var Relay = class _Relay {
|
|
|
209
208
|
}
|
|
210
209
|
});
|
|
211
210
|
}
|
|
211
|
+
/** The topics this key may send to (its `trigger:*` / `trigger:<slug>` permissions), and whether it may create more. */
|
|
212
|
+
async topics() {
|
|
213
|
+
const r = await this.request("GET", "/v1/trigger/topics");
|
|
214
|
+
return { topics: r.data, canCreate: r.canCreate };
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Create a topic. Needs the **Can create topics** permission on the key (`scope_denied` otherwise) and counts against the plan's
|
|
218
|
+
* topic limit (`topic_limit`). A key limited to specific topics may send to the ones it creates. Not retried automatically:
|
|
219
|
+
* a second attempt would only answer `slug_taken`.
|
|
220
|
+
*/
|
|
221
|
+
async createTopic(t) {
|
|
222
|
+
return await this.parse(
|
|
223
|
+
await this.raw("POST", "/v1/trigger/topics", t)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
/** The phones receiving `topic` (the full `<org>-<slug>` name). The key must be allowed to send to it. */
|
|
227
|
+
async devices(topic) {
|
|
228
|
+
const r = await this.request(
|
|
229
|
+
"GET",
|
|
230
|
+
`/v1/trigger/topics/${encodeURIComponent(topic)}/devices`
|
|
231
|
+
);
|
|
232
|
+
return r.data;
|
|
233
|
+
}
|
|
212
234
|
async send(m) {
|
|
213
235
|
checkRepeat(m.repeat);
|
|
214
236
|
checkRepeat(m.escalate?.repeat);
|
|
@@ -462,7 +484,10 @@ function buildPlaintext(m) {
|
|
|
462
484
|
}
|
|
463
485
|
|
|
464
486
|
// src/cli.ts
|
|
465
|
-
var USAGE = `relay
|
|
487
|
+
var USAGE = `relay topics [--json] list the topics this API key can send to (and whether it may create more)
|
|
488
|
+
relay topics create <slug> --name <text> [--description <text>] create a topic (the key needs "Can create topics")
|
|
489
|
+
relay devices --topic <org-topic> [--json] list the phones receiving a topic
|
|
490
|
+
relay send --topic <org-topic> --title <text> [--body <text>] [--priority min|low|default|high|urgent]
|
|
466
491
|
[--url <https url>] [--image <https url>] [--icon <name>] [--tag <t>]... [--group <g>] [--ttl <seconds>]
|
|
467
492
|
[--api-url <https://your-relay>] [--action "Label=METHOD https://url"]... [--link "Label=https://url"]...
|
|
468
493
|
(up to 3 buttons in total. --action sends a request from the phone; add "!" after the label for destructive: "Reboot!=POST \u2026".
|
|
@@ -523,13 +548,68 @@ function link(spec) {
|
|
|
523
548
|
}
|
|
524
549
|
async function main(argv, env = process.env, out = console) {
|
|
525
550
|
const [cmd, ...rest] = argv;
|
|
526
|
-
if (cmd
|
|
551
|
+
if (!["send", "topics", "devices"].includes(cmd ?? "") || rest.includes("--help")) {
|
|
527
552
|
out.log(USAGE);
|
|
528
|
-
return cmd
|
|
553
|
+
return !cmd || ["send", "topics", "devices"].includes(cmd) ? 0 : 1;
|
|
529
554
|
}
|
|
530
555
|
const f = parse(rest);
|
|
531
556
|
const one = (k) => f.get(k)?.[0];
|
|
532
557
|
try {
|
|
558
|
+
if (cmd === "topics" || cmd === "devices") {
|
|
559
|
+
const relay = Relay.fromEnv(env, { baseUrl: one("api-url") });
|
|
560
|
+
const json = f.has("json");
|
|
561
|
+
if (cmd === "devices") {
|
|
562
|
+
const topic2 = one("topic");
|
|
563
|
+
if (!topic2) throw new Error("--topic is required");
|
|
564
|
+
const list = await relay.devices(topic2);
|
|
565
|
+
if (json) out.log(JSON.stringify(list, null, 2));
|
|
566
|
+
else if (list.length === 0)
|
|
567
|
+
out.log(`no devices are receiving ${topic2}`);
|
|
568
|
+
else
|
|
569
|
+
for (const d of list)
|
|
570
|
+
out.log(
|
|
571
|
+
`${d.nickname ?? d.name ?? d.model ?? d.id} ${d.model ?? ""} app ${d.appVersion ?? "?"} last seen ${d.lastSeenAt ?? "never"} (${d.id})`.replace(
|
|
572
|
+
/ {2,}/g,
|
|
573
|
+
" "
|
|
574
|
+
)
|
|
575
|
+
);
|
|
576
|
+
return 0;
|
|
577
|
+
}
|
|
578
|
+
const positional = [];
|
|
579
|
+
for (const a of rest) {
|
|
580
|
+
if (a.startsWith("--")) break;
|
|
581
|
+
positional.push(a);
|
|
582
|
+
}
|
|
583
|
+
if (positional[0] === "create") {
|
|
584
|
+
const slug = positional[1];
|
|
585
|
+
if (!slug || !one("name"))
|
|
586
|
+
throw new Error(
|
|
587
|
+
"Usage: relay topics create <slug> --name <text> [--description <text>]"
|
|
588
|
+
);
|
|
589
|
+
const t = await relay.createTopic({
|
|
590
|
+
slug,
|
|
591
|
+
name: one("name"),
|
|
592
|
+
description: one("description")
|
|
593
|
+
});
|
|
594
|
+
out.log(
|
|
595
|
+
json ? JSON.stringify(t, null, 2) : `created ${t.fullName} (send to it with --topic ${t.fullName})`
|
|
596
|
+
);
|
|
597
|
+
return 0;
|
|
598
|
+
}
|
|
599
|
+
if (positional.length > 0)
|
|
600
|
+
throw new Error(`Unknown: relay topics ${positional[0]}`);
|
|
601
|
+
const { topics, canCreate } = await relay.topics();
|
|
602
|
+
if (json) out.log(JSON.stringify({ topics, canCreate }, null, 2));
|
|
603
|
+
else {
|
|
604
|
+
if (topics.length === 0) out.log("this key can't send to any topic");
|
|
605
|
+
for (const t of topics)
|
|
606
|
+
out.log(
|
|
607
|
+
`${t.fullName} ${t.devices} device${t.devices === 1 ? "" : "s"} ${t.name}`
|
|
608
|
+
);
|
|
609
|
+
out.log(canCreate ? "can create topics: yes" : "can create topics: no");
|
|
610
|
+
}
|
|
611
|
+
return 0;
|
|
612
|
+
}
|
|
533
613
|
const topic = one("topic");
|
|
534
614
|
if (!topic || !one("title"))
|
|
535
615
|
throw new Error("--topic and --title are required");
|
|
@@ -577,7 +657,6 @@ async function main(argv, env = process.env, out = console) {
|
|
|
577
657
|
}
|
|
578
658
|
}
|
|
579
659
|
if (import.meta.main) process.exit(await main(process.argv.slice(2)));
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
};
|
|
660
|
+
|
|
661
|
+
// src/bin.ts
|
|
662
|
+
process.exit(await main(process.argv.slice(2)));
|
package/index.cjs
CHANGED
|
@@ -236,6 +236,29 @@ var Relay = class _Relay {
|
|
|
236
236
|
}
|
|
237
237
|
});
|
|
238
238
|
}
|
|
239
|
+
/** The topics this key may send to (its `trigger:*` / `trigger:<slug>` permissions), and whether it may create more. */
|
|
240
|
+
async topics() {
|
|
241
|
+
const r = await this.request("GET", "/v1/trigger/topics");
|
|
242
|
+
return { topics: r.data, canCreate: r.canCreate };
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Create a topic. Needs the **Can create topics** permission on the key (`scope_denied` otherwise) and counts against the plan's
|
|
246
|
+
* topic limit (`topic_limit`). A key limited to specific topics may send to the ones it creates. Not retried automatically:
|
|
247
|
+
* a second attempt would only answer `slug_taken`.
|
|
248
|
+
*/
|
|
249
|
+
async createTopic(t) {
|
|
250
|
+
return await this.parse(
|
|
251
|
+
await this.raw("POST", "/v1/trigger/topics", t)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
/** The phones receiving `topic` (the full `<org>-<slug>` name). The key must be allowed to send to it. */
|
|
255
|
+
async devices(topic) {
|
|
256
|
+
const r = await this.request(
|
|
257
|
+
"GET",
|
|
258
|
+
`/v1/trigger/topics/${encodeURIComponent(topic)}/devices`
|
|
259
|
+
);
|
|
260
|
+
return r.data;
|
|
261
|
+
}
|
|
239
262
|
async send(m) {
|
|
240
263
|
checkRepeat(m.repeat);
|
|
241
264
|
checkRepeat(m.escalate?.repeat);
|
package/index.d.cts
CHANGED
|
@@ -106,6 +106,40 @@ export type RelayOptions = {
|
|
|
106
106
|
fetch?: typeof fetch;
|
|
107
107
|
maxRetries?: number;
|
|
108
108
|
};
|
|
109
|
+
/** A topic this API key can send to. `topic` is what you pass to `send()`. */
|
|
110
|
+
export type Topic = {
|
|
111
|
+
id: string;
|
|
112
|
+
slug: string;
|
|
113
|
+
/** `<org>-<slug>`: the value for `send({ topic })` */
|
|
114
|
+
fullName: string;
|
|
115
|
+
name: string;
|
|
116
|
+
description: string | null;
|
|
117
|
+
/** Phones receiving it now */
|
|
118
|
+
devices: number;
|
|
119
|
+
createdAt: string;
|
|
120
|
+
};
|
|
121
|
+
export type TopicList = {
|
|
122
|
+
topics: Topic[];
|
|
123
|
+
/** The key has the `topics:create` permission (set in the dashboard). */
|
|
124
|
+
canCreate: boolean;
|
|
125
|
+
};
|
|
126
|
+
export type CreateTopic = {
|
|
127
|
+
/** 2–32 lowercase letters, numbers and single hyphens; becomes `<org>-<slug>` */
|
|
128
|
+
slug: string;
|
|
129
|
+
name: string;
|
|
130
|
+
description?: string;
|
|
131
|
+
};
|
|
132
|
+
/** A phone receiving a topic. Metadata only; never keys or tokens. */
|
|
133
|
+
export type TopicDevice = {
|
|
134
|
+
id: string;
|
|
135
|
+
nickname: string | null;
|
|
136
|
+
name: string | null;
|
|
137
|
+
model: string | null;
|
|
138
|
+
platform: string;
|
|
139
|
+
appVersion: string | null;
|
|
140
|
+
lastSeenAt: string | null;
|
|
141
|
+
subscribedAt: string;
|
|
142
|
+
};
|
|
109
143
|
export declare class Relay {
|
|
110
144
|
private readonly opts;
|
|
111
145
|
private readonly base;
|
|
@@ -117,6 +151,16 @@ export declare class Relay {
|
|
|
117
151
|
constructor(opts: RelayOptions);
|
|
118
152
|
/** Reads `RELAY_API_KEY` and, optionally, `RELAY_API_URL`; `overrides` win over the environment. */
|
|
119
153
|
static fromEnv(env?: Record<string, string | undefined>, overrides?: Partial<RelayOptions>): Relay;
|
|
154
|
+
/** The topics this key may send to (its `trigger:*` / `trigger:<slug>` permissions), and whether it may create more. */
|
|
155
|
+
topics(): Promise<TopicList>;
|
|
156
|
+
/**
|
|
157
|
+
* Create a topic. Needs the **Can create topics** permission on the key (`scope_denied` otherwise) and counts against the plan's
|
|
158
|
+
* topic limit (`topic_limit`). A key limited to specific topics may send to the ones it creates. Not retried automatically:
|
|
159
|
+
* a second attempt would only answer `slug_taken`.
|
|
160
|
+
*/
|
|
161
|
+
createTopic(t: CreateTopic): Promise<Topic>;
|
|
162
|
+
/** The phones receiving `topic` (the full `<org>-<slug>` name). The key must be allowed to send to it. */
|
|
163
|
+
devices(topic: string): Promise<TopicDevice[]>;
|
|
120
164
|
send(m: Message): Promise<SendResult>;
|
|
121
165
|
private sendCore;
|
|
122
166
|
private sendOnce;
|
package/index.d.ts
CHANGED
|
@@ -106,6 +106,40 @@ export type RelayOptions = {
|
|
|
106
106
|
fetch?: typeof fetch;
|
|
107
107
|
maxRetries?: number;
|
|
108
108
|
};
|
|
109
|
+
/** A topic this API key can send to. `topic` is what you pass to `send()`. */
|
|
110
|
+
export type Topic = {
|
|
111
|
+
id: string;
|
|
112
|
+
slug: string;
|
|
113
|
+
/** `<org>-<slug>`: the value for `send({ topic })` */
|
|
114
|
+
fullName: string;
|
|
115
|
+
name: string;
|
|
116
|
+
description: string | null;
|
|
117
|
+
/** Phones receiving it now */
|
|
118
|
+
devices: number;
|
|
119
|
+
createdAt: string;
|
|
120
|
+
};
|
|
121
|
+
export type TopicList = {
|
|
122
|
+
topics: Topic[];
|
|
123
|
+
/** The key has the `topics:create` permission (set in the dashboard). */
|
|
124
|
+
canCreate: boolean;
|
|
125
|
+
};
|
|
126
|
+
export type CreateTopic = {
|
|
127
|
+
/** 2–32 lowercase letters, numbers and single hyphens; becomes `<org>-<slug>` */
|
|
128
|
+
slug: string;
|
|
129
|
+
name: string;
|
|
130
|
+
description?: string;
|
|
131
|
+
};
|
|
132
|
+
/** A phone receiving a topic. Metadata only; never keys or tokens. */
|
|
133
|
+
export type TopicDevice = {
|
|
134
|
+
id: string;
|
|
135
|
+
nickname: string | null;
|
|
136
|
+
name: string | null;
|
|
137
|
+
model: string | null;
|
|
138
|
+
platform: string;
|
|
139
|
+
appVersion: string | null;
|
|
140
|
+
lastSeenAt: string | null;
|
|
141
|
+
subscribedAt: string;
|
|
142
|
+
};
|
|
109
143
|
export declare class Relay {
|
|
110
144
|
private readonly opts;
|
|
111
145
|
private readonly base;
|
|
@@ -117,6 +151,16 @@ export declare class Relay {
|
|
|
117
151
|
constructor(opts: RelayOptions);
|
|
118
152
|
/** Reads `RELAY_API_KEY` and, optionally, `RELAY_API_URL`; `overrides` win over the environment. */
|
|
119
153
|
static fromEnv(env?: Record<string, string | undefined>, overrides?: Partial<RelayOptions>): Relay;
|
|
154
|
+
/** The topics this key may send to (its `trigger:*` / `trigger:<slug>` permissions), and whether it may create more. */
|
|
155
|
+
topics(): Promise<TopicList>;
|
|
156
|
+
/**
|
|
157
|
+
* Create a topic. Needs the **Can create topics** permission on the key (`scope_denied` otherwise) and counts against the plan's
|
|
158
|
+
* topic limit (`topic_limit`). A key limited to specific topics may send to the ones it creates. Not retried automatically:
|
|
159
|
+
* a second attempt would only answer `slug_taken`.
|
|
160
|
+
*/
|
|
161
|
+
createTopic(t: CreateTopic): Promise<Topic>;
|
|
162
|
+
/** The phones receiving `topic` (the full `<org>-<slug>` name). The key must be allowed to send to it. */
|
|
163
|
+
devices(topic: string): Promise<TopicDevice[]>;
|
|
120
164
|
send(m: Message): Promise<SendResult>;
|
|
121
165
|
private sendCore;
|
|
122
166
|
private sendOnce;
|
package/index.js
CHANGED
|
@@ -207,6 +207,29 @@ var Relay = class _Relay {
|
|
|
207
207
|
}
|
|
208
208
|
});
|
|
209
209
|
}
|
|
210
|
+
/** The topics this key may send to (its `trigger:*` / `trigger:<slug>` permissions), and whether it may create more. */
|
|
211
|
+
async topics() {
|
|
212
|
+
const r = await this.request("GET", "/v1/trigger/topics");
|
|
213
|
+
return { topics: r.data, canCreate: r.canCreate };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Create a topic. Needs the **Can create topics** permission on the key (`scope_denied` otherwise) and counts against the plan's
|
|
217
|
+
* topic limit (`topic_limit`). A key limited to specific topics may send to the ones it creates. Not retried automatically:
|
|
218
|
+
* a second attempt would only answer `slug_taken`.
|
|
219
|
+
*/
|
|
220
|
+
async createTopic(t) {
|
|
221
|
+
return await this.parse(
|
|
222
|
+
await this.raw("POST", "/v1/trigger/topics", t)
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
/** The phones receiving `topic` (the full `<org>-<slug>` name). The key must be allowed to send to it. */
|
|
226
|
+
async devices(topic) {
|
|
227
|
+
const r = await this.request(
|
|
228
|
+
"GET",
|
|
229
|
+
`/v1/trigger/topics/${encodeURIComponent(topic)}/devices`
|
|
230
|
+
);
|
|
231
|
+
return r.data;
|
|
232
|
+
}
|
|
210
233
|
async send(m) {
|
|
211
234
|
checkRepeat(m.repeat);
|
|
212
235
|
checkRepeat(m.escalate?.repeat);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phuzle/relay",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Send end-to-end encrypted push notifications through Relay: Node/Bun SDK and `relay` CLI. The message is encrypted on your machine; Relay only routes ciphertext.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"push",
|