@amerged/ohmyhost-mcp 0.1.10

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.
Files changed (72) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +11 -0
  4. package/THIRD_PARTY_NOTICES.md +474 -0
  5. package/dist/chunk-7MV2EVXV.js +30779 -0
  6. package/dist/index.js +9 -0
  7. package/dist/stdio-main.js +5607 -0
  8. package/package.json +32 -0
  9. package/src/apps/mcp/index.ts +30 -0
  10. package/src/apps/mcp/local-client.ts +891 -0
  11. package/src/apps/mcp/local-server.ts +1512 -0
  12. package/src/apps/mcp/stdio-main.ts +26 -0
  13. package/src/apps/product-cli/bin.ts +24 -0
  14. package/src/apps/product-cli/cli.ts +2271 -0
  15. package/src/apps/product-cli/command.ts +1811 -0
  16. package/src/apps/product-cli/composition.ts +823 -0
  17. package/src/apps/product-cli/credential-store.ts +149 -0
  18. package/src/apps/product-cli/device-token-expiry.ts +89 -0
  19. package/src/apps/product-cli/file-credential-store.ts +166 -0
  20. package/src/apps/product-cli/index.ts +23 -0
  21. package/src/apps/product-cli/product-api.ts +1082 -0
  22. package/src/apps/product-cli/project-link-store.ts +30 -0
  23. package/src/apps/product-cli/psql-session.ts +29 -0
  24. package/src/apps/product-cli/repository-init.ts +1622 -0
  25. package/src/apps/product-cli/runtime-contract.ts +1747 -0
  26. package/src/apps/product-cli/user-token-file.ts +214 -0
  27. package/src/apps/product-cli/workspace.ts +92 -0
  28. package/src/packages/agent-skills/generated-skill-resources.ts +147 -0
  29. package/src/packages/agent-skills/index.ts +37 -0
  30. package/src/packages/contracts/application-root.ts +80 -0
  31. package/src/packages/contracts/billing.ts +172 -0
  32. package/src/packages/contracts/credit-pricing.ts +214 -0
  33. package/src/packages/contracts/database-access.ts +257 -0
  34. package/src/packages/contracts/database-compute.ts +72 -0
  35. package/src/packages/contracts/database-migration-admission.ts +235 -0
  36. package/src/packages/contracts/feedback.ts +96 -0
  37. package/src/packages/contracts/framework-admission.ts +280 -0
  38. package/src/packages/contracts/framework-family.ts +18 -0
  39. package/src/packages/contracts/functions.ts +76 -0
  40. package/src/packages/contracts/index.ts +245 -0
  41. package/src/packages/contracts/operation-deployment-result.ts +165 -0
  42. package/src/packages/contracts/operation-failure.ts +150 -0
  43. package/src/packages/contracts/platform-origins.ts +5 -0
  44. package/src/packages/contracts/project-context.ts +59 -0
  45. package/src/packages/contracts/project-exports.ts +102 -0
  46. package/src/packages/contracts/user-api-keys.ts +98 -0
  47. package/src/packages/sdk-ts/generated/client/client.gen.ts +277 -0
  48. package/src/packages/sdk-ts/generated/client/index.ts +27 -0
  49. package/src/packages/sdk-ts/generated/client/types.gen.ts +217 -0
  50. package/src/packages/sdk-ts/generated/client/utils.gen.ts +316 -0
  51. package/src/packages/sdk-ts/generated/client.gen.ts +24 -0
  52. package/src/packages/sdk-ts/generated/core/auth.gen.ts +48 -0
  53. package/src/packages/sdk-ts/generated/core/bodySerializer.gen.ts +82 -0
  54. package/src/packages/sdk-ts/generated/core/params.gen.ts +178 -0
  55. package/src/packages/sdk-ts/generated/core/pathSerializer.gen.ts +171 -0
  56. package/src/packages/sdk-ts/generated/core/queryKeySerializer.gen.ts +117 -0
  57. package/src/packages/sdk-ts/generated/core/serverSentEvents.gen.ts +242 -0
  58. package/src/packages/sdk-ts/generated/core/types.gen.ts +114 -0
  59. package/src/packages/sdk-ts/generated/core/utils.gen.ts +140 -0
  60. package/src/packages/sdk-ts/generated/index.ts +578 -0
  61. package/src/packages/sdk-ts/generated/sdk.gen.ts +3440 -0
  62. package/src/packages/sdk-ts/generated/types.gen.ts +5790 -0
  63. package/src/packages/sdk-ts/index.ts +7 -0
  64. package/src/packages/workos-auth-contracts/device-flow.ts +271 -0
  65. package/src/packages/workos-auth-contracts/index.ts +8 -0
  66. package/src/packages/workos-auth-contracts/management-rpc.ts +379 -0
  67. package/src/packages/workos-auth-contracts/organizations.ts +113 -0
  68. package/src/packages/workos-auth-contracts/parsing.ts +75 -0
  69. package/src/packages/workos-auth-contracts/session-status.ts +53 -0
  70. package/src/packages/workos-auth-contracts/tokens.ts +216 -0
  71. package/src/packages/workos-auth-contracts/user-api-keys.ts +85 -0
  72. package/src/packages/workos-auth-contracts/webhooks.ts +127 -0
@@ -0,0 +1,172 @@
1
+ export type BillingTaxIssueCode =
2
+ | "billing_tax_location_required"
3
+ | "billing_tax_calculation_failed"
4
+ | "billing_tax_configuration_required";
5
+ export interface BillingTaxIssue {
6
+ code: BillingTaxIssueCode;
7
+ invoice_id: string;
8
+ observed_at: string;
9
+ required_action: "open_billing_portal" | "contact_support";
10
+ }
11
+
12
+ export interface BillingCheckoutView {
13
+ organization_id: string;
14
+ checkout_id: string;
15
+ offer: "topup" | "paid";
16
+ state: "open" | "complete" | "expired";
17
+ payment_confirmed: boolean;
18
+ url: string | null;
19
+ expires_at: string;
20
+ packs: number;
21
+ credited_micros: string;
22
+ revoked_micros: string;
23
+ paid_until: string | null;
24
+ required_action: "none" | "complete_checkout" | "open_billing_portal" | "contact_support";
25
+ billing_issue: BillingTaxIssue | null;
26
+ }
27
+ export interface BillingPortalView {
28
+ organization_id: string;
29
+ url: string;
30
+ created_at: string;
31
+ }
32
+ export function assertBillingTaxIssue(value: unknown): asserts value is BillingTaxIssue | null {
33
+ if (value === null) return;
34
+ if (!value || typeof value !== "object" || Array.isArray(value))
35
+ throw new Error("billing_response_invalid");
36
+ const v = value as Record<string, unknown>;
37
+ if (
38
+ Object.keys(v).sort().join(",") !== "code,invoice_id,observed_at,required_action" ||
39
+ ![
40
+ "billing_tax_location_required",
41
+ "billing_tax_calculation_failed",
42
+ "billing_tax_configuration_required",
43
+ ].includes(String(v["code"])) ||
44
+ typeof v["invoice_id"] !== "string" ||
45
+ !/^in_[A-Za-z0-9_]{1,253}$/u.test(v["invoice_id"]) ||
46
+ !timestamp(v["observed_at"]) ||
47
+ v["required_action"] !==
48
+ (v["code"] === "billing_tax_location_required" ? "open_billing_portal" : "contact_support")
49
+ )
50
+ throw new Error("billing_response_invalid");
51
+ }
52
+ const ulid = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u;
53
+ const timestamp = (v: unknown): v is string =>
54
+ typeof v === "string" && Number.isFinite(Date.parse(v)) && new Date(v).toISOString() === v;
55
+ function stripeUrl(value: unknown, host: string): value is string {
56
+ if (typeof value !== "string" || value.length > 8192) return false;
57
+ try {
58
+ const u = new URL(value);
59
+ return u.protocol === "https:" && u.hostname === host && !u.username && !u.password && !u.port;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ export function assertBillingCheckout(value: unknown): asserts value is BillingCheckoutView {
65
+ if (!value || typeof value !== "object" || Array.isArray(value))
66
+ throw new Error("billing_response_invalid");
67
+ const v = value as Record<string, unknown>;
68
+ if (
69
+ Object.keys(v).sort().join(",") !==
70
+ "billing_issue,checkout_id,credited_micros,expires_at,offer,organization_id,packs,paid_until,payment_confirmed,required_action,revoked_micros,state,url" ||
71
+ typeof v["organization_id"] !== "string" ||
72
+ !ulid.test(v["organization_id"]) ||
73
+ typeof v["checkout_id"] !== "string" ||
74
+ !ulid.test(v["checkout_id"]) ||
75
+ typeof v["offer"] !== "string" ||
76
+ !["topup", "paid"].includes(v["offer"]) ||
77
+ typeof v["state"] !== "string" ||
78
+ !["open", "complete", "expired"].includes(v["state"]) ||
79
+ typeof v["payment_confirmed"] !== "boolean" ||
80
+ typeof v["required_action"] !== "string" ||
81
+ !["none", "complete_checkout", "open_billing_portal", "contact_support"].includes(
82
+ v["required_action"],
83
+ ) ||
84
+ (v["url"] !== null && !stripeUrl(v["url"], "checkout.stripe.com")) ||
85
+ !timestamp(v["expires_at"]) ||
86
+ (v["paid_until"] !== null && !timestamp(v["paid_until"])) ||
87
+ !Number.isInteger(v["packs"]) ||
88
+ Number(v["packs"]) < 1 ||
89
+ Number(v["packs"]) > 100 ||
90
+ (v["offer"] === "paid" && v["packs"] !== 1) ||
91
+ ![v["credited_micros"], v["revoked_micros"]].every(
92
+ (x) => typeof x === "string" && /^(0|[1-9][0-9]{0,18})$/u.test(x),
93
+ )
94
+ )
95
+ throw new Error("billing_response_invalid");
96
+ assertBillingTaxIssue(v["billing_issue"]);
97
+ }
98
+ export function assertBillingPortal(value: unknown): asserts value is BillingPortalView {
99
+ if (!value || typeof value !== "object" || Array.isArray(value))
100
+ throw new Error("billing_response_invalid");
101
+ const v = value as Record<string, unknown>;
102
+ if (
103
+ Object.keys(v).sort().join(",") !== "created_at,organization_id,url" ||
104
+ typeof v["organization_id"] !== "string" ||
105
+ !ulid.test(v["organization_id"]) ||
106
+ !timestamp(v["created_at"]) ||
107
+ !stripeUrl(v["url"], "billing.stripe.com")
108
+ )
109
+ throw new Error("billing_response_invalid");
110
+ }
111
+
112
+ export interface RechargeView {
113
+ organization_id: string;
114
+ portal_available: boolean;
115
+ enabled: boolean;
116
+ revision: number;
117
+ status:
118
+ | "off"
119
+ | "setup_required"
120
+ | "on"
121
+ | "payment_required"
122
+ | "monthly_limit"
123
+ | "needs_reconciliation"
124
+ | "tax_required";
125
+ monthly_limit_minor: number;
126
+ spent_minor: number;
127
+ currency: "usd";
128
+ amount_minor: 900;
129
+ credits: 1000;
130
+ threshold_credits: 100;
131
+ setup_url: string | null;
132
+ invoice_url: string | null;
133
+ billing_issue: BillingTaxIssue | null;
134
+ }
135
+ export function assertRecharge(value: unknown): asserts value is RechargeView {
136
+ if (!value || typeof value !== "object" || Array.isArray(value))
137
+ throw new Error("billing_response_invalid");
138
+ const v = value as Record<string, unknown>;
139
+ if (
140
+ Object.keys(v).sort().join(",") !==
141
+ "amount_minor,billing_issue,credits,currency,enabled,invoice_url,monthly_limit_minor,organization_id,portal_available,revision,setup_url,spent_minor,status,threshold_credits" ||
142
+ typeof v["organization_id"] !== "string" ||
143
+ !ulid.test(v["organization_id"]) ||
144
+ typeof v["portal_available"] !== "boolean" ||
145
+ typeof v["enabled"] !== "boolean" ||
146
+ !Number.isSafeInteger(v["revision"]) ||
147
+ Number(v["revision"]) < 0 ||
148
+ !Number.isSafeInteger(v["monthly_limit_minor"]) ||
149
+ Number(v["monthly_limit_minor"]) < 1000 ||
150
+ Number(v["monthly_limit_minor"]) > 100000 ||
151
+ !Number.isSafeInteger(v["spent_minor"]) ||
152
+ Number(v["spent_minor"]) < 0 ||
153
+ v["currency"] !== "usd" ||
154
+ v["amount_minor"] !== 900 ||
155
+ v["credits"] !== 1000 ||
156
+ v["threshold_credits"] !== 100 ||
157
+ typeof v["status"] !== "string" ||
158
+ ![
159
+ "off",
160
+ "setup_required",
161
+ "on",
162
+ "payment_required",
163
+ "monthly_limit",
164
+ "needs_reconciliation",
165
+ "tax_required",
166
+ ].includes(v["status"]) ||
167
+ (v["setup_url"] !== null && !stripeUrl(v["setup_url"], "checkout.stripe.com")) ||
168
+ (v["invoice_url"] !== null && !stripeUrl(v["invoice_url"], "invoice.stripe.com"))
169
+ )
170
+ throw new Error("billing_response_invalid");
171
+ assertBillingTaxIssue(v["billing_issue"]);
172
+ }
@@ -0,0 +1,214 @@
1
+ export interface OrganizationCreditUsage {
2
+ organization_id: string;
3
+ month: string;
4
+ as_of: string;
5
+ unit: "microcredits";
6
+ data: {
7
+ project_id: string;
8
+ reserved_micros: string;
9
+ meters: {
10
+ environment_id: string | null;
11
+ meter: string;
12
+ unit: string;
13
+ rate_card_id: string;
14
+ quantity: string;
15
+ charged_micros: string;
16
+ funded_micros: string;
17
+ platform_overrun_micros: string;
18
+ }[];
19
+ }[];
20
+ next_cursor: string | null;
21
+ }
22
+
23
+ /** A bounded read model, not a new source of accounting or billing authorization. */
24
+ export function assertOrganizationCreditUsage(
25
+ value: unknown,
26
+ ): asserts value is OrganizationCreditUsage {
27
+ const record = (input: unknown, keys: string) => {
28
+ if (
29
+ !input ||
30
+ typeof input !== "object" ||
31
+ Array.isArray(input) ||
32
+ Object.keys(input).sort().join() !== keys.split(",").sort().join()
33
+ )
34
+ throw new TypeError("Invalid usage fields");
35
+ return input as Record<string, unknown>;
36
+ };
37
+ const identifier = (input: unknown) =>
38
+ typeof input === "string" && /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(input);
39
+ const amount = (input: unknown) =>
40
+ typeof input === "string" && /^(0|[1-9][0-9]{0,37})$/u.test(input);
41
+ const report = record(value, "organization_id,month,as_of,unit,data,next_cursor");
42
+ if (
43
+ !identifier(report["organization_id"]) ||
44
+ typeof report["month"] !== "string" ||
45
+ !/^20[0-9]{2}-(0[1-9]|1[0-2])$/u.test(report["month"]) ||
46
+ typeof report["as_of"] !== "string" ||
47
+ !Number.isFinite(Date.parse(report["as_of"])) ||
48
+ new Date(report["as_of"]).toISOString() !== report["as_of"] ||
49
+ report["unit"] !== "microcredits" ||
50
+ !Array.isArray(report["data"]) ||
51
+ report["data"].length > 20 ||
52
+ (report["next_cursor"] !== null && !identifier(report["next_cursor"]))
53
+ )
54
+ throw new TypeError("Invalid usage report");
55
+ let previous = "";
56
+ for (const input of report["data"]) {
57
+ const project = record(input, "project_id,reserved_micros,meters");
58
+ if (
59
+ !identifier(project["project_id"]) ||
60
+ String(project["project_id"]) <= previous ||
61
+ !amount(project["reserved_micros"]) ||
62
+ !Array.isArray(project["meters"]) ||
63
+ project["meters"].length > 1000
64
+ )
65
+ throw new TypeError("Invalid usage project");
66
+ previous = String(project["project_id"]);
67
+ for (const input of project["meters"]) {
68
+ const meter = record(
69
+ input,
70
+ "environment_id,meter,unit,rate_card_id,quantity,charged_micros,funded_micros,platform_overrun_micros",
71
+ );
72
+ if (meter["environment_id"] !== null && !identifier(meter["environment_id"]))
73
+ throw new TypeError("Invalid usage environment");
74
+ for (const key of ["meter", "unit", "rate_card_id"])
75
+ if (typeof meter[key] !== "string" || !/^[A-Za-z0-9_.:/-]{1,128}$/u.test(meter[key]))
76
+ throw new TypeError("Invalid usage meter");
77
+ for (const key of ["quantity", "charged_micros", "funded_micros", "platform_overrun_micros"])
78
+ if (!amount(meter[key])) throw new TypeError("Invalid usage amount");
79
+ if (
80
+ BigInt(String(meter["charged_micros"])) !==
81
+ BigInt(String(meter["funded_micros"])) + BigInt(String(meter["platform_overrun_micros"]))
82
+ )
83
+ throw new TypeError("Inconsistent usage charge");
84
+ }
85
+ }
86
+ if (report["next_cursor"] !== null && report["next_cursor"] !== previous)
87
+ throw new TypeError("Invalid usage cursor");
88
+ }
89
+
90
+ export interface PublishedCreditRateCard {
91
+ readonly id: string;
92
+ readonly currency: "USD";
93
+ readonly published_at: string;
94
+ readonly effective_from: string;
95
+ readonly rates: readonly {
96
+ readonly meter: string;
97
+ readonly unit: string;
98
+ readonly units_per_charge: string;
99
+ readonly provider_cost_micros: string;
100
+ readonly credit_micros: string;
101
+ }[];
102
+ }
103
+
104
+ /** Shared wire validation only; clients never recompute accounting decisions. */
105
+ export function assertPublishedCreditRateCards(
106
+ value: unknown,
107
+ ): asserts value is readonly PublishedCreditRateCard[] {
108
+ if (!Array.isArray(value) || value.length > 4) throw new TypeError("Invalid published prices");
109
+ const ids = new Set<string>();
110
+ for (const input of value) {
111
+ if (
112
+ !input ||
113
+ typeof input !== "object" ||
114
+ Array.isArray(input) ||
115
+ Object.keys(input).sort().join() !== "currency,effective_from,id,published_at,rates"
116
+ )
117
+ throw new TypeError("Invalid published price fields");
118
+ const card = input as Record<string, unknown>;
119
+ if (
120
+ typeof card["id"] !== "string" ||
121
+ !/^[a-z0-9_.-]{1,128}$/u.test(card["id"]) ||
122
+ ids.has(card["id"]) ||
123
+ card["currency"] !== "USD"
124
+ )
125
+ throw new TypeError("Invalid price identity");
126
+ ids.add(card["id"]);
127
+ for (const key of ["published_at", "effective_from"]) {
128
+ const date = card[key];
129
+ if (
130
+ typeof date !== "string" ||
131
+ !Number.isFinite(Date.parse(date)) ||
132
+ new Date(date).toISOString() !== date
133
+ )
134
+ throw new TypeError("Invalid price dates");
135
+ }
136
+ if (
137
+ String(card["published_at"]) > String(card["effective_from"]) ||
138
+ !Array.isArray(card["rates"]) ||
139
+ card["rates"].length < 1 ||
140
+ card["rates"].length > 32
141
+ )
142
+ throw new TypeError("Invalid price publication");
143
+ const meters = new Set<string>();
144
+ for (const item of card["rates"]) {
145
+ if (
146
+ !item ||
147
+ typeof item !== "object" ||
148
+ Array.isArray(item) ||
149
+ Object.keys(item).sort().join() !==
150
+ "credit_micros,meter,provider_cost_micros,unit,units_per_charge"
151
+ )
152
+ throw new TypeError("Invalid rate fields");
153
+ const rate = item as Record<string, unknown>;
154
+ for (const key of ["meter", "unit"])
155
+ if (typeof rate[key] !== "string" || !/^[a-z0-9_.-]{1,128}$/u.test(String(rate[key])))
156
+ throw new TypeError("Invalid rate unit");
157
+ const identity = String(rate["meter"]) + "/" + String(rate["unit"]);
158
+ if (meters.has(identity)) throw new TypeError("Duplicate rate");
159
+ meters.add(identity);
160
+ for (const key of ["units_per_charge", "provider_cost_micros", "credit_micros"]) {
161
+ const amount = rate[key];
162
+ if (
163
+ typeof amount !== "string" ||
164
+ !/^(0|[1-9][0-9]{0,18})$/u.test(amount) ||
165
+ BigInt(amount) > 9223372036854775807n ||
166
+ (key === "units_per_charge" && amount === "0")
167
+ )
168
+ throw new TypeError("Invalid rate amount");
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ /** Effective feature access and credit buckets from the existing ledger. */
175
+ export function assertOrganizationAccount(value: unknown): void {
176
+ if (
177
+ !value ||
178
+ typeof value !== "object" ||
179
+ Array.isArray(value) ||
180
+ Object.keys(value).sort().join(",") !==
181
+ "as_of,available_micros,monthly_micros,name,next_expiry,one_time_micros,organization_id,paid_until,plan,plan_source,reserved_micros,workos_organization_id"
182
+ )
183
+ throw new TypeError("Invalid account fields");
184
+ const v = value as Record<string, unknown>;
185
+ if (
186
+ typeof v["organization_id"] !== "string" ||
187
+ !/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(v["organization_id"]) ||
188
+ typeof v["name"] !== "string" ||
189
+ v["name"].length > 128 ||
190
+ typeof v["workos_organization_id"] !== "string" ||
191
+ !/^org_[A-Za-z0-9_]+$/u.test(v["workos_organization_id"]) ||
192
+ (v["plan"] !== "paid" && v["plan"] !== "free") ||
193
+ !["free", "stripe", "beta", "manual"].includes(String(v["plan_source"])) ||
194
+ (v["plan"] === "free") !== (v["plan_source"] === "free")
195
+ )
196
+ throw new TypeError("Invalid account scope");
197
+ for (const field of ["available_micros", "monthly_micros", "one_time_micros", "reserved_micros"])
198
+ if (typeof v[field] !== "string" || !/^(0|[1-9][0-9]{0,18})$/u.test(v[field] as string))
199
+ throw new TypeError("Invalid account credits");
200
+ if (
201
+ BigInt(String(v["monthly_micros"])) + BigInt(String(v["one_time_micros"])) !==
202
+ BigInt(String(v["available_micros"]))
203
+ )
204
+ throw new TypeError("Account credit buckets do not balance");
205
+ for (const field of ["as_of", "paid_until", "next_expiry"]) {
206
+ if (field !== "as_of" && v[field] === null) continue;
207
+ if (
208
+ typeof v[field] !== "string" ||
209
+ !Number.isFinite(Date.parse(v[field] as string)) ||
210
+ new Date(v[field] as string).toISOString() !== v[field]
211
+ )
212
+ throw new TypeError("Invalid account timestamp");
213
+ }
214
+ }
@@ -0,0 +1,257 @@
1
+ import { publicOperationFailure, type PublicOperationFailure } from "./operation-failure.js";
2
+
3
+ export type DatabaseEnvironment = "dev" | "prod";
4
+ export type DatabaseJson =
5
+ | null
6
+ | string
7
+ | number
8
+ | boolean
9
+ | readonly DatabaseJson[]
10
+ | { readonly [key: string]: DatabaseJson };
11
+ export interface ProjectDatabaseWriteRequest {
12
+ readonly environment: DatabaseEnvironment;
13
+ readonly statement: string;
14
+ readonly parameters: readonly DatabaseJson[];
15
+ }
16
+ export interface ProjectDatabaseWriteReceipt {
17
+ readonly operation_id: string;
18
+ readonly environment: DatabaseEnvironment;
19
+ readonly state: "running" | "succeeded" | "failed";
20
+ readonly command: "INSERT" | "UPDATE" | "DELETE" | null;
21
+ readonly affected_rows: number | null;
22
+ readonly error: PublicOperationFailure | null;
23
+ }
24
+ /** These limits apply to one direct DML statement; PostgreSQL owns SQL grammar. */
25
+ export function parseProjectDatabaseWrite(value: unknown): ProjectDatabaseWriteRequest {
26
+ if (
27
+ !record(value) ||
28
+ !exact(value, ["environment", "statement", "parameters"]) ||
29
+ !["dev", "prod"].includes(String(value["environment"])) ||
30
+ typeof value["statement"] !== "string" ||
31
+ !/^\s*(?:insert|update|delete)\b/iu.test(value["statement"]) ||
32
+ new TextEncoder().encode(value["statement"]).byteLength > 65_536 ||
33
+ value["statement"].includes("\0") ||
34
+ !Array.isArray(value["parameters"]) ||
35
+ value["parameters"].length > 100
36
+ )
37
+ throw new TypeError("Invalid database write");
38
+ const budget = { nodes: 0 };
39
+ for (const parameter of value["parameters"]) json(parameter, 0, budget);
40
+ if (new TextEncoder().encode(JSON.stringify(value["parameters"])).byteLength > 65_536)
41
+ throw new TypeError("Invalid database parameters");
42
+ return {
43
+ environment: value["environment"] as DatabaseEnvironment,
44
+ statement: value["statement"],
45
+ parameters: value["parameters"] as DatabaseJson[],
46
+ };
47
+ }
48
+ export function assertProjectDatabaseWriteReceipt(
49
+ value: unknown,
50
+ ): asserts value is ProjectDatabaseWriteReceipt {
51
+ if (
52
+ !record(value) ||
53
+ !exact(value, ["operation_id", "environment", "state", "command", "affected_rows", "error"]) ||
54
+ typeof value["operation_id"] !== "string" ||
55
+ !/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(value["operation_id"]) ||
56
+ !["dev", "prod"].includes(String(value["environment"])) ||
57
+ !["running", "succeeded", "failed"].includes(String(value["state"]))
58
+ )
59
+ throw new TypeError("Invalid database write receipt");
60
+ if (value["state"] === "succeeded") {
61
+ if (
62
+ !["INSERT", "UPDATE", "DELETE"].includes(String(value["command"])) ||
63
+ !Number.isSafeInteger(value["affected_rows"]) ||
64
+ Number(value["affected_rows"]) < 0 ||
65
+ Number(value["affected_rows"]) > 1000 ||
66
+ value["error"] !== null
67
+ )
68
+ throw new TypeError("Invalid database write result");
69
+ } else {
70
+ if (value["command"] !== null || value["affected_rows"] !== null)
71
+ throw new TypeError("Invalid database write result");
72
+ if (value["state"] === "running") {
73
+ if (value["error"] !== null) throw new TypeError("Invalid database write result");
74
+ } else {
75
+ const error = value["error"];
76
+ if (
77
+ !record(error) ||
78
+ typeof error["code"] !== "string" ||
79
+ !error["code"].startsWith("database_write_") ||
80
+ JSON.stringify(error) !== JSON.stringify(publicOperationFailure(error["code"]))
81
+ )
82
+ throw new TypeError("Invalid database write error");
83
+ }
84
+ }
85
+ }
86
+ export type DatabaseAccessMode = "read" | "write";
87
+ export const DATABASE_ACCESS_MIN_TTL_SECONDS = 300;
88
+ export const DATABASE_ACCESS_DEFAULT_TTL_SECONDS = 3600;
89
+ export const DATABASE_ACCESS_MAX_TTL_SECONDS = 86_400;
90
+ export interface ProjectDatabaseAccessRequest {
91
+ readonly environment: DatabaseEnvironment;
92
+ readonly mode: DatabaseAccessMode;
93
+ readonly ttl_seconds: number;
94
+ readonly label: string | null;
95
+ }
96
+ export interface ProjectDatabaseAccess {
97
+ readonly access_id: string;
98
+ readonly environment: DatabaseEnvironment;
99
+ readonly mode: DatabaseAccessMode;
100
+ readonly role_name: string;
101
+ readonly host: string;
102
+ readonly database: "neondb";
103
+ readonly label: string | null;
104
+ readonly state: "active" | "expired" | "revoked";
105
+ readonly issued_at: string;
106
+ readonly expires_at: string;
107
+ readonly revoked_at: string | null;
108
+ readonly revocation_reason: "principal" | "expired" | "budget" | "creation_failed" | null;
109
+ }
110
+ export interface ProjectDatabaseAccessCredential extends ProjectDatabaseAccess {
111
+ readonly connection_uri: string;
112
+ readonly psql_command: string;
113
+ }
114
+ export interface ProjectDatabaseAccessPage {
115
+ readonly items: readonly ProjectDatabaseAccess[];
116
+ }
117
+ const ACCESS_KEYS = [
118
+ "access_id",
119
+ "environment",
120
+ "mode",
121
+ "role_name",
122
+ "host",
123
+ "database",
124
+ "label",
125
+ "state",
126
+ "issued_at",
127
+ "expires_at",
128
+ "revoked_at",
129
+ "revocation_reason",
130
+ ] as const;
131
+ const ACCESS_ROLE_PATTERN = /^ohmyho_da_[0-7][0-9a-hjkmnp-tv-z]{25}_[0-9a-z]{8}$/u;
132
+ /** The customer chooses only the environment, the rights, the lifetime and a label. */
133
+ export function parseProjectDatabaseAccessRequest(value: unknown): ProjectDatabaseAccessRequest {
134
+ if (
135
+ !record(value) ||
136
+ Object.keys(value).some(
137
+ (key) => !["environment", "mode", "ttl_seconds", "label"].includes(key),
138
+ ) ||
139
+ !["dev", "prod"].includes(String(value["environment"])) ||
140
+ (value["mode"] !== undefined && !["read", "write"].includes(String(value["mode"])))
141
+ )
142
+ throw new TypeError("Invalid database access request");
143
+ const ttl = value["ttl_seconds"] ?? DATABASE_ACCESS_DEFAULT_TTL_SECONDS;
144
+ if (
145
+ !Number.isSafeInteger(ttl) ||
146
+ Number(ttl) < DATABASE_ACCESS_MIN_TTL_SECONDS ||
147
+ Number(ttl) > DATABASE_ACCESS_MAX_TTL_SECONDS
148
+ )
149
+ throw new TypeError("Invalid database access lifetime");
150
+ return {
151
+ environment: value["environment"] as DatabaseEnvironment,
152
+ mode: (value["mode"] ?? "read") as DatabaseAccessMode,
153
+ ttl_seconds: Number(ttl),
154
+ label: accessLabel(value["label"]),
155
+ };
156
+ }
157
+ export function assertProjectDatabaseAccess(
158
+ value: unknown,
159
+ ): asserts value is ProjectDatabaseAccess {
160
+ if (!record(value) || !exact(value, ACCESS_KEYS)) throw new TypeError("Invalid database access");
161
+ assertAccessFields(value);
162
+ }
163
+ export function assertProjectDatabaseAccessCredential(
164
+ value: unknown,
165
+ ): asserts value is ProjectDatabaseAccessCredential {
166
+ if (!record(value) || !exact(value, [...ACCESS_KEYS, "connection_uri", "psql_command"]))
167
+ throw new TypeError("Invalid database access credential");
168
+ assertAccessFields(value);
169
+ const uri = value["connection_uri"];
170
+ if (typeof uri !== "string" || value["psql_command"] !== `psql '${uri}'`)
171
+ throw new TypeError("Invalid database access credential");
172
+ let parsed: URL;
173
+ try {
174
+ parsed = new URL(uri);
175
+ } catch {
176
+ throw new TypeError("Invalid database access credential");
177
+ }
178
+ if (
179
+ parsed.protocol !== "postgresql:" ||
180
+ decodeURIComponent(parsed.username) !== value["role_name"] ||
181
+ parsed.password === "" ||
182
+ parsed.hostname !== value["host"] ||
183
+ parsed.host !== value["host"] ||
184
+ decodeURIComponent(parsed.pathname.slice(1)) !== "neondb" ||
185
+ parsed.search !== "?sslmode=require"
186
+ )
187
+ throw new TypeError("Invalid database access credential");
188
+ }
189
+ export function assertProjectDatabaseAccessPage(
190
+ value: unknown,
191
+ ): asserts value is ProjectDatabaseAccessPage {
192
+ if (
193
+ !record(value) ||
194
+ !exact(value, ["items"]) ||
195
+ !Array.isArray(value["items"]) ||
196
+ value["items"].length > 50
197
+ )
198
+ throw new TypeError("Invalid database access page");
199
+ for (const item of value["items"]) assertProjectDatabaseAccess(item);
200
+ }
201
+ function assertAccessFields(value: Record<string, unknown>): void {
202
+ const revoked = value["revoked_at"],
203
+ reason = value["revocation_reason"];
204
+ if (
205
+ typeof value["access_id"] !== "string" ||
206
+ !/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(value["access_id"]) ||
207
+ !["dev", "prod"].includes(String(value["environment"])) ||
208
+ !["read", "write"].includes(String(value["mode"])) ||
209
+ typeof value["role_name"] !== "string" ||
210
+ !ACCESS_ROLE_PATTERN.test(value["role_name"]) ||
211
+ typeof value["host"] !== "string" ||
212
+ !/^[a-z0-9]([a-z0-9.-]{0,251}[a-z0-9])?$/u.test(value["host"]) ||
213
+ value["database"] !== "neondb" ||
214
+ (value["label"] !== null && accessLabel(value["label"]) !== value["label"]) ||
215
+ !["active", "expired", "revoked"].includes(String(value["state"])) ||
216
+ !timestamp(value["issued_at"]) ||
217
+ !timestamp(value["expires_at"]) ||
218
+ Date.parse(String(value["expires_at"])) <= Date.parse(String(value["issued_at"])) ||
219
+ (revoked !== null && !timestamp(revoked)) ||
220
+ (revoked === null) !== (reason === null) ||
221
+ (reason !== null &&
222
+ !["principal", "expired", "budget", "creation_failed"].includes(String(reason))) ||
223
+ (value["state"] === "revoked") !== (revoked !== null)
224
+ )
225
+ throw new TypeError("Invalid database access");
226
+ }
227
+ function accessLabel(value: unknown): string | null {
228
+ if (value === undefined || value === null) return null;
229
+ if (typeof value !== "string") throw new TypeError("Invalid database access label");
230
+ const label = value.trim();
231
+ if (label.length < 1 || label.length > 64 || /\p{Cc}/u.test(label))
232
+ throw new TypeError("Invalid database access label");
233
+ return label;
234
+ }
235
+ function timestamp(value: unknown): boolean {
236
+ return typeof value === "string" && new Date(value).toISOString() === value;
237
+ }
238
+ function json(value: unknown, depth: number, budget: { nodes: number }): void {
239
+ if (++budget.nodes > 10_000 || depth > 8) throw new TypeError("Invalid database parameter");
240
+ if (value === null || typeof value === "string" || typeof value === "boolean") return;
241
+ if (typeof value === "number" && Number.isFinite(value)) return;
242
+ if (Array.isArray(value)) {
243
+ for (const entry of value) json(entry, depth + 1, budget);
244
+ return;
245
+ }
246
+ if (record(value)) {
247
+ for (const entry of Object.values(value)) json(entry, depth + 1, budget);
248
+ return;
249
+ }
250
+ throw new TypeError("Invalid database parameter");
251
+ }
252
+ function record(value: unknown): value is Record<string, unknown> {
253
+ return typeof value === "object" && value !== null && !Array.isArray(value);
254
+ }
255
+ function exact(value: Record<string, unknown>, keys: readonly string[]) {
256
+ return Object.keys(value).sort().join(",") === [...keys].sort().join(",");
257
+ }