run402 4.7.0 → 4.9.0

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/lib/secrets.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "fs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { reportSdkError, fail } from "./sdk-errors.mjs";
4
- import { assertKnownFlags, flagValue, normalizeArgv, positionalArgs, validateRegularFile } from "./argparse.mjs";
4
+ import { assertKnownFlags, flagValue, normalizeArgv, positionalArgs, resolveProjectSelector, validateRegularFile } from "./argparse.mjs";
5
5
  import { editRequestAction } from "./next-actions.mjs";
6
6
 
7
7
  const HELP = `run402 secrets — Manage project secrets
@@ -10,18 +10,25 @@ Usage:
10
10
  run402 secrets <subcommand> [args...]
11
11
 
12
12
  Subcommands:
13
- set <id> <key> <value> [--file <path>|--stdin] Set a secret on a project
14
- list <id> List all secrets for a project
15
- delete <id> <key> Delete a secret from a project
13
+ set <key> (--value <v> | --file <path> | --stdin) [--project <id>]
14
+ Set a secret on a project
15
+ list [--project <id>] List all secrets for a project
16
+ delete <key> [--project <id>] Delete a secret from a project
17
+
18
+ Legacy (still supported):
19
+ run402 secrets set <prj_id> <key> <value>
20
+ run402 secrets list <prj_id>
21
+ run402 secrets delete <prj_id> <key>
16
22
 
17
23
  Examples:
18
- printf %s "$STRIPE_KEY" | run402 secrets set prj_abc123 STRIPE_KEY --stdin
19
- run402 secrets set prj_abc123 STRIPE_KEY --file ./.secrets/stripe-key
20
- run402 secrets set prj_abc123 TLS_CERT --file cert.pem
21
- run402 secrets list prj_abc123
22
- run402 secrets delete prj_abc123 STRIPE_KEY
24
+ printf %s "$STRIPE_KEY" | run402 secrets set STRIPE_KEY --stdin --project prj_abc123
25
+ run402 secrets set STRIPE_KEY --file ./.secrets/stripe-key --project prj_abc123
26
+ run402 secrets set TLS_CERT --file cert.pem
27
+ run402 secrets list --project prj_abc123
28
+ run402 secrets delete STRIPE_KEY --project prj_abc123
23
29
 
24
30
  Notes:
31
+ - --project defaults to the active project ('run402 projects use')
25
32
  - Secrets are injected as process.env in serverless functions
26
33
  - Values are write-only — list returns keys and timestamps only
27
34
  - Deploy manifests should declare existing keys with secrets.require; never put values in deploy specs
@@ -31,68 +38,86 @@ const SUB_HELP = {
31
38
  set: `run402 secrets set — Set a secret on a project
32
39
 
33
40
  Usage:
34
- run402 secrets set <id> <key> <value>
35
- run402 secrets set <id> <key> --file <path>
36
- run402 secrets set <id> <key> --stdin
41
+ run402 secrets set <key> --value <v> [--project <id>]
42
+ run402 secrets set <key> --file <path> [--project <id>]
43
+ run402 secrets set <key> --stdin [--project <id>]
44
+
45
+ Legacy (still supported):
46
+ run402 secrets set <prj_id> <key> <value>
47
+ run402 secrets set <key> <value>
37
48
 
38
49
  Arguments:
39
- <id> Project ID (from 'run402 projects list')
40
50
  <key> Secret key name (exposed as process.env.<key>)
41
- <value> Inline secret value (omit if using --file or --stdin)
42
51
 
43
52
  Options:
53
+ --project <id> Project ID (defaults to the active project)
54
+ --value <v> Inline secret value (alternative to the legacy positional)
44
55
  --file <path> Read the secret value from a file instead of inline
45
56
  Use --file - or --file /dev/stdin to read from stdin
46
57
  --stdin Read the secret value from stdin until EOF
47
58
 
48
59
  Notes:
60
+ - Provide exactly one value source: --value, --file, --stdin, or the legacy inline positional
49
61
  - Secrets are injected as process.env in serverless functions
50
62
  - Values are write-only; 'list' cannot verify values by hash
51
63
  - Prefer --stdin or --file for real secrets so values do not land in shell history
52
64
 
53
65
  Examples:
54
- printf %s "$STRIPE_KEY" | run402 secrets set prj_abc123 STRIPE_KEY --stdin
55
- cat ./.secrets/stripe-key | run402 secrets set prj_abc123 STRIPE_KEY --file -
56
- run402 secrets set prj_abc123 STRIPE_KEY --file ./.secrets/stripe-key
57
- run402 secrets set prj_abc123 TLS_CERT --file cert.pem
66
+ printf %s "$STRIPE_KEY" | run402 secrets set STRIPE_KEY --stdin --project prj_abc123
67
+ cat ./.secrets/stripe-key | run402 secrets set STRIPE_KEY --file - --project prj_abc123
68
+ run402 secrets set STRIPE_KEY --file ./.secrets/stripe-key
69
+ run402 secrets set TLS_CERT --file cert.pem --project prj_abc123
58
70
  `,
59
71
  list: `run402 secrets list — List all secrets for a project
60
72
 
61
73
  Usage:
62
- run402 secrets list <id>
74
+ run402 secrets list [--project <id>]
63
75
 
64
- Arguments:
65
- <id> Project ID (from 'run402 projects list')
76
+ Legacy (still supported):
77
+ run402 secrets list <prj_id>
78
+
79
+ Options:
80
+ --project <id> Project ID (defaults to the active project)
66
81
 
67
82
  Notes:
68
83
  - Returns secret keys and timestamps only; raw values and value-derived hashes are never returned
69
84
 
70
85
  Examples:
71
- run402 secrets list prj_abc123
86
+ run402 secrets list --project prj_abc123
72
87
  `,
73
88
  delete: `run402 secrets delete — Delete a secret from a project
74
89
 
75
90
  Usage:
76
- run402 secrets delete <id> <key>
91
+ run402 secrets delete <key> [--project <id>]
92
+
93
+ Legacy (still supported):
94
+ run402 secrets delete <prj_id> <key>
77
95
 
78
96
  Arguments:
79
- <id> Project ID (from 'run402 projects list')
80
97
  <key> Secret key name to remove
81
98
 
99
+ Options:
100
+ --project <id> Project ID (defaults to the active project)
101
+
82
102
  Examples:
83
- run402 secrets delete prj_abc123 STRIPE_KEY
103
+ run402 secrets delete STRIPE_KEY --project prj_abc123
84
104
  `,
85
105
  };
86
106
 
107
+ const SET_VALUE_FLAGS = ["--file", "--value", "--project"];
108
+
87
109
  export function readSecretValueForSet(parsedArgs, values, readers = {}) {
88
110
  const readStdin = readers.readStdin ?? (() => readFileSync(0, "utf-8"));
89
111
  const readFile = readers.readFile ?? ((path) => readFileSync(path, "utf-8"));
90
112
  const validateFile = readers.validateFile ?? validateRegularFile;
91
113
  const file = flagValue(parsedArgs, "--file");
114
+ const valueFlagPresent = parsedArgs.includes("--value");
115
+ const valueFlag = valueFlagPresent ? flagValue(parsedArgs, "--value") : null;
92
116
  const stdinRequested = parsedArgs.includes("--stdin");
93
117
  const stdinFile = isStdinAlias(file);
94
118
  const sources = [];
95
119
  if (values.length === 1) sources.push("inline");
120
+ if (valueFlagPresent) sources.push("--value");
96
121
  if (file) sources.push(stdinFile ? "--file stdin" : "--file");
97
122
  if (stdinRequested) sources.push("--stdin");
98
123
 
@@ -101,22 +126,31 @@ export function readSecretValueForSet(parsedArgs, values, readers = {}) {
101
126
  code: "BAD_USAGE",
102
127
  message: "Provide exactly one secret value source.",
103
128
  details: { sources },
104
- hint: "Use one of: inline value, --file <path>, or --stdin.",
129
+ hint: "Use one of: --value <v>, --file <path>, --stdin, or an inline value.",
105
130
  });
106
131
  }
107
132
  if (file && !stdinFile) validateFile(file, "--file");
108
133
 
109
134
  if (stdinRequested || stdinFile) return readStdin();
135
+ if (valueFlagPresent) return valueFlag;
110
136
  if (file) return readFile(file);
111
137
  if (values.length === 1) return values[0];
112
138
  return undefined;
113
139
  }
114
140
 
115
- async function set(projectId, key, args = []) {
141
+ async function set(projectId, args = []) {
116
142
  const parsedArgs = normalizeArgv(args);
117
- const valueFlags = ["--file"];
118
- assertKnownFlags(parsedArgs, [...valueFlags, "--stdin", "--help", "-h"], valueFlags);
119
- const values = positionalArgs(parsedArgs, valueFlags);
143
+ assertKnownFlags(parsedArgs, [...SET_VALUE_FLAGS, "--stdin", "--help", "-h"], SET_VALUE_FLAGS);
144
+ const positionals = positionalArgs(parsedArgs, SET_VALUE_FLAGS);
145
+ const key = positionals[0];
146
+ if (!key) {
147
+ fail({
148
+ code: "BAD_USAGE",
149
+ message: "Missing <key>.",
150
+ hint: "run402 secrets set <key> --value <v> [--project <id>]",
151
+ });
152
+ }
153
+ const values = positionals.slice(1);
120
154
  if (values.length > 1) {
121
155
  fail({ code: "BAD_USAGE", message: `Unexpected argument for secrets set: ${values[1]}` });
122
156
  }
@@ -172,8 +206,8 @@ function failMissingStdin() {
172
206
 
173
207
  async function list(projectId, args = []) {
174
208
  const parsedArgs = normalizeArgv(args);
175
- assertKnownFlags(parsedArgs, ["--help", "-h"]);
176
- const extra = positionalArgs(parsedArgs);
209
+ assertKnownFlags(parsedArgs, ["--project", "--help", "-h"], ["--project"]);
210
+ const extra = positionalArgs(parsedArgs, ["--project"]);
177
211
  if (extra.length > 0) {
178
212
  fail({ code: "BAD_USAGE", message: `Unexpected argument for secrets list: ${extra[0]}` });
179
213
  }
@@ -192,12 +226,20 @@ async function list(projectId, args = []) {
192
226
  }
193
227
  }
194
228
 
195
- async function deleteSecret(projectId, key, args = []) {
229
+ async function deleteSecret(projectId, args = []) {
196
230
  const parsedArgs = normalizeArgv(args);
197
- assertKnownFlags(parsedArgs, ["--help", "-h"]);
198
- const extra = positionalArgs(parsedArgs);
199
- if (extra.length > 0) {
200
- fail({ code: "BAD_USAGE", message: `Unexpected argument for secrets delete: ${extra[0]}` });
231
+ assertKnownFlags(parsedArgs, ["--project", "--help", "-h"], ["--project"]);
232
+ const positionals = positionalArgs(parsedArgs, ["--project"]);
233
+ const key = positionals[0];
234
+ if (!key) {
235
+ fail({
236
+ code: "BAD_USAGE",
237
+ message: "Missing <key>.",
238
+ hint: "run402 secrets delete <key> [--project <id>]",
239
+ });
240
+ }
241
+ if (positionals.length > 1) {
242
+ fail({ code: "BAD_USAGE", message: `Unexpected argument for secrets delete: ${positionals[1]}` });
201
243
  }
202
244
  try {
203
245
  await getSdk().secrets.delete(projectId, key);
@@ -213,10 +255,26 @@ export async function run(sub, args) {
213
255
  console.log(SUB_HELP[sub] || HELP);
214
256
  process.exit(0);
215
257
  }
258
+ if (!["set", "list", "delete"].includes(sub)) {
259
+ fail({ code: "UNKNOWN_SUBCOMMAND", message: `Unknown secrets subcommand: ${sub}`, hint: "Run `run402 secrets --help` for usage.", details: { command: "secrets", subcommand: sub } });
260
+ }
261
+ const parsed = normalizeArgv(Array.isArray(args) ? args : []);
216
262
  switch (sub) {
217
- case "set": await set(args[0], args[1], args.slice(2)); break;
218
- case "list": await list(args[0], args.slice(1)); break;
219
- case "delete": await deleteSecret(args[0], args[1], args.slice(2)); break;
263
+ case "set": {
264
+ const { projectId, rest } = resolveProjectSelector(parsed, { valueFlags: SET_VALUE_FLAGS });
265
+ await set(projectId, rest);
266
+ break;
267
+ }
268
+ case "list": {
269
+ const { projectId, rest } = resolveProjectSelector(parsed, { valueFlags: ["--project"] });
270
+ await list(projectId, rest);
271
+ break;
272
+ }
273
+ case "delete": {
274
+ const { projectId, rest } = resolveProjectSelector(parsed, { valueFlags: ["--project"] });
275
+ await deleteSecret(projectId, rest);
276
+ break;
277
+ }
220
278
  default:
221
279
  fail({ code: "UNKNOWN_SUBCOMMAND", message: `Unknown secrets subcommand: ${sub}`, hint: "Run `run402 secrets --help` for usage.", details: { command: "secrets", subcommand: sub } });
222
280
  }
package/lib/snapshots.mjs CHANGED
@@ -8,24 +8,28 @@ import {
8
8
  normalizeArgv,
9
9
  parseIntegerFlag,
10
10
  positionalArgs,
11
+ resolveProjectSelector,
11
12
  } from "./argparse.mjs";
12
- import { resolveProjectId } from "./config.mjs";
13
13
 
14
14
  const HELP = `run402 snapshots — Project database restore points
15
15
 
16
16
  Usage:
17
- run402 snapshots create [project-id] [--json]
18
- run402 snapshots list [project-id] [--kind <kind>] [--limit <n>] [--after <cursor>] [--json]
19
- run402 snapshots get [project-id] <snapshot-id> [--json]
20
- run402 snapshots restore [project-id] <snapshot-id> [--include-auth] [--confirm <token>] [--json]
21
- run402 snapshots delete [project-id] <snapshot-id> [--json]
17
+ run402 snapshots create [--project <id>] [--json]
18
+ run402 snapshots list [--project <id>] [--kind <kind>] [--limit <n>] [--after <cursor>] [--json]
19
+ run402 snapshots get <snapshot-id> [--project <id>] [--json]
20
+ run402 snapshots restore <snapshot-id> [--project <id>] [--include-auth] [--confirm <token>] [--json]
21
+ run402 snapshots delete <snapshot-id> [--project <id>] [--json]
22
+
23
+ Legacy (still supported): a leading prj_... positional selects the project,
24
+ e.g. run402 snapshots restore prj_abc123 <snapshot-id>. --project defaults to
25
+ the active project.
22
26
 
23
27
  Restore is a two-step handshake. First call without --confirm to get a
24
28
  restore_plan.confirm.token, then re-run with --confirm after reviewing the
25
29
  data-loss statement.
26
30
  `;
27
31
 
28
- const FLAG_VALUES = ["--kind", "--limit", "--after", "--confirm"];
32
+ const FLAG_VALUES = ["--project", "--kind", "--limit", "--after", "--confirm"];
29
33
  const FLAGS = new Set([...FLAG_VALUES, "--include-auth", "--json", "--help", "-h"]);
30
34
  const SNAPSHOT_KINDS = ["manual", "pre_migration", "pre_restore", "scheduled"];
31
35
 
@@ -54,7 +58,7 @@ export async function run(sub, args = []) {
54
58
  }
55
59
 
56
60
  async function create(args) {
57
- const projectId = resolveOptionalProject(positionalArgs(args, FLAG_VALUES)[0]);
61
+ const { projectId } = resolveProjectSelector(args, { valueFlags: FLAG_VALUES });
58
62
  try {
59
63
  const snapshot = await getSdk().snapshots.create(projectId);
60
64
  console.log(JSON.stringify({ ok: snapshot.status === "ready", snapshot }, null, 2));
@@ -64,7 +68,7 @@ async function create(args) {
64
68
  }
65
69
 
66
70
  async function list(args) {
67
- const projectId = resolveOptionalProject(positionalArgs(args, FLAG_VALUES)[0]);
71
+ const { projectId } = resolveProjectSelector(args, { valueFlags: FLAG_VALUES });
68
72
  const kind = flagValue(args, "--kind") ?? undefined;
69
73
  if (kind !== undefined) assertAllowedValue(kind, SNAPSHOT_KINDS, "--kind");
70
74
  const limitFlag = flagValue(args, "--limit");
@@ -124,14 +128,12 @@ async function deleteSnapshot(args) {
124
128
  }
125
129
  }
126
130
 
127
- function resolveOptionalProject(value) {
128
- if (value && String(value).startsWith("prj_")) return value;
129
- return resolveProjectId(null);
130
- }
131
-
132
131
  function resolveProjectAndSnapshot(args, usage) {
133
- const pos = positionalArgs(args, FLAG_VALUES);
134
- if (pos.length === 1) return { projectId: resolveProjectId(null), snapshotId: pos[0] };
135
- if (pos.length === 2 && pos[0].startsWith("prj_")) return { projectId: pos[0], snapshotId: pos[1] };
136
- fail({ code: "BAD_USAGE", message: `Usage: ${usage}` });
132
+ // Canonical: `<snapshot-id> [--project <id>]`; legacy `<prj_id> <snapshot-id>`
133
+ // still works (requireRestPositional keeps a lone snapshot id that happens to
134
+ // start with prj_ from being eaten as the project selector).
135
+ const { projectId, rest } = resolveProjectSelector(args, { valueFlags: FLAG_VALUES, requireRestPositional: true });
136
+ const pos = positionalArgs(rest, FLAG_VALUES);
137
+ if (pos.length !== 1) fail({ code: "BAD_USAGE", message: `Usage: ${usage}` });
138
+ return { projectId, snapshotId: pos[0] };
137
139
  }
package/lib/wallets.mjs CHANGED
@@ -42,7 +42,7 @@ Usage:
42
42
  run402 wallets current Show the resolved active wallet + how it was selected
43
43
  run402 wallets new <name> Create a new named wallet (key stays local)
44
44
  run402 wallets use <name> Set the global default wallet
45
- run402 wallets rename <old> <new> Rename a wallet (migrates the default's files when old=default)
45
+ run402 wallets rename <old> --to <new> Rename a wallet (legacy: rename <old> <new>)
46
46
  run402 wallets bind [<name>] Write ./.run402.json binding this directory to a wallet
47
47
  run402 wallets unbind Remove ./.run402.json
48
48
  run402 wallets import <name> --key <path|-> Adopt an existing private key as a named wallet
@@ -174,9 +174,10 @@ function cmdUse(args) {
174
174
  }
175
175
 
176
176
  async function cmdRename(args) {
177
- const positionals = args.filter((a) => a && !a.startsWith("-"));
177
+ const toFlag = flagVal(args, "--to");
178
+ const positionals = args.filter((a, i) => a && !a.startsWith("-") && args[i - 1] !== "--to");
178
179
  const oldName = requireName(positionals[0], "old wallet name");
179
- const newName = requireName(positionals[1], "new wallet name");
180
+ const newName = requireName(toFlag ?? positionals[1], "new wallet name");
180
181
  if (newName === DEFAULT) {
181
182
  fail({ code: "BAD_WALLET_NAME", message: "Cannot rename a wallet to the reserved name 'default'.", details: { name: newName } });
182
183
  }
@@ -27,6 +27,12 @@ export async function run(sub, args = []) {
27
27
  console.log(HELP);
28
28
  process.exit(0);
29
29
  }
30
+ // --help/-h anywhere in the argv must short-circuit BEFORE auth/config side
31
+ // effects (the cli-help.test.mjs contract) — same check as notifications.mjs.
32
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
33
+ console.log(HELP);
34
+ process.exit(0);
35
+ }
30
36
  if (sub !== "rotate") {
31
37
  fail({
32
38
  code: "UNKNOWN_SUBCOMMAND",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "4.7.0",
3
+ "version": "4.9.0",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -126,9 +126,50 @@ export type NotificationPreferencesPatch = Partial<Omit<NotificationPreferences,
126
126
  /** Cannot be changed away from "always" — server returns 400. */
127
127
  security_events?: "always";
128
128
  };
129
+ export interface TestNotificationOptions {
130
+ /**
131
+ * Route the synthetic test event as if it came from the app lane
132
+ * (`project_events.source = 'app'`) or the platform. Defaults to
133
+ * `"platform"` on the gateway when omitted.
134
+ */
135
+ source?: "app" | "platform";
136
+ /**
137
+ * Synthetic `event_type` override (flat snake_case,
138
+ * `^[a-z][a-z0-9_]{2,63}$`). Use this to exercise a specific routing rule's
139
+ * `event_types` filter precisely. Defaults to the gateway's built-in
140
+ * sample event when omitted.
141
+ */
142
+ eventType?: string;
143
+ }
144
+ /** One Telegram destination's outcome from a `testNotification()` call —
145
+ * present only when the operator has a routing rule matching the synthetic
146
+ * event. Empty `telegram.destinations` is Faithful (no matching rule), not
147
+ * an error. */
148
+ export interface TestNotificationDestination {
149
+ binding_id: string;
150
+ label: string | null;
151
+ delivered: boolean;
152
+ /** Present on failures. `true` = retryable (429/5xx/timeout/rate-limited);
153
+ * `false` = permanent (bad chat, bot blocked/removed). */
154
+ transient?: boolean;
155
+ description?: string;
156
+ }
129
157
  export interface TestNotificationResult {
130
- status: "queued";
158
+ status: "delivered" | "skipped" | "queued";
131
159
  source_event_id: string;
160
+ drained: {
161
+ claimed: number;
162
+ delivered: number;
163
+ skipped: number;
164
+ failed_transient: number;
165
+ failed_permanent: number;
166
+ };
167
+ /** Telegram delivery report for the synthetic event, routed through the
168
+ * operator's normal rules — the full binding + rule + render + send
169
+ * chain, not just email/webhook. */
170
+ telegram: {
171
+ destinations: TestNotificationDestination[];
172
+ };
132
173
  note: string;
133
174
  }
134
175
  export interface RotateWebhookSecretResult {
@@ -137,6 +178,200 @@ export interface RotateWebhookSecretResult {
137
178
  grace_window_hours: number;
138
179
  note: string;
139
180
  }
181
+ export type TelegramBindingStatus = "pending" | "active" | "revoked";
182
+ /**
183
+ * One Telegram binding as returned by `GET /agent/v1/notifications/channels`
184
+ * (`telegram[]`) and `r.admin.channels.list()`. Never carries the raw
185
+ * connect code — codes are single-use, hashed at rest, and returned only
186
+ * once, inline in {@link ConnectTelegramResult}.
187
+ */
188
+ export interface TelegramChannelBinding {
189
+ id: string;
190
+ recipient_email: string;
191
+ status: TelegramBindingStatus;
192
+ chat_id: number | null;
193
+ chat_type: string | null;
194
+ chat_title: string | null;
195
+ label: string | null;
196
+ consecutive_failures: number;
197
+ /** Set once auto-disabled after 10 consecutive hard delivery failures. */
198
+ disabled_at: string | null;
199
+ /** Only set while `status === "pending"` — the connect code's 15-min TTL. */
200
+ code_expires_at: string | null;
201
+ created_at: string;
202
+ activated_at: string | null;
203
+ }
204
+ export interface ConnectTelegramOptions {
205
+ /** Human-readable label for the chat (e.g. `"kychon alerts"`), 1-64 chars. */
206
+ label?: string;
207
+ }
208
+ export interface ConnectTelegramNextAction {
209
+ type: string;
210
+ method?: string;
211
+ path?: string;
212
+ why?: string;
213
+ }
214
+ export interface ConnectTelegramResult {
215
+ binding_id: string;
216
+ status: "pending";
217
+ /** `t.me/<bot>?start=<code>` — tap to bind a PRIVATE chat. Single-use, 15-min TTL. */
218
+ connect_url: string;
219
+ /** `t.me/<bot>?startgroup=<code>` — tap to bind a GROUP chat. Same code/TTL as {@link connect_url} (whichever is tapped first consumes it). */
220
+ connect_group_url: string;
221
+ code_expires_at: string;
222
+ label: string | null;
223
+ next_actions: ConnectTelegramNextAction[];
224
+ }
225
+ export interface NotificationChannelsResult {
226
+ email: {
227
+ address: string | null;
228
+ verified: boolean;
229
+ };
230
+ webhook: {
231
+ configured: boolean;
232
+ url: string | null;
233
+ secret_configured: boolean;
234
+ };
235
+ /** Every live (non-revoked) Telegram binding for this operator, newest first. */
236
+ telegram: TelegramChannelBinding[];
237
+ }
238
+ export interface RevokeTelegramResult {
239
+ status: "revoked";
240
+ binding_id: string;
241
+ }
242
+ /**
243
+ * `"app"` = app-emitted business events (`project_events.source = 'app'`,
244
+ * e.g. `events.emit(...)` from `@run402/functions`); `"platform"` = every
245
+ * non-app platform event (deploys, lifecycle, verification, ...). Absent /
246
+ * `null` on a rule is a wildcard — matches both.
247
+ */
248
+ export type RoutingRuleSource = "app" | "platform";
249
+ /**
250
+ * Wire-shaped routing rule. Every match dimension (`project_id`, `source`,
251
+ * `event_types`, `classes`) is ANDed; `null` is a wildcard for that
252
+ * dimension. An explicit empty array (`event_types: []` / `classes: []`)
253
+ * matches NOTHING — Postgres `TEXT[]` semantics, deliberately different from
254
+ * the "`[]` means unfiltered" convention used by some read-filter query
255
+ * params elsewhere in this SDK. One rule always targets exactly one Telegram
256
+ * binding; overlapping rules that resolve to the same binding are deduped by
257
+ * the gateway at delivery time (one message, not one per matching rule).
258
+ */
259
+ export interface RoutingRule {
260
+ id: string;
261
+ recipient_email: string;
262
+ project_id: string | null;
263
+ source: RoutingRuleSource | null;
264
+ event_types: string[] | null;
265
+ classes: string[] | null;
266
+ channel: "telegram";
267
+ telegram_binding_id: string;
268
+ enabled: boolean;
269
+ created_at: string;
270
+ updated_at: string;
271
+ }
272
+ /**
273
+ * `r.admin.rules.create(...)` input. Every match dimension is optional
274
+ * (absent = wildcard); `telegramBindingId` is the only required field. An
275
+ * all-wildcard rule (every event on every project routes to one chat) is
276
+ * legal.
277
+ */
278
+ export interface CreateRoutingRuleInput {
279
+ telegramBindingId: string;
280
+ projectId?: string | null;
281
+ source?: RoutingRuleSource | null;
282
+ eventTypes?: string[] | null;
283
+ classes?: string[] | null;
284
+ }
285
+ /**
286
+ * `r.admin.rules.update(...)` patch. PATCH semantics: a field OMITTED from
287
+ * this object leaves the stored value unchanged; a field explicitly set to
288
+ * `null` CLEARS that dimension back to wildcard. There is no wire difference
289
+ * between "omitted" and "set to `undefined`" — both drop the key from the
290
+ * JSON request body, so the gateway sees no instruction to change it.
291
+ */
292
+ export interface UpdateRoutingRulePatch {
293
+ projectId?: string | null;
294
+ source?: RoutingRuleSource | null;
295
+ eventTypes?: string[] | null;
296
+ classes?: string[] | null;
297
+ telegramBindingId?: string;
298
+ enabled?: boolean;
299
+ }
300
+ export interface ListRoutingRulesResult {
301
+ rules: RoutingRule[];
302
+ }
303
+ export interface CreateRoutingRuleResult extends RoutingRule {
304
+ next_actions: ConnectTelegramNextAction[];
305
+ }
306
+ export interface DeleteRoutingRuleResult {
307
+ deleted: true;
308
+ rule_id: string;
309
+ }
310
+ /**
311
+ * `r.admin.channels` — the Telegram notification-channel binding lifecycle
312
+ * (connect / list / revoke). Mutations (`connectTelegram`, `revokeTelegram`)
313
+ * require `operator_passkey` assurance; `connectTelegram` additionally
314
+ * requires a VERIFIED operator email (bindings are addressed to it). See
315
+ * `r.admin.setAgentContact` / `r.admin.verifyAgentContactEmail` and
316
+ * `r.admin.startOperatorPasskeyEnrollment` to reach that assurance level —
317
+ * same ladder as {@link Admin.rotateWebhookSecret}.
318
+ */
319
+ export declare class Channels {
320
+ private readonly client;
321
+ constructor(client: Client);
322
+ /**
323
+ * Start binding a Telegram chat. Returns two single-use, 15-minute deep
324
+ * links — `connect_url` for a private chat, `connect_group_url` for a
325
+ * group — plus a `pending` binding id. A human taps ONE of the links and
326
+ * starts the bot; poll {@link Channels.list} until the binding's `status`
327
+ * flips to `"active"` (or `code_expires_at` passes and it's swept back to
328
+ * `"revoked"`).
329
+ *
330
+ * Throws (via the generic SDK error hierarchy — check `err.code`) HTTP 503
331
+ * `TELEGRAM_CHANNEL_NOT_CONFIGURED` until the platform's dedicated
332
+ * notification bot is provisioned, and HTTP 412
333
+ * `OPERATOR_EMAIL_NOT_VERIFIED` when the caller has no verified email yet.
334
+ */
335
+ connectTelegram(opts?: ConnectTelegramOptions): Promise<ConnectTelegramResult>;
336
+ /** List every notification channel — email, webhook, and every live
337
+ * (non-revoked) Telegram binding — for the authenticated wallet. */
338
+ list(): Promise<NotificationChannelsResult>;
339
+ /**
340
+ * Revoke a Telegram binding. Missing / already-revoked / another
341
+ * operator's binding id all return the SAME not-found error
342
+ * (authorize-before-reveal) — no existence oracle.
343
+ */
344
+ revokeTelegram(bindingId: string): Promise<RevokeTelegramResult>;
345
+ }
346
+ /**
347
+ * `r.admin.rules` — Telegram routing rules (design D4): one match (ANDed
348
+ * dimensions; an omitted dimension is a wildcard) → one Telegram binding.
349
+ * Rules govern the Telegram channel ONLY in v1 — email/webhook keep their
350
+ * existing preference-toggle semantics untouched. Mutations require
351
+ * `operator_passkey` assurance.
352
+ */
353
+ export declare class Rules {
354
+ private readonly client;
355
+ constructor(client: Client);
356
+ /** List the operator's routing rules, newest first. */
357
+ list(): Promise<ListRoutingRulesResult>;
358
+ /**
359
+ * Create a routing rule. `telegramBindingId` must reference a binding this
360
+ * operator owns and that is currently usable (`status: "active"`); an
361
+ * unusable or foreign binding id returns the same 404 as a nonexistent one
362
+ * (authorize-before-reveal).
363
+ */
364
+ create(input: CreateRoutingRuleInput): Promise<CreateRoutingRuleResult>;
365
+ /**
366
+ * Patch a routing rule. PATCH semantics: only fields PRESENT on `patch`
367
+ * are sent — `{ projectId: null }` clears that dimension back to
368
+ * wildcard; omitting a field leaves it unchanged (see
369
+ * {@link UpdateRoutingRulePatch}).
370
+ */
371
+ update(ruleId: string, patch: UpdateRoutingRulePatch): Promise<RoutingRule>;
372
+ /** Delete a routing rule. */
373
+ delete(ruleId: string): Promise<DeleteRoutingRuleResult>;
374
+ }
140
375
  export interface SetLeasePerpetualResult {
141
376
  status: "ok";
142
377
  org_id: string;
@@ -246,6 +481,16 @@ export declare class Admin {
246
481
  * preview, accept, claim, cancel, listIncoming, listOutgoing}`.
247
482
  */
248
483
  readonly transfers: Transfers;
484
+ /**
485
+ * Telegram notification-channel binding lifecycle. Access via
486
+ * `r.admin.channels.{connectTelegram, list, revokeTelegram}`.
487
+ */
488
+ readonly channels: Channels;
489
+ /**
490
+ * Telegram routing rules — one match (ANDed dimensions) to one binding.
491
+ * Access via `r.admin.rules.{list, create, update, delete}`.
492
+ */
493
+ readonly rules: Rules;
249
494
  constructor(client: Client);
250
495
  /**
251
496
  * Operator-scoped sub-client for an org id — the operator analog of
@@ -280,10 +525,15 @@ export declare class Admin {
280
525
  setNotificationPreferences(patch: NotificationPreferencesPatch): Promise<NotificationPreferences>;
281
526
  /**
282
527
  * Trigger a real test notification. Sends a sample `project_past_due`
283
- * event through the normal worker pipeline; the audit row is marked
284
- * `is_test: true`. Rate-limited per wallet at 1/min.
528
+ * event through the normal worker pipeline (email/webhook); the audit row
529
+ * is marked `is_test: true`. ALSO delivers a synthetic event through the
530
+ * operator's Telegram routing rules end-to-end (binding + rule + render +
531
+ * send) and reports a per-destination outcome in `telegram.destinations`
532
+ * — pass `opts.source` / `opts.eventType` to target a specific rule's
533
+ * filters instead of the default sample event. Rate-limited per wallet at
534
+ * 1/min.
285
535
  */
286
- testNotification(): Promise<TestNotificationResult>;
536
+ testNotification(opts?: TestNotificationOptions): Promise<TestNotificationResult>;
287
537
  /**
288
538
  * Rotate the operator's webhook signing secret. The new plaintext secret
289
539
  * is returned EXACTLY once. The previous secret remains valid for 24