@terrascale/sdk 0.0.7 → 0.0.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.9 — declared JSON Schema models
4
+
5
+ Node tooling at `@terrascale/sdk/codegen` generates document, create and
6
+ merge-patch body types from `collection.getSchema()` or a schema file. Install
7
+ the exact tooling peer `json-schema-to-typescript@16.0.0`; the `terrascale-models`
8
+ command is separate from browser/runtime entrypoints. Unsupported constructs
9
+ produce JSON Pointer diagnostics. Runtime validation remains authoritative.
10
+
3
11
  ## 0.0.7 — live shape reconnect, browser bootstrap and read replica
4
12
 
5
13
  `subscribeShape` keeps its state and retries the same position after a lost
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrascale/sdk",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "JavaScript SDK, typed with JSDoc, for project-scoped C# TerraBase v1 and hosted TerraScale identity.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -70,10 +70,16 @@
70
70
  "workerd": null,
71
71
  "browser": null,
72
72
  "node": "./src/postgres.js"
73
+ },
74
+ "./codegen": {
75
+ "types": "./types/codegen.d.ts",
76
+ "browser": null,
77
+ "node": "./src/codegen.js"
73
78
  }
74
79
  },
75
80
  "bin": {
76
- "terrascale-local": "./src/local/cli.js"
81
+ "terrascale-local": "./src/local/cli.js",
82
+ "terrascale-models": "./src/codegen-cli.js"
77
83
  },
78
84
  "files": [
79
85
  "src",
@@ -83,6 +89,7 @@
83
89
  "!types/integration",
84
90
  "sdk-v1-route-manifest.json",
85
91
  "sdk-public-v1-route-manifest.json",
92
+ "sdk-file-storage-route-manifest.json",
86
93
  "README.md",
87
94
  "CHANGELOG.md",
88
95
  "LICENSE"
@@ -108,7 +115,8 @@
108
115
  "@better-auth/core": "1.7.2",
109
116
  "react": ">=18.2.0 <20.0.0",
110
117
  "pg": "8.23.0",
111
- "@tanstack/db": "0.9.2"
118
+ "@tanstack/db": "0.9.2",
119
+ "json-schema-to-typescript": "16.0.0"
112
120
  },
113
121
  "peerDependenciesMeta": {
114
122
  "@better-auth/core": {
@@ -122,6 +130,9 @@
122
130
  },
123
131
  "@tanstack/db": {
124
132
  "optional": true
133
+ },
134
+ "json-schema-to-typescript": {
135
+ "optional": true
125
136
  }
126
137
  },
127
138
  "devDependencies": {
@@ -139,7 +150,8 @@
139
150
  "@types/react": "19.2.14",
140
151
  "@types/react-test-renderer": "19.1.0",
141
152
  "pg": "8.23.0",
142
- "@types/pg": "8.23.1"
153
+ "@types/pg": "8.23.1",
154
+ "json-schema-to-typescript": "16.0.0"
143
155
  },
144
156
  "engines": {
145
157
  "node": "22.x || 24.x",
@@ -0,0 +1,146 @@
1
+ {
2
+ "producerRevision": "1c94fe262795ea87f55248989e04f5b93f2f40bf",
3
+ "routes": [
4
+ {
5
+ "operation": "createFileBucket",
6
+ "method": "POST",
7
+ "pathTemplate": "/v1/projects/{project}/buckets",
8
+ "permission": "documents:write",
9
+ "requestMediaTypes": [
10
+ "application/json"
11
+ ],
12
+ "successStatuses": [
13
+ 201
14
+ ],
15
+ "responseMediaType": "application/json",
16
+ "idempotencyKey": "none",
17
+ "replaySafe": false,
18
+ "contract": "docs/file-buckets.md",
19
+ "contractText": []
20
+ },
21
+ {
22
+ "operation": "listFileBuckets",
23
+ "method": "GET",
24
+ "pathTemplate": "/v1/projects/{project}/buckets",
25
+ "permission": "documents:read",
26
+ "requestMediaTypes": [],
27
+ "successStatuses": [
28
+ 200
29
+ ],
30
+ "responseMediaType": "application/json",
31
+ "idempotencyKey": "none",
32
+ "replaySafe": true,
33
+ "contract": "docs/file-buckets.md",
34
+ "contractText": []
35
+ },
36
+ {
37
+ "operation": "setFileBucketRules",
38
+ "method": "PUT",
39
+ "pathTemplate": "/v1/projects/{project}/buckets/{bucket}/rules",
40
+ "permission": "documents:write",
41
+ "requestMediaTypes": [
42
+ "application/json"
43
+ ],
44
+ "successStatuses": [
45
+ 200
46
+ ],
47
+ "responseMediaType": "application/json",
48
+ "idempotencyKey": "none",
49
+ "replaySafe": false,
50
+ "contract": "docs/file-buckets.md",
51
+ "contractText": []
52
+ },
53
+ {
54
+ "operation": "deleteFileBucket",
55
+ "method": "DELETE",
56
+ "pathTemplate": "/v1/projects/{project}/buckets/{bucket}",
57
+ "permission": "documents:write",
58
+ "requestMediaTypes": [],
59
+ "successStatuses": [
60
+ 204
61
+ ],
62
+ "responseMediaType": "",
63
+ "idempotencyKey": "none",
64
+ "replaySafe": false,
65
+ "contract": "docs/file-buckets.md",
66
+ "contractText": []
67
+ },
68
+ {
69
+ "operation": "createFileUpload",
70
+ "method": "POST",
71
+ "pathTemplate": "/v1/projects/{project}/buckets/{bucket}/files/upload",
72
+ "permission": "documents:write",
73
+ "requestMediaTypes": [
74
+ "application/json"
75
+ ],
76
+ "successStatuses": [
77
+ 201
78
+ ],
79
+ "responseMediaType": "application/json",
80
+ "idempotencyKey": "none",
81
+ "replaySafe": false,
82
+ "contract": "docs/file-buckets.md",
83
+ "contractText": []
84
+ },
85
+ {
86
+ "operation": "completeFileUpload",
87
+ "method": "POST",
88
+ "pathTemplate": "/v1/projects/{project}/buckets/{bucket}/files/{file}/complete",
89
+ "permission": "documents:write",
90
+ "requestMediaTypes": [],
91
+ "successStatuses": [
92
+ 200
93
+ ],
94
+ "responseMediaType": "application/json",
95
+ "idempotencyKey": "none",
96
+ "replaySafe": false,
97
+ "contract": "docs/file-buckets.md",
98
+ "contractText": []
99
+ },
100
+ {
101
+ "operation": "listBucketFiles",
102
+ "method": "GET",
103
+ "pathTemplate": "/v1/projects/{project}/buckets/{bucket}/files",
104
+ "permission": "documents:read",
105
+ "requestMediaTypes": [],
106
+ "successStatuses": [
107
+ 200
108
+ ],
109
+ "responseMediaType": "application/json",
110
+ "idempotencyKey": "none",
111
+ "replaySafe": true,
112
+ "contract": "docs/file-buckets.md",
113
+ "contractText": []
114
+ },
115
+ {
116
+ "operation": "createFileDownload",
117
+ "method": "POST",
118
+ "pathTemplate": "/v1/projects/{project}/buckets/{bucket}/files/{file}/download",
119
+ "permission": "documents:read",
120
+ "requestMediaTypes": [],
121
+ "successStatuses": [
122
+ 200
123
+ ],
124
+ "responseMediaType": "application/json",
125
+ "idempotencyKey": "none",
126
+ "replaySafe": false,
127
+ "contract": "docs/file-buckets.md",
128
+ "contractText": []
129
+ },
130
+ {
131
+ "operation": "deleteBucketFile",
132
+ "method": "DELETE",
133
+ "pathTemplate": "/v1/projects/{project}/buckets/{bucket}/files/{file}",
134
+ "permission": "documents:write",
135
+ "requestMediaTypes": [],
136
+ "successStatuses": [
137
+ 204
138
+ ],
139
+ "responseMediaType": "",
140
+ "idempotencyKey": "none",
141
+ "replaySafe": false,
142
+ "contract": "docs/file-buckets.md",
143
+ "contractText": []
144
+ }
145
+ ]
146
+ }
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { generateDocumentModels } from "./codegen.js";
4
+ import { parseExactJson } from "./v1/json.js";
5
+ const [source, name, output, ...extra] = process.argv.slice(2);
6
+ if (!source || !name || !output || extra.length) {
7
+ process.stderr.write("Usage: terrascale-models declared-schema.json ModelName models.ts\nInput is the schema itself; use @terrascale/sdk/codegen to fetch a live declaration.\n");
8
+ process.exitCode = 1;
9
+ } else {
10
+ try {
11
+ const generated = await generateDocumentModels(parseExactJson(await readFile(source, "utf8")), name);
12
+ await writeFile(output, generated, "utf8");
13
+ } catch (error) {
14
+ process.stderr.write(`${error instanceof Error ? error.message : "Model generation failed"}\n`);
15
+ process.exitCode = 1;
16
+ }
17
+ }
package/src/codegen.js ADDED
@@ -0,0 +1,137 @@
1
+ // Node-only tooling. Never imported by the browser SDK entrypoint.
2
+ import { compile } from "json-schema-to-typescript";
3
+ import { isJsonObject, ExactDecimal } from "./v1/json.js";
4
+ /** @import { V1Collection } from "./v1/terrabase-v1.js" */
5
+ /** @typedef {Record<string, unknown>} Schema */
6
+ const dialect = "https://json-schema.org/draft/2020-12/schema";
7
+ const supported = new Set(["$schema", "type", "properties", "required", "additionalProperties", "items", "enum", "const", "readOnly", "title", "description", "default", "examples", "format", "minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf", "minLength", "maxLength", "minItems", "maxItems", "uniqueItems", "minProperties", "maxProperties"]);
8
+ /** @param {string} pointer @param {string} message @returns {never} */
9
+ function diagnostic(pointer, message) { throw new TypeError(`Collection model ${pointer || "/"}: ${message}`); }
10
+ /** @param {string} pointer @param {string} key */
11
+ function at(pointer, key) { return `${pointer}/${key.replaceAll("~", "~0").replaceAll("/", "~1")}`; }
12
+
13
+ /**
14
+ * Generate body models from a declared Draft 2020-12 schema, never inferred samples.
15
+ * Runtime validation remains authoritative; constraints such as ranges and lengths
16
+ * cannot be expressed by TypeScript. Patch is an RFC 7396 merge-patch body.
17
+ * @param {unknown} input
18
+ * @param {string} name A TypeScript identifier prefix.
19
+ * @returns {Promise<string>}
20
+ */
21
+ export async function generateDocumentModels(input, name) {
22
+ if (!/^[A-Z][A-Za-z0-9]*$/u.test(name)) throw new TypeError("Model name must start with A-Z and contain only letters and digits.");
23
+ // Also reject cyclic objects, lossy numbers, and excessive input before compilation.
24
+ let values = 0;
25
+ /** @param {unknown} value @param {number} depth */
26
+ function bounded(value, depth) {
27
+ if (++values > 4093 || depth > 15) diagnostic("", "schema exceeds 4093 values or 15 nesting levels.");
28
+ if (value instanceof ExactDecimal || (typeof value === "number" && (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))))) diagnostic("", "exact numeric literals outside JavaScript's safe range are unsupported.");
29
+ if (Array.isArray(value) || isJsonObject(value)) for (const child of Object.values(value)) bounded(child, depth + 1);
30
+ }
31
+ bounded(input, 0);
32
+ if (Buffer.byteLength(JSON.stringify(input) ?? "") > 65536) diagnostic("", "schema exceeds 64 KiB.");
33
+ if (!isJsonObject(input) || input.type !== "object") diagnostic("/type", "document root must declare type: object.");
34
+ const models = [];
35
+ for (const mode of /** @type {const} */ (["Document", "Create", "Patch"])) {
36
+ const schema = convert(input, "", mode);
37
+ const identifier = `${name}${mode}`;
38
+ const output = await compile(/** @type {import("json-schema-to-typescript").JSONSchema} */ (schema), identifier, {
39
+ bannerComment: "", format: false, unknownAny: true, ignoreMinAndMaxItems: true,
40
+ $refOptions: { resolve: { file: false, http: false } },
41
+ });
42
+ // Object aliases remain assignable to the SDK's JSON object input type.
43
+ models.push(output.replace(`export interface ${identifier} {`, `export type ${identifier} = {`).trim());
44
+ }
45
+ return `// Generated by @terrascale/sdk/codegen (json-schema-to-typescript 16.0.0). Do not edit.\n// Bodies only; id/version are server envelope fields. Runtime validation is authoritative.\n\n${models.join("\n\n")}\n`;
46
+ }
47
+
48
+ /** @param {unknown} input @param {string} pointer @param {"Document" | "Create" | "Patch"} mode @returns {Schema | boolean} */
49
+ function convert(input, pointer, mode) {
50
+ if (input === false) return false;
51
+ const jsonType = `import("@terrascale/sdk").${mode === "Document" ? "V1JsonValue" : "V1JsonInput"}`;
52
+ if (input === true) return { tsType: jsonType };
53
+ if (!isJsonObject(input)) diagnostic(pointer, "expected a schema object or boolean.");
54
+ for (const key of Object.keys(input).sort()) if (!supported.has(key)) diagnostic(at(pointer, key), `unsupported keyword ${key}; inline references or simplify the schema to the documented subset.`);
55
+ for (const key of ["minLength", "maxLength", "minItems", "maxItems", "minProperties", "maxProperties"]) if (input[key] !== undefined && (typeof input[key] !== "number" || !Number.isSafeInteger(input[key]) || input[key] < 0)) diagnostic(at(pointer, key), "expected a nonnegative safe integer.");
56
+ for (const key of ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"]) if (input[key] !== undefined && (typeof input[key] !== "number" || (key === "multipleOf" && input[key] <= 0))) diagnostic(at(pointer, key), "expected a number (positive for multipleOf).");
57
+ for (const key of ["title", "description", "format"]) if (input[key] !== undefined && typeof input[key] !== "string") diagnostic(at(pointer, key), "expected a string annotation.");
58
+ if (input.examples !== undefined && !Array.isArray(input.examples)) diagnostic(at(pointer, "examples"), "expected an array.");
59
+ if (input.uniqueItems !== undefined && typeof input.uniqueItems !== "boolean") diagnostic(at(pointer, "uniqueItems"), "expected a boolean.");
60
+ if (input.$schema !== undefined && input.$schema !== dialect) diagnostic(at(pointer, "$schema"), `only ${dialect} is supported.`);
61
+ const types = Array.isArray(input.type) ? input.type : [input.type];
62
+ const kinds = types.filter(type => type !== "null");
63
+ if (new Set(types).size !== types.length || kinds.length > 1 || types.length === 0 || types.some(type => !["object", "array", "string", "number", "integer", "boolean", "null"].includes(String(type)))) diagnostic(at(pointer, "type"), "declare one type, optionally unioned with null.");
64
+ const kind = kinds[0];
65
+ const nullable = types.includes("null");
66
+ for (const key of ["properties", "required", "additionalProperties"]) if (Object.hasOwn(input, key) && kind !== "object") diagnostic(at(pointer, key), "this keyword is supported only with an object type.");
67
+ if (Object.hasOwn(input, "items") && kind !== "array") diagnostic(at(pointer, "items"), "items is supported only with an array type.");
68
+ if (input.readOnly !== undefined && typeof input.readOnly !== "boolean") diagnostic(at(pointer, "readOnly"), "expected a boolean annotation.");
69
+ /** @type {Schema} */
70
+ const result = { type: input.type };
71
+ if (typeof input.description === "string") result.description = input.description;
72
+ for (const key of ["enum", "const"]) {
73
+ if (!Object.hasOwn(input, key)) continue;
74
+ if (kind === "object" || kind === "array") diagnostic(at(pointer, key), "container literal constraints are unsupported; use type: null for a null-only value.");
75
+ const literals = key === "enum" ? input[key] : [input[key]];
76
+ if (!Array.isArray(literals) || literals.length === 0 || literals.some(value => value !== null && !["string", "number", "boolean"].includes(typeof value))) diagnostic(at(pointer, key), "only nonempty scalar literals are supported.");
77
+ if (literals.some(value => !types.some(type => type === "null" ? value === null : type === "integer" ? typeof value === "number" && Number.isInteger(value) : typeof value === type))) diagnostic(at(pointer, key), "literal/type intersections are unsupported; each literal must satisfy the declared type.");
78
+ result[key] = input[key];
79
+ }
80
+ if (Array.isArray(input.enum) && Object.hasOwn(input, "const") && !input.enum.includes(input.const ?? null)) diagnostic(at(pointer, "const"), "const must belong to enum.");
81
+ if ((kind === "number" || kind === "integer") && result.enum === undefined && result.const === undefined) result.tsType = `number | import("@terrascale/sdk").ExactDecimal${mode === "Document" ? "" : " | bigint"}${nullable ? " | null" : ""}`;
82
+ if (kind === "array") {
83
+ if (input.items === undefined) diagnostic(at(pointer, "items"), "declare the array item schema (or true for arbitrary JSON).");
84
+ result.items = convert(input.items, at(pointer, "items"), mode === "Patch" ? "Create" : mode);
85
+ }
86
+ if (kind === "object") {
87
+ const properties = input.properties === undefined ? {} : input.properties;
88
+ if (!isJsonObject(properties)) diagnostic(at(pointer, "properties"), "expected a property map.");
89
+ const required = input.required === undefined ? [] : input.required;
90
+ if (!Array.isArray(required) || required.some(key => typeof key !== "string" || !Object.hasOwn(properties, key))) diagnostic(at(pointer, "required"), "required must list declared property names.");
91
+ if (new Set(required).size !== required.length) diagnostic(at(pointer, "required"), "required names must be unique.");
92
+ const entries = Object.keys(properties).sort().map(key => {
93
+ const property = properties[key];
94
+ const location = at(at(pointer, "properties"), key);
95
+ // Validate even omitted read-only fields, so unsupported constructs never hide.
96
+ let converted = convert(property, location, mode);
97
+ if (mode !== "Document" && isJsonObject(property) && property.readOnly === true) converted = false;
98
+ else if (mode === "Patch") {
99
+ // null deletes in RFC 7396, including when the stored field permits null.
100
+ if (!required.includes(key)) converted = { anyOf: [converted, { type: "null" }] };
101
+ else if (property === true) converted = { tsType: `Exclude<${jsonType}, null>` };
102
+ else if (isJsonObject(property)) {
103
+ /** @type {Schema & {type: unknown[]}} */
104
+ const nonnull = { ...property, type: (Array.isArray(property.type) ? property.type : [property.type]).filter(t => t !== "null") };
105
+ if (Array.isArray(nonnull.enum)) nonnull.enum = nonnull.enum.filter(value => value !== null);
106
+ converted = nonnull.type.length === 0 || nonnull.const === null || (Array.isArray(nonnull.enum) && nonnull.enum.length === 0) ? false : convert(nonnull, location, mode);
107
+ }
108
+ }
109
+ return [key, converted];
110
+ });
111
+ for (const key of required) if (typeof key === "string" && isJsonObject(properties[key]) && properties[key].readOnly === true) diagnostic(at(at(pointer, "properties"), key), "required readOnly fields cannot be populated by the producer; default is not server generation. Make this field writable or optional.");
112
+ result.properties = Object.fromEntries(entries);
113
+ result.required = mode === "Patch" ? [] : required.filter(key => typeof key === "string" && (mode === "Document" || !isJsonObject(properties[key]) || properties[key].readOnly !== true));
114
+ const additional = input.additionalProperties;
115
+ if (additional === undefined || additional === true) {
116
+ // Separate the JSON index signature so the generator cannot widen it with
117
+ // undefined for optional properties (which SDK JSON inputs prohibit).
118
+ result.additionalProperties = false;
119
+ result.type = "object";
120
+ const object = { allOf: [result, { tsType: `Record<string, ${jsonType}>` }] };
121
+ return nullable ? { anyOf: [object, { type: "null" }] } : object;
122
+ }
123
+ if (additional !== false && Object.keys(properties).length) diagnostic(at(pointer, "additionalProperties"), "typed additional properties with named properties cannot be represented exactly; use a pure map or a closed object.");
124
+ let extra = convert(additional, at(pointer, "additionalProperties"), mode);
125
+ if (additional !== false && mode === "Patch") extra = { anyOf: [extra, { type: "null" }] };
126
+ result.additionalProperties = extra;
127
+ if (additional === false && entries.length === 0) return { tsType: `Record<string, never>${nullable ? " | null" : ""}` };
128
+ }
129
+ return result;
130
+ }
131
+
132
+ /** Fetch the authorized declared schema using the public SDK operation. @param {V1Collection} collection @param {string} name @param {import("./v1/terrabase-v1.js").V1RequestOptions} [options] */
133
+ export async function generateCollectionModels(collection, name, options = {}) {
134
+ const { schema } = await collection.getSchema(options);
135
+ if (schema === null) diagnostic("", "collection has no declared JSON Schema; attach one with collection.putSchema(). Inference is not a declaration.");
136
+ return generateDocumentModels(schema, name);
137
+ }
package/src/index.js CHANGED
@@ -161,3 +161,13 @@ export { createTerraBaseSystemClient, V1Health, V1Platform } from "./v1/system.j
161
161
  /** @typedef {import("./v1/shape-manifest.js").V1ShapeManifestRequest} V1ShapeManifestRequest */
162
162
  /** @typedef {import("./v1/shape-reader.js").V1ShapeKeyRequest} V1ShapeKeyRequest */
163
163
  /** @typedef {import("./v1/shape-reader.js").V1ShapeKey} V1ShapeKey */
164
+
165
+ export { V1FileStorage, V1FileBucket } from "./v1/file-storage.js";
166
+ /** @typedef {import("./v1/file-storage.js").FileAccessPolicy} FileAccessPolicy */
167
+ /** @typedef {import("./v1/file-storage.js").FileAccessRules} FileAccessRules */
168
+ /** @typedef {import("./v1/file-storage.js").FileBucketInfo} FileBucketInfo */
169
+ /** @typedef {import("./v1/file-storage.js").StoredFile} StoredFile */
170
+ /** @typedef {import("./v1/file-storage.js").SignedFileUrl} SignedFileUrl */
171
+ /** @typedef {import("./v1/file-storage.js").FileUploadRequest} FileUploadRequest */
172
+ /** @typedef {import("./v1/file-storage.js").FileUploadOptions} FileUploadOptions */
173
+ /** @typedef {import("./v1/file-storage.js").FileListOptions} FileListOptions */
@@ -0,0 +1,17 @@
1
+ /** @typedef {"createFileBucket" | "listFileBuckets" | "setFileBucketRules" | "deleteFileBucket" | "createFileUpload" | "completeFileUpload" | "listBucketFiles" | "createFileDownload" | "deleteBucketFile"} FileStorageOperation */
2
+ const buckets = "/v1/projects/{project}/buckets";
3
+ const bucket = `${buckets}/{bucket}`;
4
+ const files = `${bucket}/files`;
5
+ const file = `${files}/{file}`;
6
+ /** @type {readonly import("./routes.js").V1Route[]} */
7
+ export const fileStorageRoutes = Object.freeze(/** @type {import("./routes.js").V1Route[]} */ ([
8
+ { operation: "createFileBucket", method: "POST", pathTemplate: buckets, permission: "documents:write", requestMediaTypes: ["application/json"], successStatuses: [201] },
9
+ { operation: "listFileBuckets", method: "GET", pathTemplate: buckets, permission: "documents:read", requestMediaTypes: [], successStatuses: [200] },
10
+ { operation: "setFileBucketRules", method: "PUT", pathTemplate: `${bucket}/rules`, permission: "documents:write", requestMediaTypes: ["application/json"], successStatuses: [200] },
11
+ { operation: "deleteFileBucket", method: "DELETE", pathTemplate: bucket, permission: "documents:write", requestMediaTypes: [], successStatuses: [204] },
12
+ { operation: "createFileUpload", method: "POST", pathTemplate: `${files}/upload`, permission: "documents:write", requestMediaTypes: ["application/json"], successStatuses: [201] },
13
+ { operation: "completeFileUpload", method: "POST", pathTemplate: `${file}/complete`, permission: "documents:write", requestMediaTypes: [], successStatuses: [200] },
14
+ { operation: "listBucketFiles", method: "GET", pathTemplate: files, permission: "documents:read", requestMediaTypes: [], successStatuses: [200] },
15
+ { operation: "createFileDownload", method: "POST", pathTemplate: `${file}/download`, permission: "documents:read", requestMediaTypes: [], successStatuses: [200] },
16
+ { operation: "deleteBucketFile", method: "DELETE", pathTemplate: file, permission: "documents:write", requestMediaTypes: [], successStatuses: [204] },
17
+ ].map(route => Object.freeze({ ...route, responseMediaType: route.successStatuses.includes(204) ? "" : "application/json", idempotencyKey: "none", replaySafe: route.method === "GET", contract: "docs/file-buckets.md", contractText: [] }))));
@@ -0,0 +1,163 @@
1
+ import { array, boolean, malformed, object, string } from "./decode.js";
2
+ import { requireId } from "../ids.js";
3
+ import { assertJsonObject, stringifyExactJson } from "./json.js";
4
+ import { requireServiceUrl } from "./transport.js";
5
+ /** @import { V1Transport } from "./transport.js" */
6
+ /** @import { FetchLike } from "../config.js" */
7
+ /** @import { JsonObject, JsonValue } from "./json.js" */
8
+ /** @typedef {{ owner: boolean, public: boolean, roles: readonly string[] }} FileAccessPolicy */
9
+ /** @typedef {{ owner: string | null, read: FileAccessPolicy, write: FileAccessPolicy }} FileAccessRules */
10
+ /** @typedef {{ id: string, name: string, region: string, rules: FileAccessRules }} FileBucketInfo */
11
+ /** @typedef {{ id: string, name: string, contentType: string, size: number, sha256: string, metadata: JsonObject, status: string, uploadExpiresAt: string }} StoredFile */
12
+ /** @typedef {{ id: string, url: string, expiresAt: string, headers: Readonly<Record<string, string>> }} SignedFileUrl */
13
+ /** @typedef {{ name: string, contentType: string, size: number, sha256: string, metadata: JsonObject }} FileUploadRequest */
14
+ /** @typedef {{ signal?: AbortSignal | undefined }} FileRequestOptions */
15
+ /** @typedef {{ after?: string | undefined, signal?: AbortSignal | undefined }} FileListOptions */
16
+ /** @typedef {{ metadata: JsonObject, contentType?: string | undefined, signal?: AbortSignal | undefined }} FileUploadOptions */
17
+ const maxFileBytes = 100 * 1024 * 1024;
18
+
19
+ /** @param {JsonValue | undefined} value @returns {FileAccessPolicy} */
20
+ function policy(value) {
21
+ const v = object(value, "file access policy");
22
+ return { owner: boolean(v.owner, "owner grant"), public: boolean(v.public, "public grant"), roles: array(v.roles, "roles").map(v => string(v, "role")) };
23
+ }
24
+ /** @param {JsonValue | undefined} value @returns {FileAccessRules} */
25
+ function rules(value) {
26
+ const v = object(value, "file access rules");
27
+ return { owner: v.owner === null ? null : string(v.owner, "owner pointer"), read: policy(v.read), write: policy(v.write) };
28
+ }
29
+ /** @param {JsonValue | undefined} value @returns {FileBucketInfo} */
30
+ function bucketInfo(value) {
31
+ const v = object(value, "file bucket");
32
+ return { id: requireId(v.id), name: string(v.name, "bucket name"), region: string(v.region, "bucket region"), rules: rules(v.rules) };
33
+ }
34
+ /** @param {JsonValue | undefined} value @returns {StoredFile} */
35
+ function storedFile(value) {
36
+ const v = object(value, "file");
37
+ if (typeof v.size !== "number" || !Number.isSafeInteger(v.size) || v.size < 1 || v.size > maxFileBytes) malformed("file size");
38
+ return { id: requireId(v.id), name: string(v.name, "file name"), contentType: string(v.contentType, "content type"), size: v.size,
39
+ sha256: string(v.sha256, "checksum"), metadata: object(v.metadata, "file metadata"), status: string(v.status, "file status"), uploadExpiresAt: string(v.uploadExpiresAt, "upload expiry") };
40
+ }
41
+ /** @param {JsonValue | undefined} value @returns {SignedFileUrl} */
42
+ function signed(value) {
43
+ const v = object(value, "signed file URL");
44
+ const url = string(v.url, "signed URL");
45
+ requireServiceUrl(url, "Signed file URL");
46
+ /** @type {Record<string, string>} */
47
+ const headers = {};
48
+ for (const [key, val] of Object.entries(object(v.headers, "signed headers"))) {
49
+ // Object-store capabilities must never carry the project's authorization or cookies.
50
+ if (!["content-type", "content-length", "x-amz-checksum-sha256"].includes(key.toLowerCase())) malformed("signed upload header");
51
+ headers[key] = string(val, "signed header value");
52
+ }
53
+ return { id: requireId(v.id), url, expiresAt: string(v.expiresAt, "signed URL expiry"), headers };
54
+ }
55
+ /** @param {FileListOptions} options @returns {URLSearchParams} */
56
+ function pageQuery(options) {
57
+ const query = new URLSearchParams();
58
+ if (options.after !== undefined) query.set("after", requireId(options.after));
59
+ return query;
60
+ }
61
+
62
+ /** Project file buckets, authenticated by the same token as documents. */
63
+ export class V1FileStorage {
64
+ /** @type {V1Transport} */ #transport;
65
+ /** @type {FetchLike} */ #fetch;
66
+ /** @param {V1Transport} transport @param {FetchLike} fetcher */
67
+ constructor(transport, fetcher) { this.#transport = transport; this.#fetch = fetcher; Object.freeze(this); }
68
+ /** @param {string} name @param {FileAccessRules} accessRules @param {FileRequestOptions} [options] @returns {Promise<FileBucketInfo>} */
69
+ async createBucket(name, accessRules, options = {}) {
70
+ return bucketInfo((await this.#transport.call("createFileBucket", {}, { body: stringifyExactJson({ name, rules: accessRules }, "file bucket"), contentType: "application/json", signal: options.signal })).value);
71
+ }
72
+ /** @param {FileListOptions} [options] @returns {Promise<{items: FileBucketInfo[], next: string | null}>} */
73
+ async listBuckets(options = {}) {
74
+ const v = object((await this.#transport.call("listFileBuckets", {}, { query: pageQuery(options), signal: options.signal })).value, "bucket page");
75
+ return { items: array(v.items, "bucket items").map(bucketInfo), next: v.next === null ? null : requireId(v.next) };
76
+ }
77
+ /** @param {string} id @returns {V1FileBucket} */
78
+ bucket(id) { return new V1FileBucket(this.#transport, this.#fetch, requireId(id)); }
79
+ }
80
+
81
+ /** Immutable files in one project bucket. */
82
+ export class V1FileBucket {
83
+ /** @type {V1Transport} */ #transport;
84
+ /** @type {FetchLike} */ #fetch;
85
+ /** @type {string} */ #bucket;
86
+ /** @param {V1Transport} transport @param {FetchLike} fetcher @param {string} bucket */
87
+ constructor(transport, fetcher, bucket) { this.#transport = transport; this.#fetch = fetcher; this.#bucket = requireId(bucket); Object.freeze(this); }
88
+ /** @param {FileAccessRules} accessRules @param {FileRequestOptions} [options] @returns {Promise<FileBucketInfo>} */
89
+ async setRules(accessRules, options = {}) {
90
+ return bucketInfo((await this.#transport.call("setFileBucketRules", { bucket: this.#bucket }, { body: stringifyExactJson({ rules: accessRules }, "file bucket rules"), contentType: "application/json", signal: options.signal })).value);
91
+ }
92
+ /** @param {FileRequestOptions} [options] @returns {Promise<void>} */
93
+ async delete(options = {}) { await this.#transport.call("deleteFileBucket", { bucket: this.#bucket }, { signal: options.signal }); }
94
+ /** @param {FileUploadRequest} request @param {FileRequestOptions} [options] @returns {Promise<SignedFileUrl>} */
95
+ async createUpload(request, options = {}) {
96
+ assertJsonObject(request.metadata, "file metadata");
97
+ if (!Number.isSafeInteger(request.size) || request.size < 1 || request.size > maxFileBytes) throw new TypeError("Files must contain 1–104857600 bytes.");
98
+ return signed((await this.#transport.call("createFileUpload", { bucket: this.#bucket }, { body: stringifyExactJson(request, "file upload"), contentType: "application/json", signal: options.signal })).value);
99
+ }
100
+ /** @param {string} id @param {FileRequestOptions} [options] @returns {Promise<StoredFile>} */
101
+ async completeUpload(id, options = {}) {
102
+ return storedFile((await this.#transport.call("completeFileUpload", { bucket: this.#bucket, file: requireId(id) }, { signal: options.signal })).value);
103
+ }
104
+ /** @param {FileListOptions} [options] @returns {Promise<{items: StoredFile[], next: string | null}>} */
105
+ async list(options = {}) {
106
+ const v = object((await this.#transport.call("listBucketFiles", { bucket: this.#bucket }, { query: pageQuery(options), signal: options.signal })).value, "file page");
107
+ return { items: array(v.items, "file items").map(storedFile), next: v.next === null ? null : requireId(v.next) };
108
+ }
109
+ /** @param {string} id @param {FileRequestOptions} [options] @returns {Promise<SignedFileUrl>} */
110
+ async downloadUrl(id, options = {}) {
111
+ return signed((await this.#transport.call("createFileDownload", { bucket: this.#bucket, file: requireId(id) }, { signal: options.signal })).value);
112
+ }
113
+ /** @param {string} id @param {FileRequestOptions} [options] @returns {Promise<void>} */
114
+ async deleteFile(id, options = {}) { await this.#transport.call("deleteBucketFile", { bucket: this.#bucket, file: requireId(id) }, { signal: options.signal }); }
115
+ /** Hashes a private byte snapshot, uploads it without project credentials, and verifies completion. No retries.
116
+ * @param {string} name @param {Uint8Array | Blob} input @param {FileUploadOptions} options @returns {Promise<StoredFile>} */
117
+ async upload(name, input, options) {
118
+ const size = input instanceof Blob ? input.size : input.byteLength;
119
+ if (size < 1 || size > maxFileBytes) throw new TypeError("Files must contain 1–104857600 bytes.");
120
+ const bytes = input instanceof Blob ? new Uint8Array(await input.arrayBuffer()) : new Uint8Array(input);
121
+ options.signal?.throwIfAborted();
122
+ const hash = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", bytes));
123
+ const sha256 = btoa(String.fromCharCode(...hash));
124
+ const capability = await this.createUpload({ name, contentType: options.contentType ?? "application/octet-stream", size, sha256, metadata: options.metadata }, options);
125
+ const headers = new Headers(capability.headers);
126
+ // Fetch computes Content-Length from the body; browsers prohibit setting it explicitly.
127
+ headers.delete("Content-Length");
128
+ /** @type {RequestInit & { duplex?: "half" }} */
129
+ const init = { method: "PUT", headers, body: bytes, credentials: "omit", redirect: "error", signal: options.signal ?? null };
130
+ if (typeof process !== "undefined" && process.release?.name === "node") {
131
+ // A one-shot stream prevents Node fetch from replaying the signed PUT on HTTP 421.
132
+ headers.set("Content-Length", String(size));
133
+ init.body = new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } });
134
+ init.duplex = "half";
135
+ }
136
+ const response = await this.#fetch(capability.url, init);
137
+ void response.body?.cancel().catch(() => {});
138
+ if (!response.ok) throw new Error(`Object upload failed (${response.status}).`);
139
+ return this.completeUpload(capability.id, options);
140
+ }
141
+ /** Fetches file bytes without sending the project token to the object store.
142
+ * @param {string} id @param {FileRequestOptions} [options] @returns {Promise<Blob>} */
143
+ async download(id, options = {}) {
144
+ const capability = await this.downloadUrl(id, options);
145
+ const response = await this.#fetch(capability.url, { method: "GET", credentials: "omit", redirect: "error", signal: options.signal ?? null });
146
+ if (!response.ok) throw new Error(`Object download failed (${response.status}).`);
147
+ if (response.body === null) throw new Error("Object download returned no body.");
148
+ const reader = response.body.getReader();
149
+ /** @type {Uint8Array<ArrayBuffer>[]} */ const chunks = [];
150
+ let length = 0;
151
+ try {
152
+ for (;;) {
153
+ options.signal?.throwIfAborted();
154
+ const part = await reader.read();
155
+ if (part.done) break;
156
+ length += part.value.byteLength;
157
+ if (length > maxFileBytes) throw new Error("Object download exceeds the file size limit.");
158
+ chunks.push(new Uint8Array(part.value));
159
+ }
160
+ } finally { await reader.cancel().catch(() => {}); reader.releaseLock(); }
161
+ return new Blob(chunks, { type: "application/octet-stream" });
162
+ }
163
+ }
package/src/v1/routes.js CHANGED
@@ -1,6 +1,7 @@
1
+ import { fileStorageRoutes } from "./file-storage-routes.js";
1
2
  import { managementV1Routes } from "./management-routes.js";
2
3
  // Current TerraBase routes at e30ffdc; readShape is retained from release 6cb9745 only.
3
- /** @typedef {import("./management-routes.js").ManagementOperation | "readFreeShape" | "getShapeKey" | "getDocument" | "putDocument" | "createDocument" | "patchDocument" | "deleteDocument" | "commitTransaction" | "queryDocuments" | "createCollection" | "getCollection" | "listCollections" | "dropCollection" | "readShape" | "runSql" | "checkSql" | "explainSql" | "streamDatabaseChanges" | "listDocuments" | "getCollectionSchema" | "putCollectionSchema" | "deleteCollectionSchema" | "createDatabase" | "listDatabases" | "getDatabase" | "updateDatabase" | "deleteDatabase" | "branchDatabase"} V1Operation */
4
+ /** @typedef {import("./file-storage-routes.js").FileStorageOperation | import("./management-routes.js").ManagementOperation | "readFreeShape" | "getShapeKey" | "getDocument" | "putDocument" | "createDocument" | "patchDocument" | "deleteDocument" | "commitTransaction" | "queryDocuments" | "createCollection" | "getCollection" | "listCollections" | "dropCollection" | "readShape" | "runSql" | "checkSql" | "explainSql" | "streamDatabaseChanges" | "listDocuments" | "getCollectionSchema" | "putCollectionSchema" | "deleteCollectionSchema" | "createDatabase" | "listDatabases" | "getDatabase" | "updateDatabase" | "deleteDatabase" | "branchDatabase"} V1Operation */
4
5
  /**
5
6
  * @typedef {object} V1Route
6
7
  * @property {V1Operation} operation
@@ -61,7 +62,7 @@ export const v1Routes = Object.freeze(/** @type {V1Route[]} */ ([
61
62
  })));
62
63
  /** @param {V1Operation} operation @returns {V1Route} */
63
64
  export function v1Route(operation) {
64
- const route = [...v1Routes, ...shapeV1Routes, ...managementV1Routes].find(candidate => candidate.operation === operation);
65
+ const route = [...v1Routes, ...shapeV1Routes, ...managementV1Routes, ...fileStorageRoutes].find(candidate => candidate.operation === operation);
65
66
  if (route === undefined) throw new TypeError(`Unknown v1 operation ${operation}.`);
66
67
  return route;
67
68
  }
@@ -1,3 +1,4 @@
1
+ import { V1FileStorage } from "./file-storage.js";
1
2
  // The TerraBase v1 client: documents, transactions, queries and shapes over
2
3
  // the project-scoped C# Documents API (decision 0004 and shape-reads.md).
3
4
  import {
@@ -1003,6 +1004,8 @@ export class V1Database {
1003
1004
  * privately; it is never serialized, logged or forwarded to redirects.
1004
1005
  */
1005
1006
  export class TerraBaseV1Client {
1007
+ /** @type {V1FileStorage} */
1008
+ #storage;
1006
1009
  /** @type {V1Health} */
1007
1010
  #health;
1008
1011
  /** @type {V1Platform} */
@@ -1022,6 +1025,7 @@ export class TerraBaseV1Client {
1022
1025
  constructor(config) {
1023
1026
  requireId(config.project);
1024
1027
  this.#transport = new V1Transport(config);
1028
+ this.#storage = new V1FileStorage(this.#transport, config.fetch ?? globalThis.fetch.bind(globalThis));
1025
1029
  this.#databases = new V1Databases(this.#transport);
1026
1030
  this.#savedQueries = new V1SavedQueries(this.#transport);
1027
1031
  this.#metrics = new V1ProjectUsageClient(this.#transport);
@@ -1041,6 +1045,8 @@ export class TerraBaseV1Client {
1041
1045
 
1042
1046
  /** @returns {V1Databases} Project-scoped database catalog. */
1043
1047
  get databases() { return this.#databases; }
1048
+ /** @returns {V1FileStorage} Project file buckets. */
1049
+ get storage() { return this.#storage; }
1044
1050
  /** @returns {V1SavedQueries} */
1045
1051
  get savedQueries() { return this.#savedQueries; }
1046
1052
  /** @returns {V1ProjectUsageClient} */
package/src/v1/usage.js CHANGED
@@ -8,7 +8,7 @@ const period = record({ from: text, to: text, granularity: text, reads: numeric,
8
8
  const series = list(record({ start: text, reads: numeric, writes: numeric }));
9
9
  const databaseUsage = record({ databaseId: requireId, storageBytes: numeric, logicalBytes: numeric, collections: numeric,
10
10
  documents: numeric, reads: numeric, writes: numeric, period, series });
11
- const projectUsage = record({ projectId: requireId, databases: numeric, storageBytes: numeric, logicalBytes: numeric,
11
+ const projectUsage = record({ projectId: requireId, databases: numeric, storageBytes: numeric, logicalBytes: numeric, fileBytes: numeric,
12
12
  documents: numeric, reads: numeric, writes: numeric, period, series, items: list(databaseUsage) });
13
13
  const bandwidth = record({ bytesSent: numeric, bytesReceived: numeric });
14
14
  const directedBandwidth = list(record({ sourceRegion: text, destinationRegion: text, kind: text, bytesSent: numeric, bytesReceived: numeric }));
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Generate body models from a declared Draft 2020-12 schema, never inferred samples.
3
+ * Runtime validation remains authoritative; constraints such as ranges and lengths
4
+ * cannot be expressed by TypeScript. Patch is an RFC 7396 merge-patch body.
5
+ * @param {unknown} input
6
+ * @param {string} name A TypeScript identifier prefix.
7
+ * @returns {Promise<string>}
8
+ */
9
+ export function generateDocumentModels(input: unknown, name: string): Promise<string>;
10
+ /** Fetch the authorized declared schema using the public SDK operation. @param {V1Collection} collection @param {string} name @param {import("./v1/terrabase-v1.js").V1RequestOptions} [options] */
11
+ export function generateCollectionModels(collection: V1Collection, name: string, options?: import("./v1/terrabase-v1.js").V1RequestOptions): Promise<string>;
12
+ export type Schema = Record<string, unknown>;
13
+ import type { V1Collection } from "./v1/terrabase-v1.js";
package/types/index.d.ts CHANGED
@@ -128,6 +128,14 @@ export type V1ShapeManifest = import("./v1/shape-manifest.js").V1ShapeManifest;
128
128
  export type V1ShapeManifestRequest = import("./v1/shape-manifest.js").V1ShapeManifestRequest;
129
129
  export type V1ShapeKeyRequest = import("./v1/shape-reader.js").V1ShapeKeyRequest;
130
130
  export type V1ShapeKey = import("./v1/shape-reader.js").V1ShapeKey;
131
+ export type FileAccessPolicy = import("./v1/file-storage.js").FileAccessPolicy;
132
+ export type FileAccessRules = import("./v1/file-storage.js").FileAccessRules;
133
+ export type FileBucketInfo = import("./v1/file-storage.js").FileBucketInfo;
134
+ export type StoredFile = import("./v1/file-storage.js").StoredFile;
135
+ export type SignedFileUrl = import("./v1/file-storage.js").SignedFileUrl;
136
+ export type FileUploadRequest = import("./v1/file-storage.js").FileUploadRequest;
137
+ export type FileUploadOptions = import("./v1/file-storage.js").FileUploadOptions;
138
+ export type FileListOptions = import("./v1/file-storage.js").FileListOptions;
131
139
  export { createTerraBaseV1Client as createTerraBaseClient, TerraBaseV1Client as TerraBaseClient, createTerraBaseV1Client, TerraBaseV1Client, V1Database, V1Collection, V1Collections, V1Transaction } from "./v1/terrabase-v1.js";
132
140
  export { terraScaleReleaseBlockers, TerraScaleCapabilityUnavailableError } from "./management-contracts.js";
133
141
  export { TerraBaseV1Error, V1PreconditionFailedError, V1InvalidKeyError, V1ForbiddenScopeError, V1NotFoundError, SqlError, V1ErrorCode, isTerraBaseV1Error } from "./v1/errors.js";
@@ -135,3 +143,4 @@ export { generateId, isValidId, requireId } from "./ids.js";
135
143
  export { V1ProjectUsageClient, V1DatabaseUsageClient } from "./v1/usage.js";
136
144
  export { V1Backups, V1BackupDestinationClient } from "./v1/backups.js";
137
145
  export { createTerraBaseSystemClient, V1Health, V1Platform } from "./v1/system.js";
146
+ export { V1FileStorage, V1FileBucket } from "./v1/file-storage.js";
@@ -0,0 +1,3 @@
1
+ /** @type {readonly import("./routes.js").V1Route[]} */
2
+ export const fileStorageRoutes: readonly import("./routes.js").V1Route[];
3
+ export type FileStorageOperation = "createFileBucket" | "listFileBuckets" | "setFileBucketRules" | "deleteFileBucket" | "createFileUpload" | "completeFileUpload" | "listBucketFiles" | "createFileDownload" | "deleteBucketFile";
@@ -0,0 +1,98 @@
1
+ /** Project file buckets, authenticated by the same token as documents. */
2
+ export class V1FileStorage {
3
+ /** @param {V1Transport} transport @param {FetchLike} fetcher */
4
+ constructor(transport: V1Transport, fetcher: FetchLike);
5
+ /** @param {string} name @param {FileAccessRules} accessRules @param {FileRequestOptions} [options] @returns {Promise<FileBucketInfo>} */
6
+ createBucket(name: string, accessRules: FileAccessRules, options?: FileRequestOptions): Promise<FileBucketInfo>;
7
+ /** @param {FileListOptions} [options] @returns {Promise<{items: FileBucketInfo[], next: string | null}>} */
8
+ listBuckets(options?: FileListOptions): Promise<{
9
+ items: FileBucketInfo[];
10
+ next: string | null;
11
+ }>;
12
+ /** @param {string} id @returns {V1FileBucket} */
13
+ bucket(id: string): V1FileBucket;
14
+ #private;
15
+ }
16
+ /** Immutable files in one project bucket. */
17
+ export class V1FileBucket {
18
+ /** @param {V1Transport} transport @param {FetchLike} fetcher @param {string} bucket */
19
+ constructor(transport: V1Transport, fetcher: FetchLike, bucket: string);
20
+ /** @param {FileAccessRules} accessRules @param {FileRequestOptions} [options] @returns {Promise<FileBucketInfo>} */
21
+ setRules(accessRules: FileAccessRules, options?: FileRequestOptions): Promise<FileBucketInfo>;
22
+ /** @param {FileRequestOptions} [options] @returns {Promise<void>} */
23
+ delete(options?: FileRequestOptions): Promise<void>;
24
+ /** @param {FileUploadRequest} request @param {FileRequestOptions} [options] @returns {Promise<SignedFileUrl>} */
25
+ createUpload(request: FileUploadRequest, options?: FileRequestOptions): Promise<SignedFileUrl>;
26
+ /** @param {string} id @param {FileRequestOptions} [options] @returns {Promise<StoredFile>} */
27
+ completeUpload(id: string, options?: FileRequestOptions): Promise<StoredFile>;
28
+ /** @param {FileListOptions} [options] @returns {Promise<{items: StoredFile[], next: string | null}>} */
29
+ list(options?: FileListOptions): Promise<{
30
+ items: StoredFile[];
31
+ next: string | null;
32
+ }>;
33
+ /** @param {string} id @param {FileRequestOptions} [options] @returns {Promise<SignedFileUrl>} */
34
+ downloadUrl(id: string, options?: FileRequestOptions): Promise<SignedFileUrl>;
35
+ /** @param {string} id @param {FileRequestOptions} [options] @returns {Promise<void>} */
36
+ deleteFile(id: string, options?: FileRequestOptions): Promise<void>;
37
+ /** Hashes a private byte snapshot, uploads it without project credentials, and verifies completion. No retries.
38
+ * @param {string} name @param {Uint8Array | Blob} input @param {FileUploadOptions} options @returns {Promise<StoredFile>} */
39
+ upload(name: string, input: Uint8Array | Blob, options: FileUploadOptions): Promise<StoredFile>;
40
+ /** Fetches file bytes without sending the project token to the object store.
41
+ * @param {string} id @param {FileRequestOptions} [options] @returns {Promise<Blob>} */
42
+ download(id: string, options?: FileRequestOptions): Promise<Blob>;
43
+ #private;
44
+ }
45
+ export type FileAccessPolicy = {
46
+ owner: boolean;
47
+ public: boolean;
48
+ roles: readonly string[];
49
+ };
50
+ export type FileAccessRules = {
51
+ owner: string | null;
52
+ read: FileAccessPolicy;
53
+ write: FileAccessPolicy;
54
+ };
55
+ export type FileBucketInfo = {
56
+ id: string;
57
+ name: string;
58
+ region: string;
59
+ rules: FileAccessRules;
60
+ };
61
+ export type StoredFile = {
62
+ id: string;
63
+ name: string;
64
+ contentType: string;
65
+ size: number;
66
+ sha256: string;
67
+ metadata: JsonObject;
68
+ status: string;
69
+ uploadExpiresAt: string;
70
+ };
71
+ export type SignedFileUrl = {
72
+ id: string;
73
+ url: string;
74
+ expiresAt: string;
75
+ headers: Readonly<Record<string, string>>;
76
+ };
77
+ export type FileUploadRequest = {
78
+ name: string;
79
+ contentType: string;
80
+ size: number;
81
+ sha256: string;
82
+ metadata: JsonObject;
83
+ };
84
+ export type FileRequestOptions = {
85
+ signal?: AbortSignal | undefined;
86
+ };
87
+ export type FileListOptions = {
88
+ after?: string | undefined;
89
+ signal?: AbortSignal | undefined;
90
+ };
91
+ export type FileUploadOptions = {
92
+ metadata: JsonObject;
93
+ contentType?: string | undefined;
94
+ signal?: AbortSignal | undefined;
95
+ };
96
+ import type { V1Transport } from "./transport.js";
97
+ import type { FetchLike } from "../config.js";
98
+ import type { JsonObject } from "./json.js";
@@ -6,7 +6,7 @@ export function v1RoutePath(route: V1Route, params: Readonly<Record<string, stri
6
6
  export const v1Routes: readonly V1Route[];
7
7
  /** @type {readonly V1Route[]} */
8
8
  export const shapeV1Routes: readonly V1Route[];
9
- export type V1Operation = import("./management-routes.js").ManagementOperation | "readFreeShape" | "getShapeKey" | "getDocument" | "putDocument" | "createDocument" | "patchDocument" | "deleteDocument" | "commitTransaction" | "queryDocuments" | "createCollection" | "getCollection" | "listCollections" | "dropCollection" | "readShape" | "runSql" | "checkSql" | "explainSql" | "streamDatabaseChanges" | "listDocuments" | "getCollectionSchema" | "putCollectionSchema" | "deleteCollectionSchema" | "createDatabase" | "listDatabases" | "getDatabase" | "updateDatabase" | "deleteDatabase" | "branchDatabase";
9
+ export type V1Operation = import("./file-storage-routes.js").FileStorageOperation | import("./management-routes.js").ManagementOperation | "readFreeShape" | "getShapeKey" | "getDocument" | "putDocument" | "createDocument" | "patchDocument" | "deleteDocument" | "commitTransaction" | "queryDocuments" | "createCollection" | "getCollection" | "listCollections" | "dropCollection" | "readShape" | "runSql" | "checkSql" | "explainSql" | "streamDatabaseChanges" | "listDocuments" | "getCollectionSchema" | "putCollectionSchema" | "deleteCollectionSchema" | "createDatabase" | "listDatabases" | "getDatabase" | "updateDatabase" | "deleteDatabase" | "branchDatabase";
10
10
  export type V1Route = {
11
11
  operation: V1Operation;
12
12
  method: "GET" | "PUT" | "POST" | "PATCH" | "DELETE";
@@ -341,6 +341,8 @@ export class TerraBaseV1Client {
341
341
  database(name: string): V1Database;
342
342
  /** @returns {V1Databases} Project-scoped database catalog. */
343
343
  get databases(): V1Databases;
344
+ /** @returns {V1FileStorage} Project file buckets. */
345
+ get storage(): V1FileStorage;
344
346
  /** @returns {V1SavedQueries} */
345
347
  get savedQueries(): V1SavedQueries;
346
348
  /** @returns {V1ProjectUsageClient} */
@@ -592,6 +594,7 @@ import { V1DatabaseUsageClient } from "./usage.js";
592
594
  import { V1Indexes } from "./indexes.js";
593
595
  import { V1DatabaseSchemaClient } from "./database-schema.js";
594
596
  import { V1Databases } from "./catalog.js";
597
+ import { V1FileStorage } from "./file-storage.js";
595
598
  import { V1SavedQueries } from "./saved-queries.js";
596
599
  import { V1ProjectUsageClient } from "./usage.js";
597
600
  import { V1BackupDestinationClient } from "./backups.js";
@@ -63,6 +63,7 @@ declare const projectUsage: import("./management-decode.js").Decoder<Readonly<{
63
63
  databases: number | import("./json.js").ExactDecimal;
64
64
  storageBytes: number | import("./json.js").ExactDecimal;
65
65
  logicalBytes: number | import("./json.js").ExactDecimal;
66
+ fileBytes: number | import("./json.js").ExactDecimal;
66
67
  documents: number | import("./json.js").ExactDecimal;
67
68
  reads: number | import("./json.js").ExactDecimal;
68
69
  writes: number | import("./json.js").ExactDecimal;