@columenlabs/lumen-ops 0.1.36 → 0.1.38

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
@@ -1,6 +1,6 @@
1
1
  # @columenlabs/lumen-ops
2
2
 
3
- Lumen's ops meters for Cloudflare Workers: every data query your app runs becomes one Analytics Engine point, so you can find slow and heavy queries and fix them. The meter wraps your bindings once, so new code is metered without opting in. It writes only each query's shape, never a value.
3
+ Lumen's ops parts for Cloudflare Workers: every data query your app runs becomes one Analytics Engine point, so you can find slow and heavy queries and fix them. The meter wraps your bindings once, so new code is metered without opting in. It writes only each query's shape, never a value. It also meters tool calls (`/tools`) and sends signed webhooks (`/webhooks`).
4
4
 
5
5
  - Docs: https://lumen.columenlabs.com/docs/
6
6
  - For agents: https://lumen.columenlabs.com/llms.txt
@@ -124,6 +124,32 @@ Arguments and results are never written; only their sizes.
124
124
  | `meteredCall(point, fn, errorKind?)` | Runs `fn`, timing it and writing its outcome |
125
125
  | `codeHash(code)` | A short hash of a script, whitespace folded |
126
126
 
127
+ ## Webhooks out
128
+
129
+ `@columenlabs/lumen-ops/webhooks` sends one delivery, signed per the [Standard Webhooks](https://www.standardwebhooks.com) spec when a secret is given, so the receiver can verify it with any Standard Webhooks library. It never throws: the result says how it went, for the app's own delivery log and retries.
130
+
131
+ ```ts
132
+ import { newWebhookSecret, sendWebhook } from '@columenlabs/lumen-ops/webhooks';
133
+
134
+ const secret = newWebhookSecret(); // whsec_…, shown to its owner once, kept sealed
135
+ const r = await sendWebhook({
136
+ url: endpoint.url,
137
+ secret,
138
+ id: event.id, // the same id on a retry
139
+ body: JSON.stringify(event.payload),
140
+ headers: { authorization: `Bearer ${endpoint.token}` },
141
+ userAgent: 'MyApp-Webhooks/1',
142
+ });
143
+ if (!r.ok) scheduleRetry(event, r.status, r.error);
144
+ ```
145
+
146
+ | Export | What it does |
147
+ |---|---|
148
+ | `sendWebhook(send)` | One POST: `{ ok, status, ms, id, error? }`; status 0 when the endpoint never answered |
149
+ | `newWebhookSecret()` | A new `whsec_` secret, 32 random bytes |
150
+
151
+ The endpoint's own headers go first, so they can never replace the signature headers. Redirects count as failures unless `redirect` says otherwise; the timeout is 10 s by default.
152
+
127
153
  ## Versions
128
154
 
129
155
  0.x: any minor may break things until 1.0. Every merge to Lumen's `main` publishes the next patch of every Lumen package; see the [releases](https://github.com/columenlabs/lumen/releases).
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './points.js';
2
2
  export * from './queries.js';
3
3
  export * from './tools.js';
4
+ export * from './webhooks.js';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './points.js';
2
2
  export * from './queries.js';
3
3
  export * from './tools.js';
4
+ export * from './webhooks.js';
@@ -0,0 +1,34 @@
1
+ export interface WebhookSend {
2
+ url: string;
3
+ /** The body as sent and signed, usually JSON. */
4
+ body: string;
5
+ /** Signs the delivery: `whsec_…` (see `newWebhookSecret`). Left out, nothing is signed. */
6
+ secret?: string;
7
+ /** The message id the signature covers; the same id on a retry. Made up when left out. */
8
+ id?: string;
9
+ /** The endpoint's own headers (its auth). They can never replace the signature headers. */
10
+ headers?: Record<string, string>;
11
+ /** Default `application/json`. */
12
+ contentType?: string;
13
+ userAgent?: string;
14
+ /** Default 10 s. */
15
+ timeoutMs?: number;
16
+ /** Default `manual`: a redirect counts as a failed delivery, never followed. */
17
+ redirect?: RequestRedirect;
18
+ /** For tests or a service binding; default the global fetch. */
19
+ fetch?: typeof fetch;
20
+ }
21
+ export interface WebhookResult {
22
+ ok: boolean;
23
+ /** The endpoint's HTTP status; 0 when it never answered (timeout, DNS, refused). */
24
+ status: number;
25
+ ms: number;
26
+ /** The message id sent. */
27
+ id: string;
28
+ /** On a failure: the first 200 characters of the answer, or why there was none. */
29
+ error?: string;
30
+ }
31
+ /** A new signing secret, `whsec_` and 32 random bytes in base64. Show it to its owner once. */
32
+ export declare function newWebhookSecret(): string;
33
+ /** Sends one delivery. */
34
+ export declare function sendWebhook(send: WebhookSend): Promise<WebhookResult>;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Webhooks out: one POST to an endpoint, signed per the Standard Webhooks spec
3
+ * (https://www.standardwebhooks.com) when a secret is given, so any of its libraries verifies it.
4
+ * The send never throws: it returns how it went, for the app's own delivery log and retries.
5
+ */
6
+ import { Webhook } from 'standardwebhooks';
7
+ /** A new signing secret, `whsec_` and 32 random bytes in base64. Show it to its owner once. */
8
+ export function newWebhookSecret() {
9
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
10
+ return `whsec_${btoa(String.fromCharCode(...bytes))}`;
11
+ }
12
+ /** Sends one delivery. */
13
+ export async function sendWebhook(send) {
14
+ const id = send.id ?? `msg_${crypto.randomUUID().replaceAll('-', '')}`;
15
+ const at = new Date();
16
+ const signed = send.secret
17
+ ? {
18
+ 'webhook-id': id,
19
+ 'webhook-timestamp': String(Math.floor(at.getTime() / 1000)),
20
+ 'webhook-signature': new Webhook(send.secret).sign(id, at, send.body),
21
+ }
22
+ : {};
23
+ const started = Date.now();
24
+ try {
25
+ const res = await (send.fetch ?? fetch)(send.url, {
26
+ method: 'POST',
27
+ redirect: send.redirect ?? 'manual',
28
+ signal: AbortSignal.timeout(send.timeoutMs ?? 10_000),
29
+ headers: {
30
+ ...send.headers,
31
+ 'content-type': send.contentType ?? 'application/json',
32
+ ...(send.userAgent ? { 'user-agent': send.userAgent } : {}),
33
+ ...signed,
34
+ },
35
+ body: send.body,
36
+ });
37
+ const ok = res.status >= 200 && res.status < 300;
38
+ let error;
39
+ if (ok)
40
+ await res.body?.cancel();
41
+ else
42
+ error = (await res.text().catch(() => '')).slice(0, 200) || `HTTP ${res.status}`;
43
+ return { ok, status: res.status, ms: Date.now() - started, id, ...(error ? { error } : {}) };
44
+ }
45
+ catch (err) {
46
+ const error = err instanceof Error ? err.message : String(err);
47
+ return { ok: false, status: 0, ms: Date.now() - started, id, error: error.slice(0, 200) };
48
+ }
49
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ops",
3
- "version": "0.1.36",
4
- "description": "Lumen's ops meters: every data query and every tool call an app runs as one Analytics Engine point, by shape and hash only.",
3
+ "version": "0.1.38",
4
+ "description": "Lumen's ops parts: every data query and tool call an app runs as one Analytics Engine point (shapes and hashes only), and signed webhooks out.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
@@ -22,6 +22,10 @@
22
22
  "./tools": {
23
23
  "types": "./dist/tools.d.ts",
24
24
  "default": "./dist/tools.js"
25
+ },
26
+ "./webhooks": {
27
+ "types": "./dist/webhooks.d.ts",
28
+ "default": "./dist/webhooks.js"
25
29
  }
26
30
  },
27
31
  "files": [
@@ -43,5 +47,7 @@
43
47
  "access": "public",
44
48
  "registry": "https://registry.npmjs.org/"
45
49
  },
46
- "dependencies": {}
50
+ "dependencies": {
51
+ "standardwebhooks": "1.1.1"
52
+ }
47
53
  }