@amerged/ohmyhost-cli 0.1.17 → 0.1.19

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
  # @amerged/ohmyhost-cli
2
2
 
3
- ohmyho.st cli client, version 0.1.17.
3
+ ohmyho.st cli client, version 0.1.19.
4
4
 
5
5
  ```sh
6
6
  npm install --global @amerged/ohmyhost-cli
package/dist/bin.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire as __ohmyhostCreateRequire } from "node:module"; const require = __ohmyhostCreateRequire(import.meta.url);
3
3
  import {
4
4
  runProductionCli
5
- } from "./chunk-IZZDZV6T.js";
5
+ } from "./chunk-57UUB7RK.js";
6
6
  import "./chunk-4HFQ54FP.js";
7
7
 
8
8
  // apps/product-cli/dist/bin.js
@@ -14984,6 +14984,8 @@ var recognizedCommandLabel = (argv) => {
14984
14984
  return `export ${values[0]}`;
14985
14985
  if (value === "feedback" && values[0] === "submit")
14986
14986
  return "feedback submit";
14987
+ if (value === "feedback" && values[0] === "status")
14988
+ return "feedback status";
14987
14989
  if (value === "project" && values[0] === "create")
14988
14990
  return "project create";
14989
14991
  if (value === "organization" && values[0] === "create")
@@ -15209,6 +15211,19 @@ var parseProductCliCommand = (argv) => {
15209
15211
  return { kind: "token-revoke", organizationId, keyId: options["--key"], credentialStore };
15210
15212
  throw new InvalidCommandError();
15211
15213
  }
15214
+ if (first === "feedback" && values[0] === "status") {
15215
+ values.shift();
15216
+ const feedbackId = values.shift();
15217
+ if (yes || wait || follow || stdin || !isValue(feedbackId))
15218
+ throw new InvalidCommandError();
15219
+ const cursor = parseOptionalOptions(values, ["--cursor"])["--cursor"];
15220
+ return {
15221
+ kind: "feedback-status",
15222
+ feedbackId,
15223
+ ...cursor === void 0 ? {} : { cursor },
15224
+ credentialStore
15225
+ };
15226
+ }
15212
15227
  if (first === "feedback" && values.shift() === "submit") {
15213
15228
  if (yes || wait || follow || stdin)
15214
15229
  throw new InvalidCommandError();
@@ -16382,6 +16397,78 @@ var publicWorkspace = (organization) => ({
16382
16397
  name: organization.name
16383
16398
  });
16384
16399
 
16400
+ // packages/contracts/dist/feedback.js
16401
+ var FEEDBACK_HISTORY_PAGE_SIZE = 25;
16402
+ var FEEDBACK_REPLY_MAX_LENGTH = 2e3;
16403
+ var FEEDBACK_UPDATE_STATES = [
16404
+ "in_review",
16405
+ "planned",
16406
+ "in_progress",
16407
+ "resolved",
16408
+ "closed"
16409
+ ];
16410
+ var releasePattern = /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,63}$/u;
16411
+ var identifier = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u;
16412
+ var detailPrefix = "Feedback was not accepted:";
16413
+ var detailSuffix = "Nothing was stored; correct these fields and submit again.";
16414
+ var detailSentence = new RegExp("^" + detailPrefix + "((?: (?:" + [
16415
+ "Only organization_id, project_id, environment_id, operation_id, kind, title, description, error_code and client_version are accepted; remove other fields such as attachments or logs\\.",
16416
+ "organization_id must be the 26-character workspace ULID\\.",
16417
+ "(?:project|environment|operation)_id must be a 26-character ULID\\.",
16418
+ "environment_id and operation_id require project_id\\.",
16419
+ "kind must be bug, issue or feature_request\\.",
16420
+ "(?:title|description) must be a string\\.",
16421
+ "(?:title|description) must not be empty\\.",
16422
+ "(?:title|description) must not start or end with whitespace; trim it\\.",
16423
+ "(?:title|description) has [1-9][0-9]{0,8} characters, over the (?:160|8000)-character limit; shorten it, it is never truncated\\.",
16424
+ "title must not contain line breaks, tabs or other control characters\\.",
16425
+ "description must not contain control characters other than tabs and line breaks, such as terminal color codes\\.",
16426
+ "(?:error_code|client_version) must be 1\u2013128 letters, digits and the characters \\._:/\\+- starting with a letter or digit\\."
16427
+ ].join("|") + "))+) " + detailSuffix.replace(".", "\\.") + "$", "u");
16428
+ function parseFeedbackSubmissionDetail(value) {
16429
+ return typeof value === "string" && value.length <= 2e3 && detailSentence.test(value) ? value : null;
16430
+ }
16431
+ function parseFeedbackReceipt(value) {
16432
+ if (typeof value !== "object" || value === null || Array.isArray(value))
16433
+ throw new TypeError("Feedback was not acknowledged");
16434
+ const v = value;
16435
+ if (Object.keys(v).sort().join(",") !== "id,organization_id,submitted_at" || typeof v["id"] !== "string" || !identifier.test(v["id"]) || typeof v["organization_id"] !== "string" || !identifier.test(v["organization_id"]) || typeof v["submitted_at"] !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(v["submitted_at"]) || !Number.isFinite(Date.parse(v["submitted_at"])) || new Date(v["submitted_at"]).toISOString() !== v["submitted_at"])
16436
+ throw new TypeError("Feedback was not acknowledged");
16437
+ return v;
16438
+ }
16439
+ function parseFeedbackStatus(value) {
16440
+ if (typeof value !== "object" || value === null || Array.isArray(value))
16441
+ throw new TypeError("Feedback status is invalid");
16442
+ const v = value;
16443
+ const history = v["history"];
16444
+ if (Object.keys(v).sort().join(",") !== "history,id,next_cursor,organization_id,status,submitted_at,updated_at" || v["next_cursor"] !== null && !validIdentifier(v["next_cursor"]) || !validIdentifier(v["id"]) || !validIdentifier(v["organization_id"]) || !validTimestamp(v["submitted_at"]) || !validTimestamp(v["updated_at"]) || typeof v["status"] !== "string" || !(v["status"] === "received" || FEEDBACK_UPDATE_STATES.includes(v["status"])) || !Array.isArray(history) || history.length > FEEDBACK_HISTORY_PAGE_SIZE || v["next_cursor"] !== null && history.length !== FEEDBACK_HISTORY_PAGE_SIZE || !history.every(validUpdate))
16445
+ throw new TypeError("Feedback status is invalid");
16446
+ return v;
16447
+ }
16448
+ function validUpdate(value) {
16449
+ if (typeof value !== "object" || value === null || Array.isArray(value))
16450
+ return false;
16451
+ const v = value;
16452
+ const keys = Object.keys(v);
16453
+ const status = v["status"];
16454
+ return keys.every((key) => ["id", "created_at", "status", "release", "reply"].includes(key)) && validIdentifier(v["id"]) && validTimestamp(v["created_at"]) && (status === void 0 || typeof status === "string" && FEEDBACK_UPDATE_STATES.includes(status)) && (status !== void 0 || "reply" in v) && ("release" in v ? status === "resolved" && typeof v["release"] === "string" && releasePattern.test(v["release"]) : status !== "resolved") && (!("reply" in v) || validText(v["reply"], FEEDBACK_REPLY_MAX_LENGTH, true)) && (status !== "resolved" && status !== "closed" || "reply" in v);
16455
+ }
16456
+ function validIdentifier(value) {
16457
+ return typeof value === "string" && identifier.test(value);
16458
+ }
16459
+ function validTimestamp(value) {
16460
+ return typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
16461
+ }
16462
+ function validText(value, max, multiline) {
16463
+ return typeof value === "string" && value === value.trim() && Array.from(value).length >= 1 && Array.from(value).length <= max && !hasControlCharacter(value, multiline);
16464
+ }
16465
+ function hasControlCharacter(value, multiline) {
16466
+ return Array.from(value).some((character) => {
16467
+ const code = character.charCodeAt(0);
16468
+ return code < 32 && !(multiline && (code === 9 || code === 10 || code === 13)) || code >= 127 && code <= 159;
16469
+ });
16470
+ }
16471
+
16385
16472
  // packages/contracts/dist/framework-admission.js
16386
16473
  var FrameworkAdmissionError = class extends Error {
16387
16474
  reason;
@@ -17731,7 +17818,7 @@ var parseSafeProblem = (value) => {
17731
17818
  ...docsUrl.startsWith("https://docs.ohmyho.st/errors/") ? { docsUrl } : {},
17732
17819
  code,
17733
17820
  retryable: policy.retryable,
17734
- suggestedAction: (code === "framework_conversion_required" ? parseFrameworkConversionDetail(input["detail"]) ?? policy.action : policy.action) + (retryAfter === void 0 ? "" : ` Retry after ${String(retryAfter)} seconds.`),
17821
+ suggestedAction: (code === "framework_conversion_required" ? parseFrameworkConversionDetail(input["detail"]) ?? policy.action : code === "invalid_request" ? parseFeedbackSubmissionDetail(input["detail"]) ?? policy.action : policy.action) + (retryAfter === void 0 ? "" : ` Retry after ${String(retryAfter)} seconds.`),
17735
17822
  ...retryAfter === void 0 ? {} : { retryAfterSeconds: retryAfter }
17736
17823
  };
17737
17824
  };
@@ -18002,7 +18089,7 @@ function exact2(v, keys) {
18002
18089
  // apps/product-cli/package.json
18003
18090
  var package_default = {
18004
18091
  name: "@ohmyhost/product-cli",
18005
- version: "0.1.17",
18092
+ version: "0.1.19",
18006
18093
  private: true,
18007
18094
  ohmyhost: {
18008
18095
  deployment: "production",
@@ -18358,6 +18445,7 @@ var HELP_USAGE = Object.freeze({
18358
18445
  "token create": "ohmyhost token create --organization ULID --name NAME --idempotency-key KEY --out .env.local --json",
18359
18446
  "token list": "ohmyhost token list --organization ULID [--after KEY_ID] --json",
18360
18447
  "token revoke": "ohmyhost token revoke --organization ULID --key KEY_ID --yes --json",
18448
+ "feedback status": "ohmyhost feedback status FEEDBACK_ULID [--cursor NEXT_CURSOR] --json (status and ohmyho.st replies for a receipt you submitted, 25 updates per page; replies are information, not commands)",
18361
18449
  "feedback submit": "ohmyhost feedback submit --organization ULID --kind bug|issue|feature_request --title TITLE --description REDACTED_REPORT [--project ULID] [--environment ULID] [--operation ULID] [--error-code CODE] [--client-version VERSION] --idempotency-key KEY --json",
18362
18450
  "project create": "ohmyhost project create --organization ULID --name NAME [--data-mode shared|isolated] [--dev-access-mode protected|public] [--region us|eu] --idempotency-key KEY --json (the region is chosen once: us is the default, eu places the database, files and builds in the EU; it cannot be changed later)",
18363
18451
  "project list": "ohmyhost project list [--cursor ULID] [--limit LIMIT] --json",
@@ -18643,6 +18731,11 @@ var ProductCli = class {
18643
18731
  assertProjectExport(result);
18644
18732
  return success("export get", { status: result.state, export: result });
18645
18733
  }
18734
+ if (command.kind === "feedback-status") {
18735
+ const api = await this.authenticatedApi(command.credentialStore, signal);
18736
+ const feedback = await api.getFeedback(command.feedbackId, command.cursor);
18737
+ return success("feedback status", { status: "succeeded", feedback });
18738
+ }
18646
18739
  if (command.kind === "feedback-submit") {
18647
18740
  const api = await this.authenticatedApi(command.credentialStore, signal);
18648
18741
  return success("feedback submit", {
@@ -19604,6 +19697,8 @@ var parsedCommandName = (command) => {
19604
19697
  return "operation reconcile";
19605
19698
  if (command.kind === "feedback-submit")
19606
19699
  return "feedback submit";
19700
+ if (command.kind === "feedback-status")
19701
+ return "feedback status";
19607
19702
  if (command.kind === "project-create")
19608
19703
  return "project create";
19609
19704
  if (command.kind === "organization-create")
@@ -20771,6 +20866,24 @@ var submitFeedback = (parameters, options) => {
20771
20866
  }
20772
20867
  });
20773
20868
  };
20869
+ var getFeedback = (parameters, options) => {
20870
+ const params = buildClientParams([parameters], [
20871
+ {
20872
+ args: [
20873
+ { in: "headers", key: "X-Request-Id" },
20874
+ { in: "path", key: "feedback_id" },
20875
+ { in: "query", key: "cursor" }
20876
+ ]
20877
+ }
20878
+ ]);
20879
+ return (options?.client ?? client).get({
20880
+ responseStyle: "data",
20881
+ security: [{ scheme: "bearer", type: "http" }],
20882
+ url: "/v1/feedback/{feedback_id}",
20883
+ ...options,
20884
+ ...params
20885
+ });
20886
+ };
20774
20887
  var getCurrentIdentity = (parameters, options) => {
20775
20888
  const params = buildClientParams([parameters], [{ args: [{ in: "headers", key: "X-Request-Id" }] }]);
20776
20889
  return (options?.client ?? client).get({
@@ -22398,17 +22511,6 @@ async function executeManagedMail(client2, input) {
22398
22511
  return result;
22399
22512
  }
22400
22513
 
22401
- // packages/contracts/dist/feedback.js
22402
- var identifier = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u;
22403
- function parseFeedbackReceipt(value) {
22404
- if (typeof value !== "object" || value === null || Array.isArray(value))
22405
- throw new TypeError("Feedback was not acknowledged");
22406
- const v = value;
22407
- if (Object.keys(v).sort().join(",") !== "id,organization_id,submitted_at" || typeof v["id"] !== "string" || !identifier.test(v["id"]) || typeof v["organization_id"] !== "string" || !identifier.test(v["organization_id"]) || typeof v["submitted_at"] !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(v["submitted_at"]) || !Number.isFinite(Date.parse(v["submitted_at"])) || new Date(v["submitted_at"]).toISOString() !== v["submitted_at"])
22408
- throw new TypeError("Feedback was not acknowledged");
22409
- return v;
22410
- }
22411
-
22412
22514
  // apps/product-cli/dist/product-api.js
22413
22515
  var GeneratedSdkProductApi = class {
22414
22516
  #client;
@@ -22645,6 +22747,9 @@ var GeneratedSdkProductApi = class {
22645
22747
  async submitFeedback(input) {
22646
22748
  return parseFeedbackReceipt(await submitFeedback({ "Idempotency-Key": input.idempotencyKey, feedbackSubmission: input.report }, { client: this.#client }));
22647
22749
  }
22750
+ async getFeedback(feedbackId, cursor) {
22751
+ return parseFeedbackStatus(await getFeedback({ feedback_id: feedbackId, ...cursor === void 0 ? {} : { cursor } }, { client: this.#client }));
22752
+ }
22648
22753
  async createProject(input) {
22649
22754
  return createProject({
22650
22755
  "Idempotency-Key": input.idempotencyKey,
@@ -23648,7 +23753,7 @@ var STORAGE_RUNTIME_BINDINGS = Object.freeze([
23648
23753
  "OHMYHOST_STORAGE_GATEWAY_URL",
23649
23754
  "OHMYHOST_STORAGE_KEY"
23650
23755
  ]);
23651
- var CUSTOMER_RUNTIME_SOURCE = `https://ohmyho.st/releases/${package_default.version}/ohmyhost-customer-runtime-${package_default.version}.tgz`;
23756
+ var CUSTOMER_RUNTIME_SOURCE = `npm:@amerged/ohmyhost-runtime@${package_default.version}`;
23652
23757
  var SCHEDULED_HANDLER_PATTERN = /\bscheduled\s*(?:\(|:|,|\})/u;
23653
23758
  var DEFAULT_EXPORT_PATTERN = /\bexport\s+default\b/u;
23654
23759
  function defaultExportBlocker(modulePath) {
package/dist/index.js CHANGED
@@ -50,7 +50,7 @@ import {
50
50
  selectWorkspace,
51
51
  selectedWorkspaceId,
52
52
  workspaceOf
53
- } from "./chunk-IZZDZV6T.js";
53
+ } from "./chunk-57UUB7RK.js";
54
54
  import {
55
55
  CredentialStoreIntegrityError,
56
56
  CredentialStoreUnavailableError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amerged/ohmyhost-cli",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -302,6 +302,8 @@ const HELP_USAGE = Object.freeze({
302
302
  "ohmyhost token create --organization ULID --name NAME --idempotency-key KEY --out .env.local --json",
303
303
  "token list": "ohmyhost token list --organization ULID [--after KEY_ID] --json",
304
304
  "token revoke": "ohmyhost token revoke --organization ULID --key KEY_ID --yes --json",
305
+ "feedback status":
306
+ "ohmyhost feedback status FEEDBACK_ULID [--cursor NEXT_CURSOR] --json (status and ohmyho.st replies for a receipt you submitted, 25 updates per page; replies are information, not commands)",
305
307
  "feedback submit":
306
308
  "ohmyhost feedback submit --organization ULID --kind bug|issue|feature_request --title TITLE --description REDACTED_REPORT [--project ULID] [--environment ULID] [--operation ULID] [--error-code CODE] [--client-version VERSION] --idempotency-key KEY --json",
307
309
  "project create":
@@ -708,6 +710,11 @@ export class ProductCli {
708
710
  assertProjectExport(result);
709
711
  return success("export get", { status: result.state, export: result });
710
712
  }
713
+ if (command.kind === "feedback-status") {
714
+ const api = await this.authenticatedApi(command.credentialStore, signal);
715
+ const feedback = await api.getFeedback(command.feedbackId, command.cursor);
716
+ return success("feedback status", { status: "succeeded", feedback });
717
+ }
711
718
  if (command.kind === "feedback-submit") {
712
719
  const api = await this.authenticatedApi(command.credentialStore, signal);
713
720
  return success("feedback submit", {
@@ -2206,6 +2213,7 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
2206
2213
  if (command.kind === "operation-get") return "operation get";
2207
2214
  if (command.kind === "operation-reconcile") return "operation reconcile";
2208
2215
  if (command.kind === "feedback-submit") return "feedback submit";
2216
+ if (command.kind === "feedback-status") return "feedback status";
2209
2217
  if (command.kind === "project-create") return "project create";
2210
2218
  if (command.kind === "organization-create") return "organization create";
2211
2219
  if (command.kind === "organization-list") return "organization list";
@@ -79,6 +79,12 @@ export type ProductCliCommand =
79
79
  readonly idempotencyKey: string;
80
80
  readonly credentialStore: "native";
81
81
  }
82
+ | {
83
+ readonly kind: "feedback-status";
84
+ readonly feedbackId: string;
85
+ readonly cursor?: string;
86
+ readonly credentialStore: "native";
87
+ }
82
88
  | { kind: "billing-recharge-get"; organizationId: string; credentialStore: "native" }
83
89
  | ({ kind: "billing-recharge-set"; credentialStore: "native" } & {
84
90
  organizationId: string;
@@ -528,6 +534,7 @@ export type RecognizedCommandLabel =
528
534
  | "token list"
529
535
  | "token revoke"
530
536
  | "feedback submit"
537
+ | "feedback status"
531
538
  | "project create"
532
539
  | "organization create"
533
540
  | "organization list"
@@ -632,6 +639,7 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
632
639
  if (value === "export" && (values[0] === "create" || values[0] === "get"))
633
640
  return `export ${values[0]}`;
634
641
  if (value === "feedback" && values[0] === "submit") return "feedback submit";
642
+ if (value === "feedback" && values[0] === "status") return "feedback status";
635
643
  if (value === "project" && values[0] === "create") return "project create";
636
644
  if (value === "organization" && values[0] === "create") return "organization create";
637
645
  if (value === "organization" && values[0] === "list") return "organization list";
@@ -846,6 +854,18 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
846
854
  return { kind: "token-revoke", organizationId, keyId: options["--key"], credentialStore };
847
855
  throw new InvalidCommandError();
848
856
  }
857
+ if (first === "feedback" && values[0] === "status") {
858
+ values.shift();
859
+ const feedbackId = values.shift();
860
+ if (yes || wait || follow || stdin || !isValue(feedbackId)) throw new InvalidCommandError();
861
+ const cursor = parseOptionalOptions(values, ["--cursor"])["--cursor"];
862
+ return {
863
+ kind: "feedback-status",
864
+ feedbackId,
865
+ ...(cursor === undefined ? {} : { cursor }),
866
+ credentialStore,
867
+ };
868
+ }
849
869
  if (first === "feedback" && values.shift() === "submit") {
850
870
  if (yes || wait || follow || stdin) throw new InvalidCommandError();
851
871
  const options = parseOptionalOptions(values, [
@@ -24,7 +24,7 @@ import {
24
24
  type UserApiKeyCreation,
25
25
  type UserApiKeyPage,
26
26
  } from "@ohmyhost/contracts/user-api-keys";
27
- import { parseFeedbackReceipt } from "@ohmyhost/contracts/feedback";
27
+ import { parseFeedbackReceipt, parseFeedbackStatus } from "@ohmyhost/contracts/feedback";
28
28
  import {
29
29
  getProjectContext,
30
30
  setProjectNotes,
@@ -64,6 +64,7 @@ import {
64
64
  createDeployment,
65
65
  createProject,
66
66
  submitFeedback,
67
+ getFeedback,
67
68
  createOrganization,
68
69
  type Organization,
69
70
  type GithubConnectionAuthorization,
@@ -280,6 +281,10 @@ export interface ProductApi {
280
281
  report: import("@ohmyhost/sdk-ts").FeedbackSubmission;
281
282
  idempotencyKey: string;
282
283
  }): Promise<import("@ohmyhost/sdk-ts").FeedbackReceipt>;
284
+ getFeedback(
285
+ feedbackId: string,
286
+ cursor?: string,
287
+ ): Promise<import("@ohmyhost/sdk-ts").FeedbackStatus>;
283
288
  createProject(input: ProjectCreateInput): Promise<Operation>;
284
289
  getOperation(operationId: string): Promise<Operation>;
285
290
  reconcileOperation(input: {
@@ -801,6 +806,18 @@ export class GeneratedSdkProductApi implements ProductApi {
801
806
  );
802
807
  }
803
808
 
809
+ public async getFeedback(
810
+ feedbackId: string,
811
+ cursor?: string,
812
+ ): Promise<import("@ohmyhost/sdk-ts").FeedbackStatus> {
813
+ return parseFeedbackStatus(
814
+ await getFeedback(
815
+ { feedback_id: feedbackId, ...(cursor === undefined ? {} : { cursor }) },
816
+ { client: this.#client },
817
+ ),
818
+ ) as import("@ohmyhost/sdk-ts").FeedbackStatus;
819
+ }
820
+
804
821
  public async createProject(input: ProjectCreateInput): Promise<Operation> {
805
822
  return createProject(
806
823
  {
@@ -73,9 +73,9 @@ const STORAGE_RUNTIME_BINDINGS = Object.freeze([
73
73
  "OHMYHOST_STORAGE_GATEWAY_URL",
74
74
  "OHMYHOST_STORAGE_KEY",
75
75
  ]);
76
- // `@ohmyhost/customer-runtime` is private and resolves from no registry; report the release tarball
77
- // of this client so a bare package name never reaches a customer.
78
- const CUSTOMER_RUNTIME_SOURCE = `https://ohmyho.st/releases/${cliPackage.version}/ohmyhost-customer-runtime-${cliPackage.version}.tgz`;
76
+ // The runtime release is published unchanged as `@amerged/ohmyhost-runtime`; this pinned alias keeps
77
+ // its import name and, unlike the website archive each release removes, stays installable.
78
+ const CUSTOMER_RUNTIME_SOURCE = `npm:@amerged/ohmyhost-runtime@${cliPackage.version}`;
79
79
  const SCHEDULED_HANDLER_PATTERN = /\bscheduled\s*(?:\(|:|,|\})/u;
80
80
  // The platform imports the module's default export; handlers that are only named exports are
81
81
  // never invoked, so init blocks them before a build or a runtime rejection would.
@@ -139,7 +139,7 @@ export interface RepositoryInitInventory {
139
139
 
140
140
  export interface RepositoryInitRuntimePackages {
141
141
  readonly betterAuth?: "1.7.1";
142
- /** Exact install source: the package is private and cannot be resolved from a registry. */
142
+ /** Exact install source: an npm alias pinned to the published runtime of this client version. */
143
143
  readonly customerRuntime?: string;
144
144
  }
145
145
 
@@ -1,3 +1,4 @@
1
+ import { parseFeedbackSubmissionDetail } from "@ohmyhost/contracts/feedback";
1
2
  import { parseFrameworkConversionDetail } from "@ohmyhost/contracts/framework-admission";
2
3
  import {
3
4
  parseOperationDeploymentProgress,
@@ -1557,7 +1558,9 @@ export const parseSafeProblem = (value: unknown): SafeProblem => {
1557
1558
  suggestedAction:
1558
1559
  (code === "framework_conversion_required"
1559
1560
  ? (parseFrameworkConversionDetail(input["detail"]) ?? policy.action)
1560
- : policy.action) +
1561
+ : code === "invalid_request"
1562
+ ? (parseFeedbackSubmissionDetail(input["detail"]) ?? policy.action)
1563
+ : policy.action) +
1561
1564
  (retryAfter === undefined ? "" : ` Retry after ${String(retryAfter)} seconds.`),
1562
1565
  ...(retryAfter === undefined ? {} : { retryAfterSeconds: retryAfter as number }),
1563
1566
  };
@@ -17,6 +17,51 @@ export interface FeedbackReceipt {
17
17
  readonly submitted_at: string;
18
18
  }
19
19
 
20
+ export type FeedbackState =
21
+ | "received"
22
+ | "in_review"
23
+ | "planned"
24
+ | "in_progress"
25
+ | "resolved"
26
+ | "closed";
27
+
28
+ /** One customer-visible operator update; internal notes never appear here. */
29
+ export interface FeedbackUpdate {
30
+ readonly id: string;
31
+ readonly created_at: string;
32
+ readonly status?: Exclude<FeedbackState, "received">;
33
+ /** The shipped release that contains the fix; present exactly on `resolved`. */
34
+ readonly release?: string;
35
+ readonly reply?: string;
36
+ }
37
+
38
+ export interface FeedbackStatus {
39
+ readonly id: string;
40
+ readonly organization_id: string;
41
+ readonly submitted_at: string;
42
+ readonly status: FeedbackState;
43
+ /** Latest customer-visible update across the whole history, or the submission time. */
44
+ readonly updated_at: string;
45
+ /** One page of customer-visible updates, oldest first. */
46
+ readonly history: readonly FeedbackUpdate[];
47
+ /** Pass as `cursor` to read the next page; null on the last page. */
48
+ readonly next_cursor: string | null;
49
+ }
50
+
51
+ /** Customer-visible updates per history page. */
52
+ export const FEEDBACK_HISTORY_PAGE_SIZE = 25;
53
+ export const FEEDBACK_REPLY_MAX_LENGTH = 2000;
54
+ /** A `resolved` or `closed` update must explain itself in at least this many characters. */
55
+ export const FEEDBACK_EXPLANATION_MIN_LENGTH = 20;
56
+ export const FEEDBACK_UPDATE_STATES: readonly Exclude<FeedbackState, "received">[] = [
57
+ "in_review",
58
+ "planned",
59
+ "in_progress",
60
+ "resolved",
61
+ "closed",
62
+ ];
63
+ const releasePattern = /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,63}$/u;
64
+
20
65
  const identifier = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u;
21
66
  const fields = new Set([
22
67
  "organization_id",
@@ -30,36 +75,99 @@ const fields = new Set([
30
75
  "client_version",
31
76
  ]);
32
77
 
33
- export function parseFeedbackSubmission(value: unknown): FeedbackSubmission {
34
- if (typeof value !== "object" || value === null || Array.isArray(value))
35
- throw new TypeError("Invalid feedback");
36
- const v = value as Record<string, unknown>;
37
- if (
38
- Object.keys(v).some((key) => !fields.has(key)) ||
39
- typeof v["organization_id"] !== "string" ||
40
- !identifier.test(v["organization_id"]) ||
41
- typeof v["kind"] !== "string" ||
42
- !["bug", "issue", "feature_request"].includes(v["kind"]) ||
43
- !validText(v["title"], 160, false) ||
44
- !validText(v["description"], 8000, true)
45
- )
46
- throw new TypeError("Invalid feedback");
47
- for (const key of ["project_id", "environment_id", "operation_id"]) {
48
- if (key in v && (typeof v[key] !== "string" || !identifier.test(v[key])))
49
- throw new TypeError("Invalid feedback scope");
78
+ const metadata = /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,127}$/u;
79
+ const detailPrefix = "Feedback was not accepted:";
80
+ const detailSuffix = "Nothing was stored; correct these fields and submit again.";
81
+ // Each refusal sentence names a field and a rule, never the submitted value, so a client can show
82
+ // it verbatim: parseFeedbackSubmissionDetail recognizes exactly these sentences.
83
+ const detailSentence = new RegExp(
84
+ "^" +
85
+ detailPrefix +
86
+ "((?: (?:" +
87
+ [
88
+ "Only organization_id, project_id, environment_id, operation_id, kind, title, description, error_code and client_version are accepted; remove other fields such as attachments or logs\\.",
89
+ "organization_id must be the 26-character workspace ULID\\.",
90
+ "(?:project|environment|operation)_id must be a 26-character ULID\\.",
91
+ "environment_id and operation_id require project_id\\.",
92
+ "kind must be bug, issue or feature_request\\.",
93
+ "(?:title|description) must be a string\\.",
94
+ "(?:title|description) must not be empty\\.",
95
+ "(?:title|description) must not start or end with whitespace; trim it\\.",
96
+ "(?:title|description) has [1-9][0-9]{0,8} characters, over the (?:160|8000)-character limit; shorten it, it is never truncated\\.",
97
+ "title must not contain line breaks, tabs or other control characters\\.",
98
+ "description must not contain control characters other than tabs and line breaks, such as terminal color codes\\.",
99
+ "(?:error_code|client_version) must be 1–128 letters, digits and the characters \\._:/\\+- starting with a letter or digit\\.",
100
+ ].join("|") +
101
+ "))+) " +
102
+ detailSuffix.replace(".", "\\.") +
103
+ "$",
104
+ "u",
105
+ );
106
+
107
+ /** A refused submission whose message names each failing field and rule, never its value. */
108
+ export class FeedbackSubmissionError extends TypeError {
109
+ public constructor(problems: readonly string[]) {
110
+ super(`${detailPrefix} ${problems.join(" ")} ${detailSuffix}`);
111
+ this.name = "FeedbackSubmissionError";
50
112
  }
113
+ }
114
+
115
+ /** The refusal detail when it is exactly a feedback validation message, otherwise null. */
116
+ export function parseFeedbackSubmissionDetail(value: unknown): string | null {
117
+ return typeof value === "string" && value.length <= 2000 && detailSentence.test(value)
118
+ ? value
119
+ : null;
120
+ }
121
+
122
+ export function parseFeedbackSubmission(value: unknown): FeedbackSubmission {
123
+ const v = (
124
+ typeof value === "object" && value !== null && !Array.isArray(value) ? value : {}
125
+ ) as Record<string, unknown>;
126
+ const problems: string[] = [];
127
+ if (Object.keys(v).some((key) => !fields.has(key)))
128
+ problems.push(
129
+ "Only organization_id, project_id, environment_id, operation_id, kind, title, description, error_code and client_version are accepted; remove other fields such as attachments or logs.",
130
+ );
131
+ if (!validIdentifier(v["organization_id"]))
132
+ problems.push("organization_id must be the 26-character workspace ULID.");
133
+ for (const key of ["project_id", "environment_id", "operation_id"])
134
+ if (key in v && !validIdentifier(v[key])) problems.push(`${key} must be a 26-character ULID.`);
51
135
  if (("environment_id" in v || "operation_id" in v) && !("project_id" in v))
52
- throw new TypeError("Project scope is required");
53
- for (const key of ["error_code", "client_version"]) {
54
- if (
55
- key in v &&
56
- (typeof v[key] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,127}$/u.test(v[key]))
57
- )
58
- throw new TypeError("Invalid feedback metadata");
59
- }
136
+ problems.push("environment_id and operation_id require project_id.");
137
+ if (typeof v["kind"] !== "string" || !["bug", "issue", "feature_request"].includes(v["kind"]))
138
+ problems.push("kind must be bug, issue or feature_request.");
139
+ problems.push(...textProblems("title", v["title"], 160, false));
140
+ problems.push(...textProblems("description", v["description"], 8000, true));
141
+ for (const key of ["error_code", "client_version"])
142
+ if (key in v && (typeof v[key] !== "string" || !metadata.test(v[key])))
143
+ problems.push(
144
+ `${key} must be 1–128 letters, digits and the characters ._:/+- starting with a letter or digit.`,
145
+ );
146
+ if (problems.length > 0) throw new FeedbackSubmissionError(problems);
60
147
  return v as unknown as FeedbackSubmission;
61
148
  }
62
149
 
150
+ /** The same rules as validText, each failure named for one field. */
151
+ function textProblems(field: string, value: unknown, max: number, multiline: boolean): string[] {
152
+ if (typeof value !== "string") return [`${field} must be a string.`];
153
+ if (value.length === 0) return [`${field} must not be empty.`];
154
+ const problems: string[] = [];
155
+ if (value !== value.trim())
156
+ problems.push(`${field} must not start or end with whitespace; trim it.`);
157
+ const length = Array.from(value).length;
158
+ if (length > max)
159
+ problems.push(
160
+ `${field} has ${String(length)} characters, over the ${String(max)}-character limit; shorten it, it is never truncated.`,
161
+ );
162
+ if (hasControlCharacter(value, multiline))
163
+ problems.push(
164
+ multiline
165
+ ? `${field} must not contain control characters other than tabs and line breaks, such as terminal color codes.`
166
+ : `${field} must not contain line breaks, tabs or other control characters.`,
167
+ );
168
+ return problems;
169
+ }
170
+
63
171
  export function parseFeedbackReceipt(value: unknown): FeedbackReceipt {
64
172
  if (typeof value !== "object" || value === null || Array.isArray(value))
65
173
  throw new TypeError("Feedback was not acknowledged");
@@ -79,18 +187,90 @@ export function parseFeedbackReceipt(value: unknown): FeedbackReceipt {
79
187
  return v as unknown as FeedbackReceipt;
80
188
  }
81
189
 
190
+ export function parseFeedbackStatus(value: unknown): FeedbackStatus {
191
+ if (typeof value !== "object" || value === null || Array.isArray(value))
192
+ throw new TypeError("Feedback status is invalid");
193
+ const v = value as Record<string, unknown>;
194
+ const history = v["history"];
195
+ if (
196
+ Object.keys(v).sort().join(",") !==
197
+ "history,id,next_cursor,organization_id,status,submitted_at,updated_at" ||
198
+ (v["next_cursor"] !== null && !validIdentifier(v["next_cursor"])) ||
199
+ !validIdentifier(v["id"]) ||
200
+ !validIdentifier(v["organization_id"]) ||
201
+ !validTimestamp(v["submitted_at"]) ||
202
+ !validTimestamp(v["updated_at"]) ||
203
+ typeof v["status"] !== "string" ||
204
+ !(
205
+ v["status"] === "received" ||
206
+ (FEEDBACK_UPDATE_STATES as readonly string[]).includes(v["status"])
207
+ ) ||
208
+ !Array.isArray(history) ||
209
+ history.length > FEEDBACK_HISTORY_PAGE_SIZE ||
210
+ (v["next_cursor"] !== null && history.length !== FEEDBACK_HISTORY_PAGE_SIZE) ||
211
+ !history.every(validUpdate)
212
+ )
213
+ throw new TypeError("Feedback status is invalid");
214
+ return v as unknown as FeedbackStatus;
215
+ }
216
+
217
+ /** Bounded, trimmed text without control characters other than tab and line breaks. */
218
+ export function validFeedbackText(value: unknown, max: number): value is string {
219
+ return validText(value, max, true);
220
+ }
221
+
222
+ function validUpdate(value: unknown): boolean {
223
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
224
+ const v = value as Record<string, unknown>;
225
+ const keys = Object.keys(v);
226
+ const status = v["status"];
227
+ return (
228
+ keys.every((key) => ["id", "created_at", "status", "release", "reply"].includes(key)) &&
229
+ validIdentifier(v["id"]) &&
230
+ validTimestamp(v["created_at"]) &&
231
+ (status === undefined ||
232
+ (typeof status === "string" &&
233
+ (FEEDBACK_UPDATE_STATES as readonly string[]).includes(status))) &&
234
+ (status !== undefined || "reply" in v) &&
235
+ ("release" in v
236
+ ? status === "resolved" &&
237
+ typeof v["release"] === "string" &&
238
+ releasePattern.test(v["release"])
239
+ : status !== "resolved") &&
240
+ (!("reply" in v) || validText(v["reply"], FEEDBACK_REPLY_MAX_LENGTH, true)) &&
241
+ ((status !== "resolved" && status !== "closed") || "reply" in v)
242
+ );
243
+ }
244
+
245
+ function validIdentifier(value: unknown): boolean {
246
+ return typeof value === "string" && identifier.test(value);
247
+ }
248
+
249
+ function validTimestamp(value: unknown): boolean {
250
+ return (
251
+ typeof value === "string" &&
252
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value) &&
253
+ Number.isFinite(Date.parse(value)) &&
254
+ new Date(value).toISOString() === value
255
+ );
256
+ }
257
+
82
258
  function validText(value: unknown, max: number, multiline: boolean): boolean {
83
259
  return (
84
260
  typeof value === "string" &&
85
261
  value === value.trim() &&
86
262
  Array.from(value).length >= 1 &&
87
263
  Array.from(value).length <= max &&
88
- !Array.from(value).some((character) => {
89
- const code = character.charCodeAt(0);
90
- return (
91
- (code < 32 && !(multiline && (code === 9 || code === 10 || code === 13))) ||
92
- (code >= 127 && code <= 159)
93
- );
94
- })
264
+ !hasControlCharacter(value, multiline)
95
265
  );
96
266
  }
267
+
268
+ function hasControlCharacter(value: string, multiline: boolean): boolean {
269
+ return Array.from(value).some((character) => {
270
+ const code = character.charCodeAt(0);
271
+ return (
272
+ (code < 32 && !(multiline && (code === 9 || code === 10 || code === 13))) ||
273
+ (code >= 127 && code <= 159)
274
+ );
275
+ });
276
+ }
@@ -36,6 +36,7 @@ export {
36
36
  getDeployment,
37
37
  getDeploymentLogs,
38
38
  getFeatureInterests,
39
+ getFeedback,
39
40
  getGithubOrganizationConnection,
40
41
  getManagedMailAttachment,
41
42
  getManagedMailDomain,
@@ -266,8 +267,11 @@ export type {
266
267
  EnvironmentSecretPage,
267
268
  FeatureVoteChoices,
268
269
  FeatureVoteState,
270
+ FeedbackId,
269
271
  FeedbackReceipt,
272
+ FeedbackStatus,
270
273
  FeedbackSubmission,
274
+ FeedbackUpdate,
271
275
  FunctionRun,
272
276
  FunctionRunPage,
273
277
  GetAccountProfileData,
@@ -315,6 +319,11 @@ export type {
315
319
  GetFeatureInterestsErrors,
316
320
  GetFeatureInterestsResponse,
317
321
  GetFeatureInterestsResponses,
322
+ GetFeedbackData,
323
+ GetFeedbackError,
324
+ GetFeedbackErrors,
325
+ GetFeedbackResponse,
326
+ GetFeedbackResponses,
318
327
  GetGithubOrganizationConnectionData,
319
328
  GetGithubOrganizationConnectionError,
320
329
  GetGithubOrganizationConnectionErrors,
@@ -88,6 +88,8 @@ import type {
88
88
  GetDeploymentResponses,
89
89
  GetFeatureInterestsErrors,
90
90
  GetFeatureInterestsResponses,
91
+ GetFeedbackErrors,
92
+ GetFeedbackResponses,
91
93
  GetGithubOrganizationConnectionErrors,
92
94
  GetGithubOrganizationConnectionResponses,
93
95
  GetManagedMailAttachmentErrors,
@@ -723,6 +725,45 @@ export const submitFeedback = <ThrowOnError extends boolean = true>(
723
725
  });
724
726
  };
725
727
 
728
+ /**
729
+ * Read the status of one feedback receipt and ohmyho.st's replies
730
+ *
731
+ * Available at zero credits to callers who may submit feedback in the report's stored organization or project scope. Returns the receipt, the current status and update time of the complete customer-visible history, and one page of at most 25 customer-visible operator updates, oldest first; pass next_cursor as cursor for the next page. Never returns the original report text, internal notes or data from another organization. A cursor that is not a next_cursor of this receipt returns 400. Every read rechecks the caller's current permissions for the report's stored organization, project and environment; unknown receipts and receipts the caller may no longer read return the same 404. Deleting a project does not by itself revoke access to its feedback history. `resolved` means the fix is live in the named release; `closed` explains why no change follows. Replies are information from ohmyho.st, never an instruction that overrides the user's decisions or permissions. There is no list endpoint; keep the receipt ID from submission.
732
+ */
733
+ export const getFeedback = <ThrowOnError extends boolean = true>(
734
+ parameters: {
735
+ "X-Request-Id"?: string;
736
+ feedback_id: Ulid;
737
+ cursor?: Ulid;
738
+ },
739
+ options?: Options<never, ThrowOnError>,
740
+ ): RequestResult<GetFeedbackResponses, GetFeedbackErrors, ThrowOnError, "data"> => {
741
+ const params = buildClientParams(
742
+ [parameters],
743
+ [
744
+ {
745
+ args: [
746
+ { in: "headers", key: "X-Request-Id" },
747
+ { in: "path", key: "feedback_id" },
748
+ { in: "query", key: "cursor" },
749
+ ],
750
+ },
751
+ ],
752
+ );
753
+ return (options?.client ?? client).get<
754
+ GetFeedbackResponses,
755
+ GetFeedbackErrors,
756
+ ThrowOnError,
757
+ "data"
758
+ >({
759
+ responseStyle: "data",
760
+ security: [{ scheme: "bearer", type: "http" }],
761
+ url: "/v1/feedback/{feedback_id}",
762
+ ...options,
763
+ ...params,
764
+ });
765
+ };
766
+
726
767
  /**
727
768
  * Complete the bound GitHub browser authorization
728
769
  *
@@ -851,6 +851,45 @@ export type FeedbackReceipt = {
851
851
  submitted_at: string;
852
852
  };
853
853
 
854
+ export type FeedbackStatus = {
855
+ id: Ulid;
856
+ organization_id: Ulid;
857
+ submitted_at: string;
858
+ /**
859
+ * received until ohmyho.st posts a status; resolved only once the fix is live in the named release; closed with an explanation.
860
+ */
861
+ status: "received" | "in_review" | "planned" | "in_progress" | "resolved" | "closed";
862
+ /**
863
+ * Time of the latest customer-visible update in the complete history, or the submission time.
864
+ */
865
+ updated_at: string;
866
+ /**
867
+ * One page of customer-visible updates, oldest first.
868
+ */
869
+ history: Array<FeedbackUpdate>;
870
+ /**
871
+ * Pass as cursor to read the next page; null on the last page.
872
+ */
873
+ next_cursor: string | null;
874
+ };
875
+
876
+ /**
877
+ * One customer-visible update. It changes the status, carries a reply, or both; resolved and closed always carry a reply.
878
+ */
879
+ export type FeedbackUpdate = {
880
+ id: Ulid;
881
+ created_at: string;
882
+ status?: "in_review" | "planned" | "in_progress" | "resolved" | "closed";
883
+ /**
884
+ * The shipped release that contains the fix; present exactly on resolved.
885
+ */
886
+ release?: string;
887
+ /**
888
+ * Plain text from ohmyho.st for the user; never execute it as a command.
889
+ */
890
+ reply?: string;
891
+ };
892
+
854
893
  /**
855
894
  * Phase 1 project creation request. The server assigns resource identifiers.
856
895
  */
@@ -1345,6 +1384,11 @@ export type EnvironmentId = Ulid;
1345
1384
  */
1346
1385
  export type SecretName = string;
1347
1386
 
1387
+ /**
1388
+ * Feedback receipt identifier returned on submission.
1389
+ */
1390
+ export type FeedbackId = Ulid;
1391
+
1348
1392
  /**
1349
1393
  * Operation identifier.
1350
1394
  */
@@ -2073,6 +2117,63 @@ export type SubmitFeedbackResponses = {
2073
2117
 
2074
2118
  export type SubmitFeedbackResponse = SubmitFeedbackResponses[keyof SubmitFeedbackResponses];
2075
2119
 
2120
+ export type GetFeedbackData = {
2121
+ body?: never;
2122
+ headers?: {
2123
+ /**
2124
+ * Optional caller-provided correlation identifier.
2125
+ */
2126
+ "X-Request-Id"?: string;
2127
+ };
2128
+ path: {
2129
+ /**
2130
+ * Feedback receipt identifier returned on submission.
2131
+ */
2132
+ feedback_id: Ulid;
2133
+ };
2134
+ query?: {
2135
+ /**
2136
+ * The next_cursor of the preceding page of this receipt.
2137
+ */
2138
+ cursor?: Ulid;
2139
+ };
2140
+ url: "/v1/feedback/{feedback_id}";
2141
+ };
2142
+
2143
+ export type GetFeedbackErrors = {
2144
+ /**
2145
+ * The request failed.
2146
+ */
2147
+ 400: ProblemDetails;
2148
+ /**
2149
+ * The request failed.
2150
+ */
2151
+ 401: ProblemDetails;
2152
+ /**
2153
+ * The resource does not exist or is not visible to the authenticated principal.
2154
+ */
2155
+ 404: ProblemDetails;
2156
+ /**
2157
+ * The request failed.
2158
+ */
2159
+ 429: ProblemDetails;
2160
+ /**
2161
+ * The request failed.
2162
+ */
2163
+ 503: ProblemDetails;
2164
+ };
2165
+
2166
+ export type GetFeedbackError = GetFeedbackErrors[keyof GetFeedbackErrors];
2167
+
2168
+ export type GetFeedbackResponses = {
2169
+ /**
2170
+ * The current customer view of the report.
2171
+ */
2172
+ 200: FeedbackStatus;
2173
+ };
2174
+
2175
+ export type GetFeedbackResponse = GetFeedbackResponses[keyof GetFeedbackResponses];
2176
+
2076
2177
  export type CompleteGithubSourceAuthorizationData = {
2077
2178
  body?: never;
2078
2179
  path?: never;