@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 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 send --topic <org-topic> --title <text> [--body <text>] [--priority min|low|default|high|urgent]
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 !== "send" || rest.includes("--help")) {
551
+ if (!["send", "topics", "devices"].includes(cmd ?? "") || rest.includes("--help")) {
527
552
  out.log(USAGE);
528
- return cmd === "send" || !cmd ? 0 : 1;
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
- export {
581
- duration,
582
- main
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.0",
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",