EXTENSION for treeos-connect
gateway-webhook
Web push channel type for the gateway extension. Registers the webapp channel type at boot, enabling trees to push notifications to web browsers using the Web Push protocol (RFC 8030). This is output-only: browsers receive notifications from the tree, but cannot send messages back through this channel. For bidirectional browser communication, use the WebSocket connection or a different channel type. Configuration requires a Push API subscription object from the browser (containing endpoint, keys.p256dh, and keys.auth), which is stored encrypted in the channel's secrets. VAPID keys (public, private, and contact email) are set in environment variables and used for all web push channels on the land. The extension dynamically imports the web-push npm package at send time, so the package must be installed but the extension boots without it. When a notification fires, the handler serializes the title, body, and type into a JSON payload and delivers it through the web-push library, which handles VAPID signing, payload encryption, and the HTTP/2 push to the browser's push service endpoint. Expired subscriptions (HTTP 410 Gone) are handled gracefully: the channel is automatically disabled and the error is recorded, preventing repeated delivery attempts to a dead subscription. This means channels self-heal when a user clears their browser data or revokes notification permissions.
v1.0.1 by TreeOS Site 0 downloads 3 files 157 lines 5.5 KB published 38d ago
treeos ext install gateway-webhook
View changelog

Manifest

Requires

  • models: Node, User
  • extensions: gateway
SHA256: 54e2fb4a4d7cb4cfc8fdcbd31be39e1f51cc9f50e87675dba509458da2bdbf0d

Dependents

1 package depend on this

PackageTypeRelationship
treeos-connect v1.0.3bundleincludes

Environment Variables

KeyRequiredDescription
VAPID_PUBLIC_KEY No VAPID public key for web push
VAPID_PRIVATE_KEY secret No VAPID private key for web push
VAPID_EMAIL No VAPID contact email for web push

Source Code

1// Web push (webhook) channel type handler.
2// Registered with gateway core during init. Output only.
3
4import log from "../../seed/log.js";
5
6// ─────────────────────────────────────────────────────────────────────────
7// VALIDATION
8// ─────────────────────────────────────────────────────────────────────────
9
10function validateConfig(config, direction) {
11  if (!config.subscription || typeof config.subscription !== "object") {
12    throw new Error("Web push channel requires a subscription object");
13  }
14  if (
15    !config.subscription.endpoint ||
16    typeof config.subscription.endpoint !== "string"
17  ) {
18    throw new Error("Web push subscription must have an endpoint");
19  }
20}
21
22function buildEncryptedConfig(config, direction) {
23  return {
24    secrets: { subscription: config.subscription },
25    metadata: {},
26    displayIdentifier: config.displayIdentifier || null,
27  };
28}
29
30// ─────────────────────────────────────────────────────────────────────────
31// SENDER
32// ─────────────────────────────────────────────────────────────────────────
33
34async function send(secrets, metadata, notification) {
35  const { subscription } = secrets;
36
37  let webpush;
38  try {
39    webpush = await import("web-push");
40    webpush = webpush.default || webpush;
41  } catch {
42    throw new Error("web-push package not installed");
43  }
44
45  const vapidPublic = process.env.VAPID_PUBLIC_KEY;
46  const vapidPrivate = process.env.VAPID_PRIVATE_KEY;
47  const vapidEmail = process.env.VAPID_EMAIL;
48
49  if (!vapidPublic || !vapidPrivate || !vapidEmail) {
50    throw new Error("VAPID keys not configured");
51  }
52
53  const email = vapidEmail.startsWith("mailto:") ? vapidEmail : "mailto:" + vapidEmail;
54  webpush.setVapidDetails(email, vapidPublic, vapidPrivate);
55
56  const payload = JSON.stringify({
57    title: notification.title,
58    body: notification.content,
59    type: notification.type,
60  });
61
62  try {
63    await webpush.sendNotification(subscription, payload);
64  } catch (err) {
65    if (err.statusCode === 410) {
66      // Subscription expired, disable the channel
67      try {
68        const { getExtension } = await import("../loader.js");
69        const GatewayChannel = getExtension("gateway")?.exports?.GatewayChannel;
70        await GatewayChannel.findByIdAndUpdate(notification._channelId, {
71          $set: { enabled: false, lastError: "Push subscription expired (410 Gone)" },
72        });
73      } catch (err2) { log.warn("GatewayWebhook", "failed to disable expired channel:", err2.message); }
74      throw new Error("Push subscription expired");
75    }
76    throw err;
77  }
78}
79
80// ─────────────────────────────────────────────────────────────────────────
81// EXPORT HANDLER
82// ─────────────────────────────────────────────────────────────────────────
83
84export default {
85  allowedDirections: ["output"],
86  validateConfig,
87  buildEncryptedConfig,
88  send,
89};
90
1import log from "../../seed/log.js";
2import handler from "./handler.js";
3import { getExtension } from "../loader.js";
4
5export async function init(core) {
6  const gateway = getExtension("gateway");
7  if (!gateway?.exports?.registerChannelType) {
8    throw new Error("gateway-webhook requires the gateway extension to be loaded first");
9  }
10
11  gateway.exports.registerChannelType("webapp", handler);
12  log.verbose("GatewayWebhook", "Registered webapp (webhook) channel type");
13
14  return {};
15}
16
1export default {
2  name: "gateway-webhook",
3  version: "1.0.1",
4  builtFor: "treeos-connect",
5  description:
6    "Web push channel type for the gateway extension. Registers the webapp channel type at boot, " +
7    "enabling trees to push notifications to web browsers using the Web Push protocol (RFC 8030). " +
8    "This is output-only: browsers receive notifications from the tree, but cannot send messages " +
9    "back through this channel. For bidirectional browser communication, use the WebSocket " +
10    "connection or a different channel type.\n\n" +
11    "Configuration requires a Push API subscription object from the browser (containing endpoint, " +
12    "keys.p256dh, and keys.auth), which is stored encrypted in the channel's secrets. VAPID keys " +
13    "(public, private, and contact email) are set in environment variables and used for all web " +
14    "push channels on the land. The extension dynamically imports the web-push npm package at send " +
15    "time, so the package must be installed but the extension boots without it.\n\n" +
16    "When a notification fires, the handler serializes the title, body, and type into a JSON " +
17    "payload and delivers it through the web-push library, which handles VAPID signing, payload " +
18    "encryption, and the HTTP/2 push to the browser's push service endpoint. Expired subscriptions " +
19    "(HTTP 410 Gone) are handled gracefully: the channel is automatically disabled and the error " +
20    "is recorded, preventing repeated delivery attempts to a dead subscription. This means " +
21    "channels self-heal when a user clears their browser data or revokes notification permissions.",
22
23  needs: {
24    models: ["Node", "User"],
25    extensions: ["gateway"],
26  },
27
28  optional: {},
29
30  provides: {
31    models: {},
32    routes: false,
33    tools: false,
34    jobs: false,
35    orchestrator: false,
36    energyActions: {},
37    sessionTypes: {},
38    env: [
39      { key: "VAPID_PUBLIC_KEY", required: false, description: "VAPID public key for web push" },
40      { key: "VAPID_PRIVATE_KEY", required: false, secret: true, description: "VAPID private key for web push" },
41      { key: "VAPID_EMAIL", required: false, description: "VAPID contact email for web push" },
42    ],
43    cli: [],
44
45    hooks: {
46      fires: [],
47      listens: [],
48    },
49  },
50};
51

Versions

Version Published Downloads
1.0.1 38d ago 0
1.0.0 48d ago 0
0 stars
0 flags
React from the CLI: treeos ext star gateway-webhook

Comments

Loading comments...

Post comments from the CLI: treeos ext comment gateway-webhook "your comment"
Max 3 comments per extension. One star and one flag per user.