@amerged/ohmyhost-cli 0.1.43 → 0.1.45

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.
@@ -38879,12 +38879,12 @@ var failures = {
38879
38879
  "Do not repeat the write with a new key. Inspect the target data with a read query and retain this operation ID. The same key only reads this receipt."
38880
38880
  ],
38881
38881
  database_compute_failed: [
38882
- "The database size change failed at the provider.",
38883
- "Read the current database compute and report this operation ID through feedback. Preserve data; do not reset or redeploy the database to repair its size."
38882
+ "The database provider refused the size change or had no room for it. The database keeps its previous size and keeps running, and no credits are charged for the failed change.",
38883
+ "Read the current size with 'ohmyhost database compute get --project PROJECT_ULID --environment dev|prod --json' or MCP database_compute_get, then repeat the change with a new idempotency key; the old key only returns this failure. If it fails again after a few minutes, start a new deployment of the project, repeat the change and report this operation ID through feedback. Never reset the database to change its size."
38884
38884
  ],
38885
38885
  database_compute_plan_changed: [
38886
- "Paid access ended before the database size change started.",
38887
- "Read the organization's plan and current database compute, then confirm a new standard-size request. This operation did not resize the database."
38886
+ "Paid access ended before the database size change started, so it was not sent to the database provider. The database keeps its previous size and keeps running, and no credits are charged for the change.",
38887
+ "Read the organization's plan with 'ohmyhost credits account --organization ORGANIZATION_ULID --json' or MCP organization_account_get. Sizes above 256 MiB need active Paid: ask the organization Owner to activate Paid through billing checkout, then read the current size with database compute get and repeat the change with a new idempotency key."
38888
38888
  ],
38889
38889
  database_compute_rejected: [
38890
38890
  "The database size change could not be started.",
@@ -40219,6 +40219,36 @@ var PROBLEM_POLICY = {
40219
40219
  action: "The platform is still retrying this webhook delivery. Read the delivery in the delivery list; replay it only if it ends dead_lettered or the receiver needs the event again.",
40220
40220
  title: "Webhook delivery still pending"
40221
40221
  },
40222
+ database_watch_limit: {
40223
+ status: 409,
40224
+ retryable: false,
40225
+ action: "List the project's database watches for this environment and delete one you no longer need, then create the watch again.",
40226
+ title: "Database watch limit reached"
40227
+ },
40228
+ database_watch_table_unavailable: {
40229
+ status: 409,
40230
+ retryable: false,
40231
+ action: "The table must exist as an ordinary table owned by the application, created by a deployed migration; views, partitions, temporary tables and the schemas pgcf, ohmyhost, information_schema and pg_* cannot be watched. Fix the name or deploy the migration, then create the watch again.",
40232
+ title: "Table cannot be watched"
40233
+ },
40234
+ database_watch_column_unavailable: {
40235
+ status: 409,
40236
+ retryable: false,
40237
+ action: "List only existing column names exactly as written, names are case-sensitive. Delete the watch before a migration drops a published column, because the write would fail.",
40238
+ title: "Watched column does not exist"
40239
+ },
40240
+ database_watch_events_unavailable: {
40241
+ status: 409,
40242
+ retryable: false,
40243
+ action: "The platform event schema is installed when the database wakes: run any query against that environment, wait for the answer, then create the watch again. If it still fails, report the request_id through feedback.",
40244
+ title: "Database events not installed yet"
40245
+ },
40246
+ database_watch_conflict: {
40247
+ status: 409,
40248
+ retryable: false,
40249
+ action: "A migration already attaches a capture trigger to this table: keep it and skip the watch, or remove it from the migrations, deploy and create the watch again. A table carries one capture trigger.",
40250
+ title: "Table already has a capture trigger"
40251
+ },
40222
40252
  project_notes_conflict: {
40223
40253
  status: 409,
40224
40254
  retryable: false,
@@ -40483,7 +40513,7 @@ var PROBLEM_POLICY = {
40483
40513
  api_key_permissions_unavailable: {
40484
40514
  status: 503,
40485
40515
  retryable: false,
40486
- action: "Report the platform user-key permission configuration through feedback. Repeated login or creation will not fix it.",
40516
+ action: "Report the platform user-key permission configuration through feedback with this request_id; it affects every token the same way, whatever the integration. Repeated login does not fix it. After the platform confirms the fix, repeat the same create with the same name and Idempotency-Key.",
40487
40517
  title: "Token permissions unavailable"
40488
40518
  },
40489
40519
  reconciliation_exhausted: {
@@ -42389,6 +42419,9 @@ var DATABASE_WEBHOOK_EVENT_TYPES = [
42389
42419
  "database.resized",
42390
42420
  "database.deleted"
42391
42421
  ];
42422
+ var DATABASE_WATCH_OPERATIONS = ["insert", "update", "delete"];
42423
+ var DATABASE_WATCH_MAX_COLUMNS = 32;
42424
+ var DATABASE_WATCH_DEFAULT_SCHEMA = "public";
42392
42425
  var DATABASE_WEBHOOK_MAX_EVENT_TYPES = 16;
42393
42426
  var DATABASE_WEBHOOK_MAX_HEADERS = 8;
42394
42427
  var DATABASE_WEBHOOK_URL_MAX_LENGTH = 2048;
@@ -42432,7 +42465,22 @@ var USAGE_MAX_RANGE_MS = 31 * 24 * USAGE_HOUR_MS;
42432
42465
  var WEBHOOK_KEYS = ["id", "environment", "url", "event_types", "header_names", "created_at"];
42433
42466
  var WEBHOOK_ID_PATTERN = /^whk_[a-z0-9]{20}$/u;
42434
42467
  var DELIVERY_ID_PATTERN = /^dlv_[0-9a-f]{20}$/u;
42435
- var EVENT_ID_PATTERN = /^evt_[0-9a-f]{20}$/u;
42468
+ var EVENT_ID_PATTERN = /^(?:evt_[0-9a-f]{20}|[a-z][a-z0-9]{19}:[1-9][0-9]{0,15}:[1-9][0-9]{0,15})$/u;
42469
+ var DATA_SUBSCRIPTION_PATTERN = /^data\.(?:\*|[a-z][a-z0-9_]{0,47})$/u;
42470
+ var DATA_EVENT_TYPE_PATTERN = /^data\.[a-z][a-z0-9_]{0,47}$/u;
42471
+ var DATABASE_WATCH_ID_PATTERN = /^pgcf_watch_[0-9a-f]{8}$/u;
42472
+ var DATABASE_WATCH_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/u;
42473
+ var DATABASE_WATCH_FORBIDDEN_SCHEMAS = ["information_schema", "pgcf", "ohmyhost"];
42474
+ var DATABASE_WATCH_KEYS = [
42475
+ "id",
42476
+ "environment",
42477
+ "schema",
42478
+ "table",
42479
+ "columns",
42480
+ "operations",
42481
+ "topic",
42482
+ "event_type"
42483
+ ];
42436
42484
  var SCHEDULE_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,62}$/u;
42437
42485
  var CURSOR_PATTERN = /^[A-Za-z0-9._|:=+/-]{1,200}$/u;
42438
42486
  var HEADER_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,63}$/u;
@@ -42454,11 +42502,53 @@ function parseDatabaseWebhookId(value) {
42454
42502
  throw new TypeError("Invalid database webhook id");
42455
42503
  return value;
42456
42504
  }
42505
+ function parseDatabaseWatchId(value) {
42506
+ if (!isDatabaseWatchTriggerName(value))
42507
+ throw new TypeError("Invalid database watch id");
42508
+ return value;
42509
+ }
42457
42510
  function parseDatabaseDeliveryId(value) {
42458
42511
  if (!matches(value, DELIVERY_ID_PATTERN))
42459
42512
  throw new TypeError("Invalid database delivery id");
42460
42513
  return value;
42461
42514
  }
42515
+ function isDatabaseWatchTriggerName(value) {
42516
+ return matches(value, DATABASE_WATCH_ID_PATTERN);
42517
+ }
42518
+ function isDatabaseWatchIdentifier(value) {
42519
+ return matches(value, DATABASE_WATCH_IDENTIFIER_PATTERN);
42520
+ }
42521
+ function isDatabaseWatchSchema(value) {
42522
+ if (!isDatabaseWatchIdentifier(value))
42523
+ return false;
42524
+ const lower = value.toLowerCase();
42525
+ return !lower.startsWith("pg_") && !DATABASE_WATCH_FORBIDDEN_SCHEMAS.includes(lower);
42526
+ }
42527
+ function dataEventTopic(tableName) {
42528
+ const mapped = Array.from(tableName, (character) => {
42529
+ const lower = Array.from(character.toLowerCase())[0] ?? "_";
42530
+ return /^[a-z0-9_]$/u.test(lower) ? lower : "_";
42531
+ }).join("");
42532
+ const topic = /^[a-z]/u.test(mapped) ? mapped : `t_${mapped}`;
42533
+ return topic.slice(0, 48);
42534
+ }
42535
+ function parseDatabaseWatchCreateRequest(value) {
42536
+ if (!record2(value) || !onlyKeys(value, ["schema", "table", "columns", "operations"]) || !isDatabaseWatchIdentifier(value["table"]) || !Array.isArray(value["columns"]) || value["columns"].length < 1 || value["columns"].length > DATABASE_WATCH_MAX_COLUMNS || !value["columns"].every(isDatabaseWatchIdentifier) || new Set(value["columns"]).size !== value["columns"].length)
42537
+ throw new TypeError("Invalid database watch request");
42538
+ return {
42539
+ ...parseDatabaseWatchTarget({ schema: value["schema"], table: value["table"] }),
42540
+ columns: value["columns"],
42541
+ operations: parseWatchOperations(value["operations"])
42542
+ };
42543
+ }
42544
+ function parseDatabaseWatchTarget(value) {
42545
+ if (!record2(value) || !onlyKeys(value, ["schema", "table"]) || !isDatabaseWatchIdentifier(value["table"]))
42546
+ throw new TypeError("Invalid database watch target");
42547
+ const schema = value["schema"] === void 0 ? DATABASE_WATCH_DEFAULT_SCHEMA : value["schema"];
42548
+ if (!isDatabaseWatchSchema(schema))
42549
+ throw new TypeError("Invalid database watch target");
42550
+ return { schema, table: value["table"] };
42551
+ }
42462
42552
  function parseDatabaseScheduleName(value) {
42463
42553
  if (!matches(value, SCHEDULE_NAME_PATTERN))
42464
42554
  throw new TypeError("Invalid database schedule name");
@@ -42515,6 +42605,20 @@ function assertDatabaseWebhook(value) {
42515
42605
  throw new TypeError("Invalid database webhook");
42516
42606
  assertWebhookFields(value);
42517
42607
  }
42608
+ function assertDatabaseWatch(value) {
42609
+ if (!record2(value) || !exact2(value, DATABASE_WATCH_KEYS) || !matches(value["id"], DATABASE_WATCH_ID_PATTERN) || !oneOf(["dev", "prod"], value["environment"]) || !isDatabaseWatchSchema(value["schema"]) || !isDatabaseWatchIdentifier(value["table"]) || !Array.isArray(value["columns"]) || value["columns"].length < 1 || value["columns"].length > DATABASE_WATCH_MAX_COLUMNS || !value["columns"].every(isDatabaseWatchIdentifier) || !isCanonicalOperations(value["operations"]) || value["topic"] !== dataEventTopic(value["table"]) || value["event_type"] !== `data.${value["topic"]}`)
42610
+ throw new TypeError("Invalid database watch");
42611
+ }
42612
+ function assertDatabaseWatchPage(value) {
42613
+ if (!record2(value) || !exact2(value, ["items"]) || !pageItems(value["items"]))
42614
+ throw new TypeError("Invalid database watch page");
42615
+ for (const item of value["items"])
42616
+ assertDatabaseWatch(item);
42617
+ }
42618
+ function assertDatabaseWatchDeleted(value) {
42619
+ if (!record2(value) || !exact2(value, ["id", "deleted"]) || !matches(value["id"], DATABASE_WATCH_ID_PATTERN) || value["deleted"] !== true)
42620
+ throw new TypeError("Invalid database watch deletion");
42621
+ }
42518
42622
  function assertDatabaseWebhookCreated(value) {
42519
42623
  if (!record2(value) || !exact2(value, [...WEBHOOK_KEYS, "secret"]) || typeof value["secret"] !== "string" || value["secret"].length === 0)
42520
42624
  throw new TypeError("Invalid database webhook");
@@ -42543,7 +42647,7 @@ function assertDatabaseWebhookDelivery(value) {
42543
42647
  "delivered_at",
42544
42648
  "last_status",
42545
42649
  "last_error"
42546
- ]) || !matches(value["id"], DELIVERY_ID_PATTERN) || !matches(value["event_id"], EVENT_ID_PATTERN) || !oneOf(DATABASE_WEBHOOK_EVENT_TYPES, value["event_type"]) || !matches(value["webhook_id"], WEBHOOK_ID_PATTERN) || !oneOf(["pending", "delivered", "dead_lettered"], value["state"]) || !nonNegativeInteger(value["attempts"]) || !timestamp2(value["created_at"]) || !nullable(value["next_attempt_at"], timestamp2) || !nullable(value["delivered_at"], timestamp2) || !nullable(value["last_status"], Number.isSafeInteger) || !nullable(value["last_error"], (error) => oneOf(["timeout", "network", "http", "expired", "permanent"], error)))
42650
+ ]) || !matches(value["id"], DELIVERY_ID_PATTERN) || !matches(value["event_id"], EVENT_ID_PATTERN) || !isDeliveredEventType(value["event_type"]) || !matches(value["webhook_id"], WEBHOOK_ID_PATTERN) || !oneOf(["pending", "delivered", "dead_lettered"], value["state"]) || !nonNegativeInteger(value["attempts"]) || !timestamp2(value["created_at"]) || !nullable(value["next_attempt_at"], timestamp2) || !nullable(value["delivered_at"], timestamp2) || !nullable(value["last_status"], Number.isSafeInteger) || !nullable(value["last_error"], (error) => oneOf(["timeout", "network", "http", "expired", "permanent"], error)))
42547
42651
  throw new TypeError("Invalid database webhook delivery");
42548
42652
  }
42549
42653
  function assertDatabaseWebhookDeliveryPage(value) {
@@ -42626,7 +42730,7 @@ function assertUsageRow(value) {
42626
42730
  throw new TypeError("Invalid database execution usage row");
42627
42731
  }
42628
42732
  function assertWebhookFields(value) {
42629
- if (!matches(value["id"], WEBHOOK_ID_PATTERN) || !oneOf(["dev", "prod"], value["environment"]) || !isHttpsUrl2(value["url"]) || !Array.isArray(value["event_types"]) || value["event_types"].length < 1 || value["event_types"].length > DATABASE_WEBHOOK_MAX_EVENT_TYPES || !value["event_types"].every((type) => oneOf(DATABASE_WEBHOOK_EVENT_TYPES, type)) || !Array.isArray(value["header_names"]) || value["header_names"].length > DATABASE_WEBHOOK_MAX_HEADERS || !value["header_names"].every((name) => matches(name, HEADER_NAME_PATTERN)) || !timestamp2(value["created_at"]))
42733
+ if (!matches(value["id"], WEBHOOK_ID_PATTERN) || !oneOf(["dev", "prod"], value["environment"]) || !isHttpsUrl2(value["url"]) || !Array.isArray(value["event_types"]) || value["event_types"].length < 1 || value["event_types"].length > DATABASE_WEBHOOK_MAX_EVENT_TYPES || !value["event_types"].every(isDatabaseWebhookEventPattern) || !Array.isArray(value["header_names"]) || value["header_names"].length > DATABASE_WEBHOOK_MAX_HEADERS || !value["header_names"].every((name) => matches(name, HEADER_NAME_PATTERN)) || !timestamp2(value["created_at"]))
42630
42734
  throw new TypeError("Invalid database webhook");
42631
42735
  }
42632
42736
  function parseHttpsUrl(value) {
@@ -42643,11 +42747,33 @@ function isHttpsUrl2(value) {
42643
42747
  return false;
42644
42748
  }
42645
42749
  }
42750
+ function isDatabaseWebhookEventPattern(value) {
42751
+ return oneOf(DATABASE_WEBHOOK_EVENT_TYPES, value) || matches(value, DATA_SUBSCRIPTION_PATTERN);
42752
+ }
42753
+ function isDeliveredEventType(value) {
42754
+ return oneOf(DATABASE_WEBHOOK_EVENT_TYPES, value) || matches(value, DATA_EVENT_TYPE_PATTERN);
42755
+ }
42756
+ function isDatabaseWebhookEventTypes(value) {
42757
+ return Array.isArray(value) && value.length >= 1 && value.length <= DATABASE_WEBHOOK_MAX_EVENT_TYPES && value.every(isDatabaseWebhookEventPattern) && new Set(value).size === value.length;
42758
+ }
42646
42759
  function parseEventTypes(value) {
42647
- if (!Array.isArray(value) || value.length < 1 || value.length > DATABASE_WEBHOOK_MAX_EVENT_TYPES || !value.every((type) => oneOf(DATABASE_WEBHOOK_EVENT_TYPES, type)) || new Set(value).size !== value.length)
42760
+ if (!isDatabaseWebhookEventTypes(value))
42648
42761
  throw new TypeError("Invalid database webhook event types");
42649
42762
  return value;
42650
42763
  }
42764
+ function parseWatchOperations(value) {
42765
+ if (value === void 0)
42766
+ return DATABASE_WATCH_OPERATIONS;
42767
+ if (!Array.isArray(value) || value.length < 1 || !value.every((operation) => oneOf(DATABASE_WATCH_OPERATIONS, operation)) || new Set(value).size !== value.length)
42768
+ throw new TypeError("Invalid database watch request");
42769
+ return DATABASE_WATCH_OPERATIONS.filter((operation) => value.includes(operation));
42770
+ }
42771
+ function isCanonicalOperations(value) {
42772
+ if (!Array.isArray(value) || value.length < 1)
42773
+ return false;
42774
+ const canonical = DATABASE_WATCH_OPERATIONS.filter((operation) => value.includes(operation));
42775
+ return canonical.length === value.length && canonical.every((op, index) => op === value[index]);
42776
+ }
42651
42777
  function parseHeaders(value) {
42652
42778
  if (!record2(value) || Object.keys(value).length > DATABASE_WEBHOOK_MAX_HEADERS || new Set(Object.keys(value).map((name) => name.toLowerCase())).size !== Object.keys(value).length)
42653
42779
  throw new TypeError("Invalid database webhook headers");
@@ -43083,6 +43209,14 @@ var recognizedCommandLabel = (argv) => {
43083
43209
  if (values[1] === "revoke")
43084
43210
  return "database access revoke";
43085
43211
  }
43212
+ if (value === "database" && values[0] === "watch") {
43213
+ if (values[1] === "create")
43214
+ return "database watch create";
43215
+ if (values[1] === "list")
43216
+ return "database watch list";
43217
+ if (values[1] === "delete")
43218
+ return "database watch delete";
43219
+ }
43086
43220
  if (value === "database" && values[0] === "webhook") {
43087
43221
  if (values[1] === "create")
43088
43222
  return "database webhook create";
@@ -43932,6 +44066,73 @@ var parseProductCliCommand = (argv) => {
43932
44066
  }
43933
44067
  throw new InvalidCommandError();
43934
44068
  }
44069
+ if (first === "database" && values[0] === "watch") {
44070
+ values.shift();
44071
+ const subcommand = values.shift();
44072
+ if (subcommand === "create") {
44073
+ if (!yes || wait || follow || revoke || stdin)
44074
+ throw new InvalidCommandError();
44075
+ const options = parseOptionalOptions(values, [
44076
+ "--project",
44077
+ "--environment",
44078
+ "--table",
44079
+ "--schema",
44080
+ "--columns",
44081
+ "--operations",
44082
+ "--idempotency-key"
44083
+ ]);
44084
+ const projectId = options["--project"], environment = options["--environment"], table = options["--table"], schema = options["--schema"], columns = options["--columns"], operations = options["--operations"], idempotencyKey = options["--idempotency-key"];
44085
+ if (!isValue(projectId) || !isValue(idempotencyKey) || !isValue(table) || !isValue(columns) || environment !== "dev" && environment !== "prod")
44086
+ throw new InvalidCommandError();
44087
+ const request = contractValue(() => parseDatabaseWatchCreateRequest({
44088
+ schema,
44089
+ table,
44090
+ columns: columns.split(","),
44091
+ ...operations === void 0 ? {} : { operations: operations.split(",") }
44092
+ }));
44093
+ return {
44094
+ kind: "database-watch-create",
44095
+ projectId,
44096
+ environment,
44097
+ request,
44098
+ idempotencyKey,
44099
+ yes: true,
44100
+ credentialStore
44101
+ };
44102
+ }
44103
+ if (subcommand === "list") {
44104
+ if (yes || wait || follow || revoke || stdin)
44105
+ throw new InvalidCommandError();
44106
+ const options = parseOptionalOptions(values, ["--project", "--environment"]);
44107
+ const projectId = options["--project"], environment = options["--environment"] ?? "dev";
44108
+ if (!isValue(projectId) || environment !== "dev" && environment !== "prod")
44109
+ throw new InvalidCommandError();
44110
+ return { kind: "database-watch-list", projectId, environment, credentialStore };
44111
+ }
44112
+ if (subcommand === "delete") {
44113
+ if (!yes || wait || follow || revoke || stdin)
44114
+ throw new InvalidCommandError();
44115
+ const options = parseOptionalOptions(values, [
44116
+ "--project",
44117
+ "--environment",
44118
+ "--watch",
44119
+ "--idempotency-key"
44120
+ ]);
44121
+ const projectId = options["--project"], environment = options["--environment"], watchId = options["--watch"], idempotencyKey = options["--idempotency-key"];
44122
+ if (!isValue(projectId) || !isValue(idempotencyKey) || environment !== "dev" && environment !== "prod")
44123
+ throw new InvalidCommandError();
44124
+ return {
44125
+ kind: "database-watch-delete",
44126
+ projectId,
44127
+ environment,
44128
+ watchId: contractValue(() => parseDatabaseWatchId(watchId)),
44129
+ idempotencyKey,
44130
+ yes: true,
44131
+ credentialStore
44132
+ };
44133
+ }
44134
+ throw new InvalidCommandError();
44135
+ }
43935
44136
  if (first === "database" && values[0] === "webhook") {
43936
44137
  values.shift();
43937
44138
  const subcommand = values.shift();
@@ -45317,7 +45518,7 @@ function exact4(v, keys) {
45317
45518
  // apps/product-cli/package.json
45318
45519
  var package_default = {
45319
45520
  name: "@ohmyhost/product-cli",
45320
- version: "0.1.43",
45521
+ version: "0.1.45",
45321
45522
  private: true,
45322
45523
  ohmyhost: {
45323
45524
  deployment: "production",
@@ -45662,20 +45863,23 @@ var HELP_USAGE = Object.freeze({
45662
45863
  "database access create": "ohmyhost database access create --project ULID --environment dev|prod [--mode read|write] [--ttl 5m|1h|24h|SECONDS] [--label TEXT] --yes --json\n The connection URI is shown once. Use it immediately, never store it in files, notes or source, and revoke it when finished.",
45663
45864
  "database access list": "ohmyhost database access list --project ULID [--environment dev|prod] --json",
45664
45865
  "database access revoke": "ohmyhost database access revoke --project ULID --access ULID --yes --json",
45665
- "database webhook create": "ohmyhost database webhook create --project ULID --environment dev|prod --url HTTPS_URL --event-types TYPE[,TYPE] [--headers-file PATH] --idempotency-key KEY --yes --json (the signing secret is shown once in this answer; types: database.created, database.ready, database.woke, database.slept, database.suspended, database.resized, database.deleted; the headers file is a UTF-8 JSON object of at most 8 KiB)",
45866
+ "database watch create": "ohmyhost database watch create --project ULID --environment dev|prod --table NAME [--schema public] --columns COL[,COL] [--operations insert,update,delete] --idempotency-key KEY --yes --json (publishes only the listed columns, at most 32, of each row change as the event type data.<topic> printed in this answer; the table must exist as an ordinary table of the application; a sleeping database wakes; next, subscribe with 'ohmyhost database webhook create ... --event-types data.<topic>')",
45867
+ "database watch list": "ohmyhost database watch list --project ULID [--environment dev|prod] --json (dev when omitted; a sleeping database wakes)",
45868
+ "database watch delete": "ohmyhost database watch delete --project ULID --environment dev|prod --watch ID --idempotency-key KEY --yes --json",
45869
+ "database webhook create": "ohmyhost database webhook create --project ULID --environment dev|prod --url HTTPS_URL --event-types TYPE[,TYPE] [--headers-file PATH] --idempotency-key KEY --yes --json (the signing secret is shown once in this answer; types: database.created, database.ready, database.woke, database.slept, database.suspended, database.resized, database.deleted, data.<topic> for the rows of one watched table printed by database watch create, or data.* for every watched table; a bare * is not accepted and does not include data events; the headers file is a UTF-8 JSON object of at most 8 KiB)",
45666
45870
  "database webhook list": "ohmyhost database webhook list --project ULID [--environment dev|prod] --json (dev when omitted; the secret is never listed)",
45667
45871
  "database webhook get": "ohmyhost database webhook get --project ULID [--environment dev|prod] --webhook ID --json",
45668
45872
  "database webhook delete": "ohmyhost database webhook delete --project ULID --environment dev|prod --webhook ID --yes --json",
45669
45873
  "database webhook test": "ohmyhost database webhook test --project ULID --environment dev|prod --webhook ID --yes --json (sends one signed test request to the receiver; nothing is stored)",
45670
45874
  "database webhook deliveries": "ohmyhost database webhook deliveries --project ULID [--environment dev|prod] [--webhook ID] [--limit 1..100] [--cursor CURSOR] --json",
45671
45875
  "database webhook replay": "ohmyhost database webhook replay --project ULID --environment dev|prod --delivery ID --yes --json",
45672
- "database schedule set": "ohmyhost database schedule set --project ULID --environment dev|prod --name NAME (--cron EXPR | --every-seconds N) --statements-file PATH [--timeout-ms N] [--enabled true|false] --yes --json (runs the statements in the file, a UTF-8 JSON array of 1 to 20 SQL strings, as the runtime role of the application; cron has five fields in UTC; every-seconds is 60 to 2678400; timeout-ms is 1000 to 30000, default 10000)",
45876
+ "database schedule set": "ohmyhost database schedule set --project ULID --environment dev|prod --name NAME (--cron EXPR | --every-seconds N) --statements-file PATH [--timeout-ms N] [--enabled true|false] --yes --json (runs the statements in the file, a UTF-8 JSON array of 1 to 20 SQL strings, as the runtime role of the application; cron has five fields in UTC; every-seconds is 60 to 2678400; timeout-ms is 1000 to 30000, default 10000). A project whose Stop budget is reached has its schedules disabled automatically, within about an hour of the billed usage reaching the budget: the credit service does this after its hourly pgcf pass, and Continue budgets are never touched. While the budget is reached, schedule set with enabled true and schedule run are refused with project_budget_exceeded (HTTP 402). Re-enabling is manual: raise the budget (or wait for the next UTC month) and run database schedule set --enabled true again with the schedule body (read it with schedule get).",
45673
45877
  "database schedule list": "ohmyhost database schedule list --project ULID [--environment dev|prod] --json (dev when omitted)",
45674
45878
  "database schedule get": "ohmyhost database schedule get --project ULID [--environment dev|prod] --name NAME --json",
45675
45879
  "database schedule delete": "ohmyhost database schedule delete --project ULID --environment dev|prod --name NAME --yes --json",
45676
45880
  "database schedule runs": "ohmyhost database schedule runs --project ULID [--environment dev|prod] --name NAME [--limit 1..50] --json",
45677
- "database schedule run": "ohmyhost database schedule run --project ULID --environment dev|prod --name NAME --yes --json (queues one run now, in addition to the timer)",
45678
- "database executions": "ohmyhost database executions --project ULID [--environment dev|prod] --from ISO --to ISO --granularity hour|day [--limit 1..100] [--cursor CURSOR] --json (UTC timestamps like 2026-10-30T00:00:00.000Z aligned to the granularity, at most 31 days; counts schedule runs, webhook attempts and events per hour or day; usage counters only, nothing is charged for them until the operator prices them)",
45881
+ "database schedule run": "ohmyhost database schedule run --project ULID --environment dev|prod --name NAME --yes --json (queues one run now, in addition to the timer; refused with project_budget_exceeded (HTTP 402) while the project's Stop budget is reached)",
45882
+ "database executions": "ohmyhost database executions --project ULID [--environment dev|prod] --from ISO --to ISO --granularity hour|day [--limit 1..100] [--cursor CURSOR] --json (UTC timestamps like 2026-10-30T00:00:00.000Z aligned to the granularity, at most 31 days; counts schedule runs, webhook attempts and events per hour or day; these are counters only, executions have no price of their own and the awake compute time of the database is what is billed)",
45679
45883
  "database psql": "ohmyhost database psql --project ULID --environment dev|prod [--mode read|write] [--ttl 5m|1h|24h|SECONDS] [--json] (starts local psql with a temporary credential and revokes it on exit)",
45680
45884
  link: "ohmyhost link --project ULID --repository-owner OWNER --repository-name REPOSITORY --idempotency-key KEY --json (uses the workspace GitHub connection and waits for the source-link operation)",
45681
45885
  "source auto-deploy set": "ohmyhost source auto-deploy set --project ULID --branch BRANCH --enabled true|false --idempotency-key KEY --json",
@@ -46153,6 +46357,12 @@ var ProductCli = class {
46153
46357
  return await this.listDatabaseAccess(command, signal);
46154
46358
  if (command.kind === "database-access-revoke")
46155
46359
  return await this.revokeDatabaseAccess(command, signal);
46360
+ if (command.kind === "database-watch-create")
46361
+ return await this.createDatabaseWatch(command, signal);
46362
+ if (command.kind === "database-watch-list")
46363
+ return await this.listDatabaseWatches(command, signal);
46364
+ if (command.kind === "database-watch-delete")
46365
+ return await this.deleteDatabaseWatch(command, signal);
46156
46366
  if (command.kind === "database-webhook-create")
46157
46367
  return await this.createDatabaseWebhook(command, signal);
46158
46368
  if (command.kind === "database-webhook-list")
@@ -46684,6 +46894,39 @@ var ProductCli = class {
46684
46894
  assertProjectDatabaseAccess(access);
46685
46895
  return success("database access revoke", { status: "succeeded", access });
46686
46896
  }
46897
+ /** The answer names the event type to subscribe to; the watch already carries the columns and the topic. */
46898
+ async createDatabaseWatch(command, signal) {
46899
+ const api = await this.authenticatedApi(command.credentialStore, signal);
46900
+ const watch = await api.createProjectDatabaseWatch({
46901
+ projectId: command.projectId,
46902
+ environment: command.environment,
46903
+ idempotencyKey: command.idempotencyKey,
46904
+ request: command.request
46905
+ });
46906
+ return success("database watch create", {
46907
+ status: "succeeded",
46908
+ watch,
46909
+ next_action: `Run '${this.dependencies.commandPrefix} database webhook create --project ${command.projectId} --environment ${command.environment} --url HTTPS_URL --event-types ${watch.event_type} --idempotency-key KEY --yes --json' to receive ${watch.event_type} events at an HTTPS receiver.`
46910
+ });
46911
+ }
46912
+ async listDatabaseWatches(command, signal) {
46913
+ const api = await this.authenticatedApi(command.credentialStore, signal);
46914
+ const page = await api.listProjectDatabaseWatches({
46915
+ projectId: command.projectId,
46916
+ environment: command.environment
46917
+ });
46918
+ return success("database watch list", { status: "succeeded", watch: page.items });
46919
+ }
46920
+ async deleteDatabaseWatch(command, signal) {
46921
+ const api = await this.authenticatedApi(command.credentialStore, signal);
46922
+ const deleted = await api.deleteProjectDatabaseWatch({
46923
+ projectId: command.projectId,
46924
+ environment: command.environment,
46925
+ idempotencyKey: command.idempotencyKey,
46926
+ watchId: command.watchId
46927
+ });
46928
+ return success("database watch delete", { status: "succeeded", deleted });
46929
+ }
46687
46930
  /** The signing secret is returned by create only; every other answer carries no secret. */
46688
46931
  async createDatabaseWebhook(command, signal) {
46689
46932
  const request = await webhookCreateRequest(command);
@@ -47714,6 +47957,12 @@ var parsedCommandName = (command) => {
47714
47957
  return "database access revoke";
47715
47958
  if (command.kind === "database-psql")
47716
47959
  return "database psql";
47960
+ if (command.kind === "database-watch-create")
47961
+ return "database watch create";
47962
+ if (command.kind === "database-watch-list")
47963
+ return "database watch list";
47964
+ if (command.kind === "database-watch-delete")
47965
+ return "database watch delete";
47717
47966
  if (command.kind === "database-webhook-create")
47718
47967
  return "database webhook create";
47719
47968
  if (command.kind === "database-webhook-list")
@@ -50586,6 +50835,68 @@ var revokeProjectDatabaseAccess = (parameters, options) => {
50586
50835
  ...params
50587
50836
  });
50588
50837
  };
50838
+ var listProjectDatabaseWatches = (parameters, options) => {
50839
+ const params = buildClientParams([parameters], [
50840
+ {
50841
+ args: [
50842
+ { in: "headers", key: "X-Request-Id" },
50843
+ { in: "path", key: "project_id" },
50844
+ { in: "query", key: "environment" }
50845
+ ]
50846
+ }
50847
+ ]);
50848
+ return (options?.client ?? client).get({
50849
+ responseStyle: "data",
50850
+ security: [{ scheme: "bearer", type: "http" }],
50851
+ url: "/v1/projects/{project_id}/database/watches",
50852
+ ...options,
50853
+ ...params
50854
+ });
50855
+ };
50856
+ var createProjectDatabaseWatch = (parameters, options) => {
50857
+ const params = buildClientParams([parameters], [
50858
+ {
50859
+ args: [
50860
+ { in: "headers", key: "X-Request-Id" },
50861
+ { in: "headers", key: "Idempotency-Key" },
50862
+ { in: "path", key: "project_id" },
50863
+ { key: "databaseWatchCreateRequest", map: "body" }
50864
+ ]
50865
+ }
50866
+ ]);
50867
+ return (options?.client ?? client).post({
50868
+ responseStyle: "data",
50869
+ security: [{ scheme: "bearer", type: "http" }],
50870
+ url: "/v1/projects/{project_id}/database/watches",
50871
+ ...options,
50872
+ ...params,
50873
+ headers: {
50874
+ "Content-Type": "application/json",
50875
+ ...options?.headers,
50876
+ ...params.headers
50877
+ }
50878
+ });
50879
+ };
50880
+ var deleteProjectDatabaseWatch = (parameters, options) => {
50881
+ const params = buildClientParams([parameters], [
50882
+ {
50883
+ args: [
50884
+ { in: "headers", key: "X-Request-Id" },
50885
+ { in: "headers", key: "Idempotency-Key" },
50886
+ { in: "path", key: "project_id" },
50887
+ { in: "path", key: "watch_id" },
50888
+ { in: "query", key: "environment" }
50889
+ ]
50890
+ }
50891
+ ]);
50892
+ return (options?.client ?? client).delete({
50893
+ responseStyle: "data",
50894
+ security: [{ scheme: "bearer", type: "http" }],
50895
+ url: "/v1/projects/{project_id}/database/watches/{watch_id}",
50896
+ ...options,
50897
+ ...params
50898
+ });
50899
+ };
50589
50900
  var listProjectDatabaseWebhooks = (parameters, options) => {
50590
50901
  const params = buildClientParams([parameters], [
50591
50902
  {
@@ -51959,6 +52270,7 @@ function checkedResponse(kind, value) {
51959
52270
 
51960
52271
  // apps/product-cli/dist/product-api.js
51961
52272
  var WEBHOOK_SCOPE_MISMATCH = "Webhook response scope mismatch";
52273
+ var WATCH_SCOPE_MISMATCH = "Watch response scope mismatch";
51962
52274
  var SCHEDULE_SCOPE_MISMATCH = "Schedule response scope mismatch";
51963
52275
  var GeneratedSdkProductApi = class {
51964
52276
  #client;
@@ -52156,6 +52468,42 @@ var GeneratedSdkProductApi = class {
52156
52468
  assertProjectDatabaseAccess(value);
52157
52469
  return value;
52158
52470
  }
52471
+ async createProjectDatabaseWatch(input) {
52472
+ const value = await createProjectDatabaseWatch({
52473
+ project_id: input.projectId,
52474
+ "Idempotency-Key": input.idempotencyKey,
52475
+ databaseWatchCreateRequest: {
52476
+ environment: input.environment,
52477
+ schema: input.request.schema,
52478
+ table: input.request.table,
52479
+ columns: [...input.request.columns],
52480
+ operations: [...input.request.operations]
52481
+ }
52482
+ }, { client: this.#client });
52483
+ assertDatabaseWatch(value);
52484
+ if (value.environment !== input.environment || value.schema !== input.request.schema || value.table !== input.request.table)
52485
+ throw new TypeError(WATCH_SCOPE_MISMATCH);
52486
+ return value;
52487
+ }
52488
+ async listProjectDatabaseWatches(input) {
52489
+ const value = await listProjectDatabaseWatches({ project_id: input.projectId, environment: input.environment }, { client: this.#client });
52490
+ assertDatabaseWatchPage(value);
52491
+ if (value.items.some((item) => item.environment !== input.environment))
52492
+ throw new TypeError(WATCH_SCOPE_MISMATCH);
52493
+ return value;
52494
+ }
52495
+ async deleteProjectDatabaseWatch(input) {
52496
+ const value = await deleteProjectDatabaseWatch({
52497
+ project_id: input.projectId,
52498
+ environment: input.environment,
52499
+ "Idempotency-Key": input.idempotencyKey,
52500
+ watch_id: input.watchId
52501
+ }, { client: this.#client });
52502
+ assertDatabaseWatchDeleted(value);
52503
+ if (value.id !== input.watchId)
52504
+ throw new TypeError(WATCH_SCOPE_MISMATCH);
52505
+ return value;
52506
+ }
52159
52507
  /** The signing secret exists only in this answer; the CLI prints it once and stores nothing. */
52160
52508
  async createProjectDatabaseWebhook(input) {
52161
52509
  const value = await createProjectDatabaseWebhook({
@@ -54123,6 +54471,8 @@ function rejectMigrationSqlReferences(tokens, allowIdentityHelpers = false) {
54123
54471
  if (token?.kind !== "word" && token?.kind !== "identifier")
54124
54472
  continue;
54125
54473
  const value = token.value.toLowerCase();
54474
+ if (value === "pgcf")
54475
+ throw new DatabaseMigrationAdmissionError("unsupported");
54126
54476
  if (value === "ohmyhost") {
54127
54477
  if (allowIdentityHelpers && token.value === "ohmyhost" && tokens[index + 1]?.kind === "symbol" && tokens[index + 1]?.value === "." && ["user_id", "claims"].includes(tokens[index + 2]?.value ?? "") && ["word", "identifier"].includes(tokens[index + 2]?.kind ?? "") && tokens[index + 3]?.kind === "symbol" && tokens[index + 3]?.value === "(" && tokens[index + 4]?.kind === "symbol" && tokens[index + 4]?.value === ")")
54128
54478
  continue;
@@ -54134,6 +54484,139 @@ function rejectMigrationSqlReferences(tokens, allowIdentityHelpers = false) {
54134
54484
  }
54135
54485
  }
54136
54486
 
54487
+ // packages/contracts/dist/database-migration-capture.js
54488
+ var MAX_NAME_BYTES = 63;
54489
+ function readMigrationCaptureTrigger(tokens) {
54490
+ if (!isSqlKeyword(tokens[0], "create") || !isSqlKeyword(tokens[1], "trigger"))
54491
+ return void 0;
54492
+ if (!tokens.some((token) => isNamedPgcf(token, true)))
54493
+ return void 0;
54494
+ return new CaptureReader(tokens).read();
54495
+ }
54496
+ var CaptureReader = class {
54497
+ tokens;
54498
+ position = 2;
54499
+ constructor(tokens) {
54500
+ this.tokens = tokens;
54501
+ }
54502
+ read() {
54503
+ const trigger = this.triggerName();
54504
+ this.expectKeyword("after");
54505
+ const operations = this.operations();
54506
+ this.expectKeyword("on");
54507
+ const schema = this.name();
54508
+ this.expectSymbol(".");
54509
+ const table = this.name();
54510
+ if (!isDatabaseWatchSchema(schema))
54511
+ throw unsupported();
54512
+ for (const keyword of ["for", "each", "row", "execute", "function"]) {
54513
+ this.expectKeyword(keyword);
54514
+ }
54515
+ const header = this.tokens.slice(0, this.position);
54516
+ if (!isNamedPgcf(this.token(), false))
54517
+ throw unsupported();
54518
+ this.position += 1;
54519
+ this.expectSymbol(".");
54520
+ this.expectCapture();
54521
+ this.expectSymbol("(");
54522
+ const columns = this.columns();
54523
+ this.expectSymbol(")");
54524
+ if (this.position !== this.tokens.length)
54525
+ throw unsupported();
54526
+ return Object.freeze({
54527
+ target: Object.freeze({ trigger, schema, table, operations, columns }),
54528
+ header
54529
+ });
54530
+ }
54531
+ triggerName() {
54532
+ const token = this.token();
54533
+ if (token.kind !== "word" && token.kind !== "identifier" || !isDatabaseWatchIdentifier(token.value)) {
54534
+ throw unsupported();
54535
+ }
54536
+ this.position += 1;
54537
+ return token.value;
54538
+ }
54539
+ /** Table and schema names may be quoted identifiers; the executor checks the catalog for the exact name. */
54540
+ name() {
54541
+ const token = this.token();
54542
+ const valid = token.kind === "word" ? isDatabaseWatchIdentifier(token.value) : token.kind === "identifier" && isQuotedName(token.value);
54543
+ if (!valid)
54544
+ throw unsupported();
54545
+ this.position += 1;
54546
+ return token.value;
54547
+ }
54548
+ operations() {
54549
+ const events = [];
54550
+ do {
54551
+ const token = this.token();
54552
+ const operation = DATABASE_WATCH_OPERATIONS.find((name) => isSqlKeyword(token, name));
54553
+ if (operation === void 0 || events.includes(operation))
54554
+ throw unsupported();
54555
+ this.position += 1;
54556
+ events.push(operation);
54557
+ } while (this.takeKeyword("or"));
54558
+ return DATABASE_WATCH_OPERATIONS.filter((operation) => events.includes(operation));
54559
+ }
54560
+ expectCapture() {
54561
+ const token = this.token();
54562
+ if (token.kind !== "word" && token.kind !== "identifier" || token.value !== "capture") {
54563
+ throw unsupported();
54564
+ }
54565
+ this.position += 1;
54566
+ }
54567
+ columns() {
54568
+ const columns = [];
54569
+ do {
54570
+ const token = this.token();
54571
+ if (columns.length >= DATABASE_WATCH_MAX_COLUMNS || token.kind !== "string" || token.text !== `'${token.value}'` || !isDatabaseWatchIdentifier(token.value) || columns.includes(token.value)) {
54572
+ throw unsupported();
54573
+ }
54574
+ this.position += 1;
54575
+ columns.push(token.value);
54576
+ } while (this.takeSymbol(","));
54577
+ return columns;
54578
+ }
54579
+ token() {
54580
+ const token = this.tokens[this.position];
54581
+ if (token === void 0)
54582
+ throw unsupported();
54583
+ return token;
54584
+ }
54585
+ takeKeyword(keyword) {
54586
+ if (!isSqlKeyword(this.tokens[this.position], keyword))
54587
+ return false;
54588
+ this.position += 1;
54589
+ return true;
54590
+ }
54591
+ expectKeyword(keyword) {
54592
+ if (!this.takeKeyword(keyword))
54593
+ throw unsupported();
54594
+ }
54595
+ takeSymbol(symbol) {
54596
+ const token = this.tokens[this.position];
54597
+ if (token?.kind !== "symbol" || token.value !== symbol)
54598
+ return false;
54599
+ this.position += 1;
54600
+ return true;
54601
+ }
54602
+ expectSymbol(symbol) {
54603
+ if (!this.takeSymbol(symbol))
54604
+ throw unsupported();
54605
+ }
54606
+ };
54607
+ function isNamedPgcf(token, ignoreCase) {
54608
+ if (token.kind !== "word" && token.kind !== "identifier")
54609
+ return false;
54610
+ return ignoreCase ? token.value.toLowerCase() === "pgcf" : token.value === "pgcf";
54611
+ }
54612
+ function isQuotedName(value) {
54613
+ const bytes = new TextEncoder().encode(value).byteLength;
54614
+ return bytes >= 1 && bytes <= MAX_NAME_BYTES;
54615
+ }
54616
+ function unsupported() {
54617
+ return new DatabaseMigrationAdmissionError("unsupported");
54618
+ }
54619
+
54137
54620
  // packages/contracts/dist/database-migration-policies.js
54138
54621
  var PolicyReader = class {
54139
54622
  tokens;
@@ -54287,6 +54770,9 @@ function admitExpandOnlyMigrations(migrations) {
54287
54770
  throw new DatabaseMigrationAdmissionError("invalid");
54288
54771
  }
54289
54772
  const statements = parseDatabaseMigrationStatements(sql);
54773
+ if (statements.some((statement) => statement.captureTrigger !== void 0 && isDatabaseWatchTriggerName(statement.captureTrigger.trigger))) {
54774
+ throw new DatabaseMigrationAdmissionError("unsupported");
54775
+ }
54290
54776
  statementCount += statements.length;
54291
54777
  return Object.freeze({ path: migration.path, statementCount: statements.length });
54292
54778
  });
@@ -54300,6 +54786,11 @@ function parseDatabaseMigrationStatements(sql) {
54300
54786
  if (statements.length === 0)
54301
54787
  throw new DatabaseMigrationAdmissionError("invalid");
54302
54788
  return Object.freeze(statements.map((statement) => {
54789
+ const capture = readMigrationCaptureTrigger(statement.tokens);
54790
+ if (capture !== void 0) {
54791
+ rejectMigrationSqlReferences(capture.header);
54792
+ return Object.freeze({ sql: statement.sql, captureTrigger: capture.target });
54793
+ }
54303
54794
  rejectMigrationSqlReferences(statement.tokens, true);
54304
54795
  const rlsTarget = readMigrationRlsTarget(statement.tokens);
54305
54796
  if (rlsTarget !== void 0)