@amerged/ohmyhost-runtime 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.
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +11 -0
- package/THIRD_PARTY_NOTICES.md +3 -0
- package/dist/database.d.ts +39 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +334 -0
- package/dist/database.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +32 -0
- package/dist/errors.js.map +1 -0
- package/dist/http.d.ts +21 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +105 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/mail.d.ts +22 -0
- package/dist/mail.d.ts.map +1 -0
- package/dist/mail.js +102 -0
- package/dist/mail.js.map +1 -0
- package/dist/scheduled.d.ts +20 -0
- package/dist/scheduled.d.ts.map +1 -0
- package/dist/scheduled.js +67 -0
- package/dist/scheduled.js.map +1 -0
- package/dist/storage.d.ts +62 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +217 -0
- package/dist/storage.js.map +1 -0
- package/package.json +41 -0
- package/src/packages/customer-runtime/database.ts +413 -0
- package/src/packages/customer-runtime/errors.ts +57 -0
- package/src/packages/customer-runtime/http.ts +135 -0
- package/src/packages/customer-runtime/index.ts +5 -0
- package/src/packages/customer-runtime/mail.ts +161 -0
- package/src/packages/customer-runtime/storage.ts +294 -0
package/dist/storage.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { CustomerRuntimeGatewayError } from "./errors.js";
|
|
2
|
+
import { exactRecord, fetchWithTimeout, invalidResponse, postGatewayJson, validateGatewayConnection, } from "./http.js";
|
|
3
|
+
export function createPrivateStorageClient(configuration) {
|
|
4
|
+
const connection = validateGatewayConnection(configuration);
|
|
5
|
+
if (typeof configuration.capabilityFetch !== "function") {
|
|
6
|
+
throw new CustomerRuntimeGatewayError("gateway_configuration_invalid", null, false);
|
|
7
|
+
}
|
|
8
|
+
const capabilityFetch = configuration.capabilityFetch;
|
|
9
|
+
const projectId = requireUlid(configuration.projectId);
|
|
10
|
+
const environmentId = requireUlid(configuration.environmentId);
|
|
11
|
+
const authenticatedBody = Object.freeze({ projectId, environmentId });
|
|
12
|
+
async function reserveUpload(input) {
|
|
13
|
+
validateStorageMutation(input.idempotencyKey, input.objectKey);
|
|
14
|
+
if (!Number.isSafeInteger(input.contentLength) ||
|
|
15
|
+
input.contentLength < 0 ||
|
|
16
|
+
input.contentLength > 1_073_741_824 ||
|
|
17
|
+
!validContentType(input.contentType)) {
|
|
18
|
+
invalidRequest();
|
|
19
|
+
}
|
|
20
|
+
const response = await postGatewayJson(connection, "/v1/uploads", { ...authenticatedBody, ...input }, 8 * 1024, input.idempotencyKey);
|
|
21
|
+
if (response.status !== 200)
|
|
22
|
+
throwStorageError(response.status, response.body);
|
|
23
|
+
const record = response.body;
|
|
24
|
+
if (record?.["state"] === "completed") {
|
|
25
|
+
const completed = exactRecord(response.body, ["state", "transfer_id"]);
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
state: "completed",
|
|
28
|
+
transferId: requireUlid(completed["transfer_id"]),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const ready = exactRecord(response.body, ["state", "transfer_id", "capability"]);
|
|
32
|
+
if (ready["state"] !== "ready")
|
|
33
|
+
invalidResponse();
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
state: "ready",
|
|
36
|
+
transferId: requireUlid(ready["transfer_id"]),
|
|
37
|
+
capability: parseCapability(ready["capability"], "PUT", input.objectKey),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async function completeUpload(transferId) {
|
|
41
|
+
const response = await postGatewayJson(connection, "/v1/uploads/complete", {
|
|
42
|
+
...authenticatedBody,
|
|
43
|
+
transferId: requireUlid(transferId),
|
|
44
|
+
}, 8 * 1024);
|
|
45
|
+
if (response.status !== 200 && response.status !== 202) {
|
|
46
|
+
throwStorageError(response.status, response.body);
|
|
47
|
+
}
|
|
48
|
+
const record = exactRecord(response.body, ["state", "transfer_id"]);
|
|
49
|
+
if (record["state"] !== "completed" && record["state"] !== "pending")
|
|
50
|
+
invalidResponse();
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
state: record["state"],
|
|
53
|
+
transferId: requireUlid(record["transfer_id"]),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async function createSignedRead(objectKey) {
|
|
57
|
+
validateObjectKey(objectKey);
|
|
58
|
+
const response = await postGatewayJson(connection, "/v1/downloads", {
|
|
59
|
+
...authenticatedBody,
|
|
60
|
+
objectKey,
|
|
61
|
+
}, 8 * 1024);
|
|
62
|
+
if (response.status !== 200)
|
|
63
|
+
throwStorageError(response.status, response.body);
|
|
64
|
+
const record = exactRecord(response.body, ["state", "capability"]);
|
|
65
|
+
if (record["state"] !== "ready")
|
|
66
|
+
invalidResponse();
|
|
67
|
+
return parseCapability(record["capability"], "GET", objectKey);
|
|
68
|
+
}
|
|
69
|
+
async function deleteObject(input) {
|
|
70
|
+
validateStorageMutation(input.idempotencyKey, input.objectKey);
|
|
71
|
+
const response = await postGatewayJson(connection, "/v1/deletions", { ...authenticatedBody, objectKey: input.objectKey }, 8 * 1024, input.idempotencyKey);
|
|
72
|
+
if (response.status !== 200)
|
|
73
|
+
throwStorageError(response.status, response.body);
|
|
74
|
+
const record = exactRecord(response.body, ["state", "deletion_id"]);
|
|
75
|
+
if (record["state"] !== "completed")
|
|
76
|
+
invalidResponse();
|
|
77
|
+
return Object.freeze({
|
|
78
|
+
state: "completed",
|
|
79
|
+
deletionId: requireUlid(record["deletion_id"]),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async function upload(input) {
|
|
83
|
+
const bytes = Uint8Array.from(input.bytes);
|
|
84
|
+
const reservation = await reserveUpload({
|
|
85
|
+
idempotencyKey: input.idempotencyKey,
|
|
86
|
+
objectKey: input.objectKey,
|
|
87
|
+
contentType: input.contentType,
|
|
88
|
+
contentLength: bytes.byteLength,
|
|
89
|
+
});
|
|
90
|
+
if (reservation.state === "completed")
|
|
91
|
+
return reservation;
|
|
92
|
+
const uploaded = await fetchWithTimeout(capabilityFetch, new Request(reservation.capability.url, {
|
|
93
|
+
method: "PUT",
|
|
94
|
+
headers: reservation.capability.requiredHeaders,
|
|
95
|
+
body: bytes,
|
|
96
|
+
}), connection.timeoutMilliseconds);
|
|
97
|
+
if (uploaded.status < 200 || uploaded.status >= 300) {
|
|
98
|
+
throw new CustomerRuntimeGatewayError("storage_upload_rejected", uploaded.status, false);
|
|
99
|
+
}
|
|
100
|
+
return completeUpload(reservation.transferId);
|
|
101
|
+
}
|
|
102
|
+
return Object.freeze({ reserveUpload, completeUpload, createSignedRead, deleteObject, upload });
|
|
103
|
+
}
|
|
104
|
+
function parseCapability(value, operation, objectKey) {
|
|
105
|
+
const record = exactRecord(value, [
|
|
106
|
+
"operation",
|
|
107
|
+
"objectKey",
|
|
108
|
+
"url",
|
|
109
|
+
"expiresAt",
|
|
110
|
+
"expectedContentLength",
|
|
111
|
+
"requiredHeaders",
|
|
112
|
+
]);
|
|
113
|
+
const headers = exactHeaders(record["requiredHeaders"]);
|
|
114
|
+
if (record["operation"] !== operation ||
|
|
115
|
+
record["objectKey"] !== objectKey ||
|
|
116
|
+
typeof record["url"] !== "string" ||
|
|
117
|
+
!validSignedUrl(record["url"]) ||
|
|
118
|
+
typeof record["expiresAt"] !== "string" ||
|
|
119
|
+
!Number.isFinite(Date.parse(record["expiresAt"])) ||
|
|
120
|
+
(record["expectedContentLength"] !== null &&
|
|
121
|
+
(!Number.isSafeInteger(record["expectedContentLength"]) ||
|
|
122
|
+
Number(record["expectedContentLength"]) < 0 ||
|
|
123
|
+
Number(record["expectedContentLength"]) > 1_073_741_824))) {
|
|
124
|
+
invalidResponse();
|
|
125
|
+
}
|
|
126
|
+
return Object.freeze({
|
|
127
|
+
operation,
|
|
128
|
+
objectKey,
|
|
129
|
+
url: record["url"],
|
|
130
|
+
expiresAt: record["expiresAt"],
|
|
131
|
+
expectedContentLength: record["expectedContentLength"] === null ? null : Number(record["expectedContentLength"]),
|
|
132
|
+
requiredHeaders: headers,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function exactHeaders(value) {
|
|
136
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
137
|
+
invalidResponse();
|
|
138
|
+
const entries = Object.entries(value);
|
|
139
|
+
if (entries.length > 16 ||
|
|
140
|
+
entries.some(([name, header]) => !/^[a-z0-9-]{1,64}$/u.test(name) ||
|
|
141
|
+
/^(authorization|cookie|set-cookie)$/u.test(name) ||
|
|
142
|
+
typeof header !== "string" ||
|
|
143
|
+
header.length > 1_024 ||
|
|
144
|
+
/[\r\n]/u.test(header))) {
|
|
145
|
+
invalidResponse();
|
|
146
|
+
}
|
|
147
|
+
return Object.freeze(Object.fromEntries(entries));
|
|
148
|
+
}
|
|
149
|
+
function throwStorageError(status, body) {
|
|
150
|
+
const record = exactRecord(body, typeof body === "object" && body !== null && "transfer_id" in body
|
|
151
|
+
? ["code", "transfer_id"]
|
|
152
|
+
: ["code"]);
|
|
153
|
+
const code = record["code"];
|
|
154
|
+
if (status === 401 && code === "unauthorized") {
|
|
155
|
+
throw new CustomerRuntimeGatewayError("unauthorized", status, false);
|
|
156
|
+
}
|
|
157
|
+
if (status === 404 && code === "object_not_found") {
|
|
158
|
+
throw new CustomerRuntimeGatewayError("storage_object_not_found", status, false);
|
|
159
|
+
}
|
|
160
|
+
if (status === 409 && code === "upload_expired") {
|
|
161
|
+
requireUlid(record["transfer_id"]);
|
|
162
|
+
throw new CustomerRuntimeGatewayError("storage_upload_expired", status, false);
|
|
163
|
+
}
|
|
164
|
+
if (status === 409 && typeof code === "string") {
|
|
165
|
+
throw new CustomerRuntimeGatewayError("storage_conflict", status, false);
|
|
166
|
+
}
|
|
167
|
+
if (status === 413 && typeof code === "string") {
|
|
168
|
+
throw new CustomerRuntimeGatewayError("storage_quota_exceeded", status, false);
|
|
169
|
+
}
|
|
170
|
+
if (status === 503 && code === "storage_gateway_unavailable") {
|
|
171
|
+
throw new CustomerRuntimeGatewayError("gateway_unavailable", status, true);
|
|
172
|
+
}
|
|
173
|
+
return invalidResponse();
|
|
174
|
+
}
|
|
175
|
+
function validateStorageMutation(idempotencyKey, objectKey) {
|
|
176
|
+
if (!/^[A-Za-z0-9._:-]{1,128}$/u.test(idempotencyKey))
|
|
177
|
+
invalidRequest();
|
|
178
|
+
validateObjectKey(objectKey);
|
|
179
|
+
}
|
|
180
|
+
function validateObjectKey(value) {
|
|
181
|
+
const bytes = new TextEncoder().encode(value).byteLength;
|
|
182
|
+
if (bytes < 1 ||
|
|
183
|
+
bytes > 512 ||
|
|
184
|
+
value.startsWith("/") ||
|
|
185
|
+
value.endsWith("/") ||
|
|
186
|
+
value.includes("//") ||
|
|
187
|
+
!value
|
|
188
|
+
.split("/")
|
|
189
|
+
.every((segment) => /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$/u.test(segment))) {
|
|
190
|
+
invalidRequest();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function validContentType(value) {
|
|
194
|
+
return /^[a-z0-9][a-z0-9!#$&^_.+-]{0,63}\/[a-z0-9][a-z0-9!#$&^_.+-]{0,63}$/u.test(value);
|
|
195
|
+
}
|
|
196
|
+
function validSignedUrl(value) {
|
|
197
|
+
try {
|
|
198
|
+
const url = new URL(value);
|
|
199
|
+
return (url.protocol === "https:" &&
|
|
200
|
+
url.username === "" &&
|
|
201
|
+
url.password === "" &&
|
|
202
|
+
url.hash === "" &&
|
|
203
|
+
value.length <= 8_192);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function requireUlid(value) {
|
|
210
|
+
if (typeof value !== "string" || !/^[0-9A-HJKMNP-TV-Z]{26}$/u.test(value))
|
|
211
|
+
invalidResponse();
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
function invalidRequest() {
|
|
215
|
+
throw new CustomerRuntimeGatewayError("gateway_request_invalid", null, false);
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@amerged/ohmyhost-runtime",
|
|
3
|
+
"version": "0.1.10",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src",
|
|
11
|
+
"LICENSE",
|
|
12
|
+
"NOTICE",
|
|
13
|
+
"README.md",
|
|
14
|
+
"THIRD_PARTY_NOTICES.md"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./mail": {
|
|
22
|
+
"types": "./dist/mail.d.ts",
|
|
23
|
+
"import": "./dist/mail.js"
|
|
24
|
+
},
|
|
25
|
+
"./database": {
|
|
26
|
+
"types": "./dist/database.d.ts",
|
|
27
|
+
"import": "./dist/database.js"
|
|
28
|
+
},
|
|
29
|
+
"./storage": {
|
|
30
|
+
"types": "./dist/storage.d.ts",
|
|
31
|
+
"import": "./dist/storage.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"license": "Apache-2.0",
|
|
35
|
+
"homepage": "https://docs.ohmyho.st/",
|
|
36
|
+
"description": "ohmyho.st runtime client",
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"registry": "https://registry.npmjs.org/"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { CustomerDatabaseError, CustomerRuntimeError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
const MAX_QUERY_BYTES = 64 * 1024;
|
|
4
|
+
const MAX_PARAMETERS = 100;
|
|
5
|
+
const MAX_PARAMETER_BYTES = 1024 * 1024;
|
|
6
|
+
const MAX_TRANSACTION_QUERIES = 25;
|
|
7
|
+
const MAX_RESULT_ROWS = 1_000;
|
|
8
|
+
const MAX_RESULT_NODES = 10_000;
|
|
9
|
+
const MAX_RESULT_BYTES = 1024 * 1024;
|
|
10
|
+
const MAX_VALUE_DEPTH = 8;
|
|
11
|
+
|
|
12
|
+
export type CustomerDatabaseJson =
|
|
13
|
+
| string
|
|
14
|
+
| number
|
|
15
|
+
| boolean
|
|
16
|
+
| null
|
|
17
|
+
| readonly CustomerDatabaseJson[]
|
|
18
|
+
| Readonly<{ [key: string]: CustomerDatabaseJson }>;
|
|
19
|
+
|
|
20
|
+
export type CustomerDatabaseParameter = CustomerDatabaseJson | Date | Uint8Array;
|
|
21
|
+
|
|
22
|
+
export interface CustomerDatabaseQuery {
|
|
23
|
+
readonly text: string;
|
|
24
|
+
readonly values: readonly CustomerDatabaseParameter[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CustomerDatabaseQueryResult {
|
|
28
|
+
readonly command: string;
|
|
29
|
+
readonly rows: readonly Readonly<Record<string, unknown>>[];
|
|
30
|
+
readonly rowCount: number | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CustomerDatabaseBinding {
|
|
34
|
+
query(input: unknown): Promise<unknown>;
|
|
35
|
+
transaction(input: unknown): Promise<unknown>;
|
|
36
|
+
open?(): Promise<CustomerDatabaseConnectionStub>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The server-side held connection, reached through a stub. Never handed to customer code raw. */
|
|
40
|
+
export interface CustomerDatabaseConnectionStub {
|
|
41
|
+
query(input: unknown): Promise<unknown>;
|
|
42
|
+
close(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What a callback may do inside a held connection: one statement at a time, including BEGIN. */
|
|
46
|
+
export interface CustomerDatabaseConnection {
|
|
47
|
+
query(input: CustomerDatabaseQuery): Promise<CustomerDatabaseQueryResult>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CustomerDatabaseClient {
|
|
51
|
+
/**
|
|
52
|
+
* Run a read, a decision and a write on one connection. The callback shape is deliberate: the
|
|
53
|
+
* connection is always released, including when the callback throws, so a customer cannot leak
|
|
54
|
+
* one and pin a database connection their whole project shares.
|
|
55
|
+
*/
|
|
56
|
+
withConnection<Result>(
|
|
57
|
+
run: (connection: CustomerDatabaseConnection) => Promise<Result>,
|
|
58
|
+
): Promise<Result>;
|
|
59
|
+
query(input: CustomerDatabaseQuery): Promise<CustomerDatabaseQueryResult>;
|
|
60
|
+
transaction(
|
|
61
|
+
queries: readonly CustomerDatabaseQuery[],
|
|
62
|
+
): Promise<readonly CustomerDatabaseQueryResult[]>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createPrivateDatabaseClient(
|
|
66
|
+
binding: CustomerDatabaseBinding,
|
|
67
|
+
): CustomerDatabaseClient {
|
|
68
|
+
if (
|
|
69
|
+
typeof binding !== "object" ||
|
|
70
|
+
binding === null ||
|
|
71
|
+
typeof binding.query !== "function" ||
|
|
72
|
+
typeof binding.transaction !== "function"
|
|
73
|
+
) {
|
|
74
|
+
throw new CustomerRuntimeError("database_query_invalid", false);
|
|
75
|
+
}
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
async withConnection<Result>(
|
|
78
|
+
run: (connection: CustomerDatabaseConnection) => Promise<Result>,
|
|
79
|
+
): Promise<Result> {
|
|
80
|
+
if (typeof binding.open !== "function") throw unavailable();
|
|
81
|
+
let stub: CustomerDatabaseConnectionStub;
|
|
82
|
+
try {
|
|
83
|
+
stub = await binding.open();
|
|
84
|
+
} catch {
|
|
85
|
+
throw unavailable();
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return await run(
|
|
89
|
+
Object.freeze({
|
|
90
|
+
async query(input: CustomerDatabaseQuery) {
|
|
91
|
+
const parsed = validateQuery(input, undefined, true);
|
|
92
|
+
let raw: unknown;
|
|
93
|
+
try {
|
|
94
|
+
raw = await stub.query(parsed);
|
|
95
|
+
} catch {
|
|
96
|
+
throw unavailable();
|
|
97
|
+
}
|
|
98
|
+
throwStatementFailure(raw);
|
|
99
|
+
return validateResult(raw);
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
} finally {
|
|
104
|
+
// Ending the connection rolls back anything uncommitted and frees the project's slot.
|
|
105
|
+
await stub.close().catch(() => undefined);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
async query(input: CustomerDatabaseQuery) {
|
|
109
|
+
const parsed = validateQuery(input);
|
|
110
|
+
let raw: unknown;
|
|
111
|
+
try {
|
|
112
|
+
raw = await binding.query(parsed);
|
|
113
|
+
} catch {
|
|
114
|
+
throw unavailable();
|
|
115
|
+
}
|
|
116
|
+
throwStatementFailure(raw);
|
|
117
|
+
return validateResult(raw);
|
|
118
|
+
},
|
|
119
|
+
async transaction(queries: readonly CustomerDatabaseQuery[]) {
|
|
120
|
+
const parsed = validateTransaction(queries);
|
|
121
|
+
let raw: unknown;
|
|
122
|
+
try {
|
|
123
|
+
raw = await binding.transaction(parsed);
|
|
124
|
+
} catch {
|
|
125
|
+
throw unavailable();
|
|
126
|
+
}
|
|
127
|
+
throwStatementFailure(raw);
|
|
128
|
+
if (!Array.isArray(raw) || raw.length !== parsed.length) invalidResponse();
|
|
129
|
+
return Object.freeze(raw.map(validateResult));
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function throwStatementFailure(value: unknown): void {
|
|
135
|
+
if (!isRecord(value) || value["type"] !== "ohmyhost.database-error/v1") return;
|
|
136
|
+
const sqlstate = value["sqlstate"];
|
|
137
|
+
if (
|
|
138
|
+
Object.keys(value).sort().join(",") !== "sqlstate,type" ||
|
|
139
|
+
typeof sqlstate !== "string" ||
|
|
140
|
+
!/^[0-9A-Z]{5}$/u.test(sqlstate)
|
|
141
|
+
)
|
|
142
|
+
invalidResponse();
|
|
143
|
+
throw new CustomerDatabaseError(sqlstate);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function validateQuery(
|
|
147
|
+
value: unknown,
|
|
148
|
+
aggregateBudget?: { nodes: number; bytes: number },
|
|
149
|
+
inConnection = false,
|
|
150
|
+
): CustomerDatabaseQuery {
|
|
151
|
+
if (!isRecord(value) || Object.keys(value).sort().join(",") !== "text,values") invalidQuery();
|
|
152
|
+
const text = value["text"];
|
|
153
|
+
const values = value["values"];
|
|
154
|
+
if (
|
|
155
|
+
typeof text !== "string" ||
|
|
156
|
+
text.trim().length === 0 ||
|
|
157
|
+
new TextEncoder().encode(text).byteLength > MAX_QUERY_BYTES ||
|
|
158
|
+
!Array.isArray(values) ||
|
|
159
|
+
values.length > MAX_PARAMETERS
|
|
160
|
+
) {
|
|
161
|
+
invalidQuery();
|
|
162
|
+
}
|
|
163
|
+
validateSql(text, inConnection);
|
|
164
|
+
const budget = aggregateBudget ?? { nodes: 1, bytes: 2 };
|
|
165
|
+
const parsedValues = values.map((parameter, index) => {
|
|
166
|
+
if (index > 0) consume(budget, 1, MAX_PARAMETER_BYTES, invalidQuery);
|
|
167
|
+
return validateStructured(parameter, budget, 0, MAX_PARAMETER_BYTES, invalidQuery);
|
|
168
|
+
});
|
|
169
|
+
return Object.freeze({
|
|
170
|
+
text,
|
|
171
|
+
values: Object.freeze(parsedValues as CustomerDatabaseParameter[]),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function validateTransaction(value: unknown): readonly CustomerDatabaseQuery[] {
|
|
176
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_TRANSACTION_QUERIES) {
|
|
177
|
+
invalidQuery();
|
|
178
|
+
}
|
|
179
|
+
const budget = { nodes: 1, bytes: 2 };
|
|
180
|
+
const queries = value.map((query, index) => {
|
|
181
|
+
if (index > 0) consume(budget, 1, MAX_PARAMETER_BYTES, invalidQuery);
|
|
182
|
+
budget.nodes += 1;
|
|
183
|
+
if (budget.nodes > MAX_RESULT_NODES) invalidQuery();
|
|
184
|
+
consume(budget, 2, MAX_PARAMETER_BYTES, invalidQuery);
|
|
185
|
+
return validateQuery(query, budget);
|
|
186
|
+
});
|
|
187
|
+
return Object.freeze(queries);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function validateSql(text: string, inConnection = false): void {
|
|
191
|
+
let visible = "";
|
|
192
|
+
let index = 0;
|
|
193
|
+
while (index < text.length) {
|
|
194
|
+
const character = text[index] as string;
|
|
195
|
+
const next = text[index + 1];
|
|
196
|
+
if (character === "'") {
|
|
197
|
+
const consumed = quotedEnd(text, index, "'");
|
|
198
|
+
visible += " ".repeat(consumed - index);
|
|
199
|
+
index = consumed;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (character === '"') {
|
|
203
|
+
const consumed = quotedEnd(text, index, '"');
|
|
204
|
+
visible += " ".repeat(consumed - index);
|
|
205
|
+
index = consumed;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (character === "-" && next === "-") {
|
|
209
|
+
const end = text.indexOf("\n", index + 2);
|
|
210
|
+
const consumed = end === -1 ? text.length : end;
|
|
211
|
+
visible += " ".repeat(consumed - index);
|
|
212
|
+
index = consumed;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (character === "/" && next === "*") {
|
|
216
|
+
let depth = 1;
|
|
217
|
+
let cursor = index + 2;
|
|
218
|
+
while (cursor < text.length && depth > 0) {
|
|
219
|
+
if (text.slice(cursor, cursor + 2) === "/*") {
|
|
220
|
+
depth += 1;
|
|
221
|
+
cursor += 2;
|
|
222
|
+
} else if (text.slice(cursor, cursor + 2) === "*/") {
|
|
223
|
+
depth -= 1;
|
|
224
|
+
cursor += 2;
|
|
225
|
+
} else cursor += 1;
|
|
226
|
+
}
|
|
227
|
+
if (depth !== 0) invalidQuery();
|
|
228
|
+
visible += " ".repeat(cursor - index);
|
|
229
|
+
index = cursor;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (character === "$") {
|
|
233
|
+
const match = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/u.exec(text.slice(index));
|
|
234
|
+
if (match !== null) {
|
|
235
|
+
const tag = match[0];
|
|
236
|
+
const end = text.indexOf(tag, index + tag.length);
|
|
237
|
+
if (end === -1) invalidQuery();
|
|
238
|
+
const consumed = end + tag.length;
|
|
239
|
+
visible += " ".repeat(consumed - index);
|
|
240
|
+
index = consumed;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
visible += character;
|
|
245
|
+
index += 1;
|
|
246
|
+
}
|
|
247
|
+
const statements = visible
|
|
248
|
+
.split(";")
|
|
249
|
+
.map((statement) => statement.trim())
|
|
250
|
+
.filter((statement) => statement.length > 0);
|
|
251
|
+
const semicolonCount = [...visible].filter((character) => character === ";").length;
|
|
252
|
+
if (
|
|
253
|
+
statements.length !== 1 ||
|
|
254
|
+
semicolonCount > 1 ||
|
|
255
|
+
(inConnection
|
|
256
|
+
? // Transaction control is the whole point of holding a connection; session-wide state is not.
|
|
257
|
+
/^(?:set|reset|discard|prepare\s+transaction)\b/iu.test(statements[0] as string)
|
|
258
|
+
: /^(?:begin|start(?:\s+transaction)?|commit|end|abort|rollback|savepoint|release|set|reset|discard|prepare\s+transaction)\b/iu.test(
|
|
259
|
+
statements[0] as string,
|
|
260
|
+
))
|
|
261
|
+
) {
|
|
262
|
+
invalidQuery();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function quotedEnd(text: string, start: number, quote: "'" | '"'): number {
|
|
267
|
+
const escapedString =
|
|
268
|
+
quote === "'" &&
|
|
269
|
+
(text[start - 1] === "E" || text[start - 1] === "e") &&
|
|
270
|
+
(start < 2 || !/[A-Za-z0-9_$]/u.test(text[start - 2] as string));
|
|
271
|
+
let index = start + 1;
|
|
272
|
+
while (index < text.length) {
|
|
273
|
+
if (escapedString && text[index] === "\\") {
|
|
274
|
+
index += 2;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (text[index] !== quote) {
|
|
278
|
+
index += 1;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (text[index + 1] === quote) {
|
|
282
|
+
index += 2;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
return index + 1;
|
|
286
|
+
}
|
|
287
|
+
return invalidQuery();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function validateResult(value: unknown): CustomerDatabaseQueryResult {
|
|
291
|
+
if (!isRecord(value) || Object.keys(value).sort().join(",") !== "command,rowCount,rows") {
|
|
292
|
+
return invalidResponse();
|
|
293
|
+
}
|
|
294
|
+
const command = value["command"];
|
|
295
|
+
const rowCount = value["rowCount"];
|
|
296
|
+
const rows = value["rows"];
|
|
297
|
+
if (
|
|
298
|
+
typeof command !== "string" ||
|
|
299
|
+
!/^[A-Z][A-Z ]{0,31}$/u.test(command) ||
|
|
300
|
+
(rowCount !== null && (!Number.isSafeInteger(rowCount) || Number(rowCount) < 0)) ||
|
|
301
|
+
!Array.isArray(rows) ||
|
|
302
|
+
rows.length > MAX_RESULT_ROWS ||
|
|
303
|
+
rows.some((row) => !isRecord(row))
|
|
304
|
+
)
|
|
305
|
+
return invalidResponse();
|
|
306
|
+
const budget = { nodes: 0, bytes: 0 };
|
|
307
|
+
// Count the complete envelope, but start each row's value depth at zero like the server.
|
|
308
|
+
// Rows still share the envelope's byte/node budgets, including their separating commas.
|
|
309
|
+
validateStructured({ command, rows: [], rowCount }, budget, 0, MAX_RESULT_BYTES, invalidResponse);
|
|
310
|
+
const clonedRows: Readonly<Record<string, unknown>>[] = [];
|
|
311
|
+
for (const [index, row] of rows.entries()) {
|
|
312
|
+
if (index > 0) consume(budget, 1, MAX_RESULT_BYTES, invalidResponse);
|
|
313
|
+
const cloned = validateStructured(row, budget, 0, MAX_RESULT_BYTES, invalidResponse);
|
|
314
|
+
if (!isRecord(cloned)) return invalidResponse();
|
|
315
|
+
clonedRows.push(cloned);
|
|
316
|
+
}
|
|
317
|
+
return Object.freeze({
|
|
318
|
+
command,
|
|
319
|
+
rows: Object.freeze(clonedRows),
|
|
320
|
+
rowCount: rowCount === null ? null : Number(rowCount),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function validateStructured(
|
|
325
|
+
value: unknown,
|
|
326
|
+
budget: { nodes: number; bytes: number },
|
|
327
|
+
depth: number,
|
|
328
|
+
maximumBytes: number,
|
|
329
|
+
invalid: () => never,
|
|
330
|
+
): unknown {
|
|
331
|
+
budget.nodes += 1;
|
|
332
|
+
if (budget.nodes > MAX_RESULT_NODES || depth > MAX_VALUE_DEPTH) invalid();
|
|
333
|
+
if (value === null || typeof value === "boolean") {
|
|
334
|
+
consume(budget, value === null ? 4 : value ? 4 : 5, maximumBytes, invalid);
|
|
335
|
+
return value;
|
|
336
|
+
}
|
|
337
|
+
if (typeof value === "number") {
|
|
338
|
+
if (!Number.isFinite(value)) invalid();
|
|
339
|
+
consume(budget, new TextEncoder().encode(String(value)).byteLength, maximumBytes, invalid);
|
|
340
|
+
return value;
|
|
341
|
+
}
|
|
342
|
+
if (typeof value === "string") {
|
|
343
|
+
consume(budget, jsonStringBytes(value), maximumBytes, invalid);
|
|
344
|
+
return value;
|
|
345
|
+
}
|
|
346
|
+
if (value instanceof Date) {
|
|
347
|
+
if (!Number.isFinite(value.getTime())) invalid();
|
|
348
|
+
consume(budget, jsonStringBytes(value.toISOString()), maximumBytes, invalid);
|
|
349
|
+
return new Date(value.getTime());
|
|
350
|
+
}
|
|
351
|
+
if (value instanceof Uint8Array) {
|
|
352
|
+
consume(budget, value.byteLength + 2, maximumBytes, invalid);
|
|
353
|
+
return Uint8Array.from(value);
|
|
354
|
+
}
|
|
355
|
+
if (Array.isArray(value)) {
|
|
356
|
+
consume(budget, 2, maximumBytes, invalid);
|
|
357
|
+
return Object.freeze(
|
|
358
|
+
value.map((item, index) => {
|
|
359
|
+
if (index > 0) consume(budget, 1, maximumBytes, invalid);
|
|
360
|
+
return validateStructured(item, budget, depth + 1, maximumBytes, invalid);
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
if (!isRecord(value)) return invalid();
|
|
365
|
+
consume(budget, 2, maximumBytes, invalid);
|
|
366
|
+
// RPC rejects null-prototype objects. The forbidden keys below keep a normal object safe
|
|
367
|
+
// while allowing JSON parameters to cross a real Worker Service Binding.
|
|
368
|
+
const output: Record<string, unknown> = {};
|
|
369
|
+
for (const [index, [key, item]] of Object.entries(value).entries()) {
|
|
370
|
+
if (
|
|
371
|
+
!/^[A-Za-z_?][A-Za-z0-9_?$ -]{0,127}$/u.test(key) ||
|
|
372
|
+
key === "__proto__" ||
|
|
373
|
+
key === "constructor" ||
|
|
374
|
+
key === "prototype"
|
|
375
|
+
) {
|
|
376
|
+
invalid();
|
|
377
|
+
}
|
|
378
|
+
if (index > 0) consume(budget, 1, maximumBytes, invalid);
|
|
379
|
+
consume(budget, jsonStringBytes(key) + 1, maximumBytes, invalid);
|
|
380
|
+
output[key] = validateStructured(item, budget, depth + 1, maximumBytes, invalid);
|
|
381
|
+
}
|
|
382
|
+
return Object.freeze(output);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function unavailable(): CustomerRuntimeError {
|
|
386
|
+
return new CustomerRuntimeError("database_unavailable", true);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function consume(
|
|
390
|
+
budget: { bytes: number },
|
|
391
|
+
bytes: number,
|
|
392
|
+
maximum: number,
|
|
393
|
+
invalid: () => never,
|
|
394
|
+
): void {
|
|
395
|
+
budget.bytes += bytes;
|
|
396
|
+
if (budget.bytes > maximum) invalid();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function jsonStringBytes(value: string): number {
|
|
400
|
+
return new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function invalidQuery(): never {
|
|
404
|
+
throw new CustomerRuntimeError("database_query_invalid", false);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function invalidResponse(): never {
|
|
408
|
+
throw new CustomerRuntimeError("database_response_invalid", false);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
412
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
413
|
+
}
|