@amerged/ohmyhost-mcp 0.1.50 → 0.1.52

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.
@@ -0,0 +1,179 @@
1
+ /** Credential-free public contracts for temporary previews of saved Git commits. */
2
+ export const PREVIEW_MAX_LIFETIME_MILLISECONDS = 60 * 60_000;
3
+ export const PREVIEW_IDLE_MILLISECONDS = 5 * 60_000;
4
+
5
+ export type PreviewDataEnvironment = "dev" | "prod";
6
+ export type PreviewStatus =
7
+ | "queued"
8
+ | "starting"
9
+ | "ready"
10
+ | "sleeping"
11
+ | "failed"
12
+ | "stopped"
13
+ | "expired"
14
+ | "superseded";
15
+
16
+ export interface PreviewCreateRequest {
17
+ readonly commit_sha: string;
18
+ /** Paid may select Dev or Prod. Free can select only Prod. Omission follows the current tier. */
19
+ readonly data_environment?: PreviewDataEnvironment;
20
+ }
21
+
22
+ export interface PreviewComputeQuote {
23
+ readonly rate_card_id: "cloudflare-preview-2026-11-06-v1";
24
+ readonly meter: "preview.container.basic" | "preview.container.standard-1";
25
+ readonly unit: "usd_pico";
26
+ readonly reserved_seconds: 60;
27
+ /** Microcredits held for one runtime interval; unused credit is released. */
28
+ readonly credit_micros: string;
29
+ readonly memory_mib: 1024 | 4096;
30
+ readonly vcpus: 0.25 | 0.5;
31
+ readonly disk_gb: 4 | 8;
32
+ }
33
+
34
+ export interface Preview {
35
+ readonly preview_id: string;
36
+ readonly project_id: string;
37
+ readonly commit_sha: string;
38
+ readonly data_environment: PreviewDataEnvironment;
39
+ readonly status: PreviewStatus;
40
+ readonly created_at: string;
41
+ readonly expires_at: string;
42
+ /** A working preview URL is advertised only after the runtime has confirmed readiness. */
43
+ readonly url: string | null;
44
+ readonly instance_type: "basic" | "standard-1";
45
+ readonly compute_quote: PreviewComputeQuote;
46
+ readonly shared_data: boolean;
47
+ readonly failure_code: string | null;
48
+ }
49
+ export interface PreviewPage {
50
+ readonly previews: readonly Preview[];
51
+ }
52
+
53
+ export function parsePreviewCreateRequest(value: unknown): PreviewCreateRequest {
54
+ const body = record(value);
55
+ exact(body, ["commit_sha", ...("data_environment" in body ? ["data_environment"] : [])]);
56
+ if (!commit(body["commit_sha"])) invalid();
57
+ if (
58
+ "data_environment" in body &&
59
+ body["data_environment"] !== "dev" &&
60
+ body["data_environment"] !== "prod"
61
+ )
62
+ invalid();
63
+ return body as unknown as PreviewCreateRequest;
64
+ }
65
+
66
+ export function assertPreview(value: unknown): asserts value is Preview {
67
+ const body = record(value);
68
+ exact(body, [
69
+ "preview_id",
70
+ "project_id",
71
+ "commit_sha",
72
+ "data_environment",
73
+ "status",
74
+ "created_at",
75
+ "expires_at",
76
+ "url",
77
+ "instance_type",
78
+ "compute_quote",
79
+ "shared_data",
80
+ "failure_code",
81
+ ]);
82
+ const created = instant(body["created_at"]),
83
+ expires = instant(body["expires_at"]);
84
+ if (
85
+ !ulid(body["preview_id"]) ||
86
+ !ulid(body["project_id"]) ||
87
+ !commit(body["commit_sha"]) ||
88
+ (body["data_environment"] !== "dev" && body["data_environment"] !== "prod") ||
89
+ ![
90
+ "queued",
91
+ "starting",
92
+ "ready",
93
+ "sleeping",
94
+ "failed",
95
+ "stopped",
96
+ "expired",
97
+ "superseded",
98
+ ].includes(String(body["status"])) ||
99
+ expires <= created ||
100
+ expires - created > PREVIEW_MAX_LIFETIME_MILLISECONDS ||
101
+ (body["instance_type"] !== "basic" && body["instance_type"] !== "standard-1") ||
102
+ typeof body["shared_data"] !== "boolean" ||
103
+ (body["failure_code"] !== null &&
104
+ (typeof body["failure_code"] !== "string" ||
105
+ !/^(?:[a-z][a-z0-9_]{0,99}|[A-Z][A-Z0-9_]{0,99})$/u.test(body["failure_code"])))
106
+ )
107
+ invalid();
108
+ const quote = record(body["compute_quote"]);
109
+ exact(quote, [
110
+ "rate_card_id",
111
+ "meter",
112
+ "unit",
113
+ "reserved_seconds",
114
+ "credit_micros",
115
+ "memory_mib",
116
+ "vcpus",
117
+ "disk_gb",
118
+ ]);
119
+ const basic = body["instance_type"] === "basic";
120
+ if (
121
+ quote["rate_card_id"] !== "cloudflare-preview-2026-11-06-v1" ||
122
+ quote["meter"] !== `preview.container.${String(body["instance_type"])}` ||
123
+ quote["unit"] !== "usd_pico" ||
124
+ quote["reserved_seconds"] !== 60 ||
125
+ typeof quote["credit_micros"] !== "string" ||
126
+ !/^[1-9][0-9]{0,15}$/u.test(quote["credit_micros"]) ||
127
+ !Number.isSafeInteger(Number(quote["credit_micros"])) ||
128
+ quote["memory_mib"] !== (basic ? 1024 : 4096) ||
129
+ quote["vcpus"] !== (basic ? 0.25 : 0.5) ||
130
+ quote["disk_gb"] !== (basic ? 4 : 8)
131
+ )
132
+ invalid();
133
+ const usable = body["status"] === "ready" || body["status"] === "sleeping";
134
+ if (body["url"] === null) {
135
+ if (usable) invalid();
136
+ } else {
137
+ if (!usable || typeof body["url"] !== "string") invalid();
138
+ let url: URL;
139
+ try {
140
+ url = new URL(body["url"]);
141
+ } catch {
142
+ invalid();
143
+ }
144
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.hash !== "")
145
+ invalid();
146
+ }
147
+ }
148
+
149
+ export function assertPreviewPage(value: unknown): asserts value is PreviewPage {
150
+ const body = record(value);
151
+ exact(body, ["previews"]);
152
+ if (!Array.isArray(body["previews"]) || body["previews"].length > 100) invalid();
153
+ for (const entry of body["previews"]) assertPreview(entry);
154
+ }
155
+ function record(value: unknown): Record<string, unknown> {
156
+ if (typeof value !== "object" || value === null || Array.isArray(value)) invalid();
157
+ return value as Record<string, unknown>;
158
+ }
159
+ function exact(value: Record<string, unknown>, keys: readonly string[]): void {
160
+ if (Object.keys(value).sort().join(",") !== [...keys].sort().join(",")) invalid();
161
+ }
162
+ function ulid(value: unknown): value is string {
163
+ return typeof value === "string" && /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(value);
164
+ }
165
+ function commit(value: unknown): value is string {
166
+ return typeof value === "string" && /^[a-f0-9]{40}$/u.test(value);
167
+ }
168
+ function instant(value: unknown): number {
169
+ if (
170
+ typeof value !== "string" ||
171
+ !Number.isFinite(Date.parse(value)) ||
172
+ new Date(value).toISOString() !== value
173
+ )
174
+ invalid();
175
+ return Date.parse(value);
176
+ }
177
+ function invalid(): never {
178
+ throw new TypeError("Preview contract is invalid");
179
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohmyhost/sdk-ts",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -16,6 +16,7 @@ export interface VerifyWorkosWebhookInput {
16
16
  readonly rawBody: Uint8Array;
17
17
  readonly signatureHeader: string;
18
18
  readonly secret: string;
19
+ readonly additionalSecrets?: readonly string[];
19
20
  readonly now: Date;
20
21
  readonly toleranceMs: number;
21
22
  readonly maximumBodyBytes: number;
@@ -66,9 +67,20 @@ export const parseWorkosWebhookEnvelope = (value: unknown): WorkosWebhookEnvelop
66
67
  export const verifyAndParseWorkosWebhook = async (
67
68
  input: VerifyWorkosWebhookInput,
68
69
  ): Promise<WorkosWebhookEnvelope> => {
69
- if (input.secret.length === 0) {
70
+ if (
71
+ typeof input.secret !== "string" ||
72
+ input.secret.length === 0 ||
73
+ (input.additionalSecrets !== undefined &&
74
+ (!Array.isArray(input.additionalSecrets) ||
75
+ input.additionalSecrets.length > 1 ||
76
+ (input.additionalSecrets.length === 1 &&
77
+ (!Object.hasOwn(input.additionalSecrets, 0) ||
78
+ typeof input.additionalSecrets[0] !== "string" ||
79
+ input.additionalSecrets[0].length === 0))))
80
+ ) {
70
81
  throw new WorkosContractError("invalid_webhook_secret", "Webhook secret is invalid");
71
82
  }
83
+ const secrets = [...new Set([input.secret, ...(input.additionalSecrets ?? [])])];
72
84
  if (
73
85
  !Number.isSafeInteger(input.maximumBodyBytes) ||
74
86
  input.maximumBodyBytes <= 0 ||
@@ -94,11 +106,11 @@ export const verifyAndParseWorkosWebhook = async (
94
106
  const message = new Uint8Array(prefix.byteLength + input.rawBody.byteLength);
95
107
  message.set(prefix);
96
108
  message.set(input.rawBody, prefix.byteLength);
97
- const importKey = async () => {
109
+ const importKey = async (secret: string) => {
98
110
  try {
99
111
  return await globalThis.crypto.subtle.importKey(
100
112
  "raw",
101
- new TextEncoder().encode(input.secret),
113
+ new TextEncoder().encode(secret),
102
114
  { name: "HMAC", hash: "SHA-256" },
103
115
  false,
104
116
  ["verify"],
@@ -107,21 +119,26 @@ export const verifyAndParseWorkosWebhook = async (
107
119
  throw new WorkosContractError("invalid_webhook_secret", "Webhook secret is invalid");
108
120
  }
109
121
  };
110
- const key = await importKey();
122
+ const keys = await Promise.all(secrets.map(importKey));
111
123
  let valid = false;
112
- for (const expectedHexDigest of signature.signatures) {
113
- const expected = Uint8Array.from(expectedHexDigest.match(/.{2}/gu) ?? [], (byte) =>
114
- Number.parseInt(byte, 16),
115
- );
116
- try {
117
- valid = (await globalThis.crypto.subtle.verify("HMAC", key, expected, message)) || valid;
118
- } catch {
119
- // Only a verified signature can change the accumulated result.
124
+ for (const key of keys) {
125
+ for (const expectedHexDigest of signature.signatures) {
126
+ const expected = Uint8Array.from(expectedHexDigest.match(/.{2}/gu) ?? [], (byte) =>
127
+ Number.parseInt(byte, 16),
128
+ );
129
+ try {
130
+ valid = (await globalThis.crypto.subtle.verify("HMAC", key, expected, message)) || valid;
131
+ } catch {
132
+ // Only a verified signature can change the accumulated result.
133
+ }
120
134
  }
121
135
  }
122
136
  if (!valid)
123
137
  throw new WorkosContractError("invalid_webhook_signature", "Webhook signature is invalid");
138
+ // Parse the authenticated snapshot, never caller-owned bytes which may change across awaits.
124
139
  return parseWorkosWebhookEnvelope(
125
- parseJsonObject(new TextDecoder("utf-8", { fatal: true }).decode(input.rawBody)),
140
+ parseJsonObject(
141
+ new TextDecoder("utf-8", { fatal: true }).decode(message.subarray(prefix.byteLength)),
142
+ ),
126
143
  );
127
144
  };