@terrascale/sdk 0.0.8 → 0.0.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/CHANGELOG.md +16 -0
- package/README.md +47 -50
- package/package.json +15 -4
- package/src/codegen-cli.js +17 -0
- package/src/codegen.js +137 -0
- package/src/http.js +21 -29
- package/src/index.js +1 -1
- package/src/ts-auth.js +24 -4
- package/src/v1/system.js +4 -2
- package/types/codegen-cli.d.ts +2 -0
- package/types/codegen.d.ts +13 -0
- package/types/ts-auth.d.ts +15 -0
- package/types/v1/system.d.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.10 — production contract fixes
|
|
4
|
+
|
|
5
|
+
The named hosted-auth token operation accepts explicit project API-key grants
|
|
6
|
+
without a resource parameter. Public health probes accept omitted private build
|
|
7
|
+
metadata. Hosted-auth operations return authentication denials without replaying
|
|
8
|
+
requests with a changed credential. README qualification commands follow the
|
|
9
|
+
binary-free HTTP test policy and document the existing TypeScript model tooling.
|
|
10
|
+
|
|
11
|
+
## 0.0.9 — declared JSON Schema models
|
|
12
|
+
|
|
13
|
+
Node tooling at `@terrascale/sdk/codegen` generates document, create and
|
|
14
|
+
merge-patch body types from `collection.getSchema()` or a schema file. Install
|
|
15
|
+
the exact tooling peer `json-schema-to-typescript@16.0.0`; the `terrascale-models`
|
|
16
|
+
command is separate from browser/runtime entrypoints. Unsupported constructs
|
|
17
|
+
produce JSON Pointer diagnostics. Runtime validation remains authoritative.
|
|
18
|
+
|
|
3
19
|
## 0.0.7 — live shape reconnect, browser bootstrap and read replica
|
|
4
20
|
|
|
5
21
|
`subscribeShape` keeps its state and retries the same position after a lost
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @terrascale/sdk
|
|
2
2
|
|
|
3
3
|
The JavaScript/JSDoc SDK for the project-scoped C#/.NET TerraBase server
|
|
4
|
-
(`TerraScale/terrascale`).
|
|
4
|
+
(`TerraScale/terrascale`). The historical source compatibility audit records
|
|
5
5
|
`e30ffdc287530551b161b0130220be53d3c0475e`; see the
|
|
6
6
|
[compatibility audit](docs/api-compatibility-20261013.md) for exact evidence and open gates.
|
|
7
7
|
The package targets Node.js 22 and 24 and current Chrome, Firefox, Safari, and Edge.
|
|
@@ -100,8 +100,8 @@ to TS-507; this release requires an explicit URL or caller-supplied resolver.
|
|
|
100
100
|
|
|
101
101
|
Current handler source implements project-scoped database names and point
|
|
102
102
|
addresses as `/documents/by-key?id=…`. The complete method/path/scope inventory
|
|
103
|
-
is in [sdk-v1-route-manifest.json](sdk-v1-route-manifest.json),
|
|
104
|
-
|
|
103
|
+
is in [sdk-v1-route-manifest.json](sdk-v1-route-manifest.json), including the
|
|
104
|
+
current public inventory and a retained historical shape route. Current generated
|
|
105
105
|
OpenAPI and immutable release qualification remain open gates.
|
|
106
106
|
|
|
107
107
|
| Surface | Public methods |
|
|
@@ -257,9 +257,9 @@ for await (const row of db.export({ after: last, signal: controller.signal })) {
|
|
|
257
257
|
}
|
|
258
258
|
```
|
|
259
259
|
|
|
260
|
-
The exported collection `shape()`
|
|
261
|
-
`
|
|
262
|
-
|
|
260
|
+
The exported collection `shape()` reads the current free shape protocol using
|
|
261
|
+
`documents:read`. A retained historical `shapes:read` route descriptor does not
|
|
262
|
+
qualify that current protocol. SSE changes are a separate database
|
|
263
263
|
feed, not authorized bounded-query snapshots or the planned reactive WebSocket API.
|
|
264
264
|
|
|
265
265
|
## Free shape subscriptions (protocol v1)
|
|
@@ -354,12 +354,13 @@ in either runtime.
|
|
|
354
354
|
This implements the [proposed protocol](contracts/shape-v1/shape-reads.md) at
|
|
355
355
|
`993927ce7ecf8f9b889d24d4d1d21ec96eaf9416`, using `documents:read`. HTTP fakes and the
|
|
356
356
|
upstream [conformance vectors](test/fixtures/shape-vectors.json) verify client
|
|
357
|
-
behavior; they do not qualify a producer deployment.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
357
|
+
behavior; they do not qualify a producer deployment. Isolated production checks
|
|
358
|
+
on 2026-10-26 verified shape bootstrap, continuation, manifest and epoch-key reads,
|
|
359
|
+
live subscription updates, and checkpoint restore. Full current generated
|
|
360
|
+
OpenAPI and semantic-corpus parity, sealed-object delivery/decryption, and
|
|
361
|
+
automatic region discovery remain open qualification gates. These collection
|
|
362
|
+
reads do not implement the planned bounded-query WebSocket result protocol.
|
|
363
|
+
No TerraBase binary is downloaded or run by SDK tests.
|
|
363
364
|
|
|
364
365
|
Failures throw `TerraBaseV1Error` with status, code, optional transaction `op`,
|
|
365
366
|
parsed `body`, raw `responseText`, response `headers`, and a received
|
|
@@ -376,44 +377,22 @@ failing in this draft. See the audit for exact results and producer dependencies
|
|
|
376
377
|
## Ouroboros and qualification
|
|
377
378
|
|
|
378
379
|
Ouroboros means TerraScale services store their data in the same production
|
|
379
|
-
TerraBase that customers use.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
contract
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
This official prerelease contains the qualified TS-754 ULID producer, the renamed
|
|
396
|
-
`TerraBase` assembly and `src/TerraBase` sources. Its asset name remains `terrabase-linux-amd64`.
|
|
397
|
-
The SDK integration harness consumes its official asset read-only when the
|
|
398
|
-
version, source revision, size and SHA-256 are verified.
|
|
399
|
-
The acquisition script verifies the official release, revision, asset size and
|
|
400
|
-
digest. Private downloads require `FORGEJO_TOKEN`. Missing or mismatched official
|
|
401
|
-
assets block qualification rather than selecting older bytes.
|
|
402
|
-
|
|
403
|
-
```sh
|
|
404
|
-
nice -n 19 npm run provision:terrabase
|
|
405
|
-
export TERRABASE_BINARY=/absolute/path/printed/by/the/provisioner
|
|
406
|
-
nice -n 19 npm run test:integration
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
The integration runner rechecks the binary digest, starts an isolated TLS server,
|
|
410
|
-
verifies its version and runtime OpenAPI digest, and provisions a disposable
|
|
411
|
-
organization, project, database, collections and scoped keys. It supplies the
|
|
412
|
-
`TB_V1_*` environment and trusted local certificate to the real-server tests and
|
|
413
|
-
cleans up the process and allocation afterwards. No TLS validation is disabled.
|
|
414
|
-
Direct live test configuration uses `TB_V1_URL`, `TB_V1_PROJECT`,
|
|
415
|
-
`TB_V1_DATA_KEY` and optionally `TB_V1_DATABASE` (default `app`);
|
|
416
|
-
`TB_V1_REQUIRED=1` makes absent integration inputs fail rather than skip.
|
|
380
|
+
TerraBase that customers use. Qualification requires an authorized, isolated
|
|
381
|
+
real-service allocation, independently verified producer release provenance,
|
|
382
|
+
and current generated-contract and route-manifest parity. HTTP fakes, local
|
|
383
|
+
SQLite, historical contract inputs and typed builds cannot establish deployed
|
|
384
|
+
producer compatibility.
|
|
385
|
+
|
|
386
|
+
This SDK repository does not download or launch a TerraBase binary. The
|
|
387
|
+
historical [release pin](scripts/ci/terrabase-release.json) does not qualify the
|
|
388
|
+
current producer or JWT authorization. Keep real HTTP tests environment-gated;
|
|
389
|
+
missing live configuration is skipped, never successful integration evidence.
|
|
390
|
+
|
|
391
|
+
Existing live tests consume `TB_V1_URL`, `TB_V1_PROJECT`, `TB_V1_DATA_KEY` and
|
|
392
|
+
optionally `TB_V1_DATABASE` (default `app`) from an independently provisioned
|
|
393
|
+
allocation. Denial checks use `TB_V1_READ_KEY` and `TB_V1_WRITE_KEY`. Supply
|
|
394
|
+
credentials privately; never commit keys, cookies, environment files or real
|
|
395
|
+
tenant data. Local checks below do not substitute for this live gate.
|
|
417
396
|
|
|
418
397
|
Local checks run at low CPU priority:
|
|
419
398
|
|
|
@@ -429,6 +408,24 @@ nice -n 19 npm test
|
|
|
429
408
|
without adding a linter dependency. CI runs these commands at normal priority.
|
|
430
409
|
See [CHANGELOG.md](CHANGELOG.md) for the 0.0.1 migration and breaking changes.
|
|
431
410
|
|
|
411
|
+
## TypeScript document models
|
|
412
|
+
|
|
413
|
+
Node-only `@terrascale/sdk/codegen` generates body aliases from declared JSON
|
|
414
|
+
Schema, using `generateDocumentModels(schema, "Order")` or the authorized
|
|
415
|
+
`generateCollectionModels(collection, "Order", { signal })` operation.
|
|
416
|
+
Install its exact tooling peer separately:
|
|
417
|
+
|
|
418
|
+
```sh
|
|
419
|
+
npm install --save-dev json-schema-to-typescript@16.0.0
|
|
420
|
+
npx terrascale-models schema.json Order models.ts
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
The input is the schema itself, without the API response wrapper. Output has
|
|
424
|
+
`OrderDocument`, `OrderCreate` and `OrderPatch`; keep codegen out of browser
|
|
425
|
+
bundles. Generated aliases do not validate or automatically narrow SDK reads.
|
|
426
|
+
See [TypeScript document models](https://docs.terrascale.tech/guides/schema-validation#generate-typescript-document-models)
|
|
427
|
+
for the supported subset, merge-patch null semantics and strict compiler setup.
|
|
428
|
+
|
|
432
429
|
## Separate and deprecated entrypoints
|
|
433
430
|
|
|
434
431
|
`@terrascale/sdk/ts-auth` retains its separate hosted identity management and OIDC
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terrascale/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
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",
|
|
@@ -109,7 +115,8 @@
|
|
|
109
115
|
"@better-auth/core": "1.7.2",
|
|
110
116
|
"react": ">=18.2.0 <20.0.0",
|
|
111
117
|
"pg": "8.23.0",
|
|
112
|
-
"@tanstack/db": "0.9.2"
|
|
118
|
+
"@tanstack/db": "0.9.2",
|
|
119
|
+
"json-schema-to-typescript": "16.0.0"
|
|
113
120
|
},
|
|
114
121
|
"peerDependenciesMeta": {
|
|
115
122
|
"@better-auth/core": {
|
|
@@ -123,6 +130,9 @@
|
|
|
123
130
|
},
|
|
124
131
|
"@tanstack/db": {
|
|
125
132
|
"optional": true
|
|
133
|
+
},
|
|
134
|
+
"json-schema-to-typescript": {
|
|
135
|
+
"optional": true
|
|
126
136
|
}
|
|
127
137
|
},
|
|
128
138
|
"devDependencies": {
|
|
@@ -140,7 +150,8 @@
|
|
|
140
150
|
"@types/react": "19.2.14",
|
|
141
151
|
"@types/react-test-renderer": "19.1.0",
|
|
142
152
|
"pg": "8.23.0",
|
|
143
|
-
"@types/pg": "8.23.1"
|
|
153
|
+
"@types/pg": "8.23.1",
|
|
154
|
+
"json-schema-to-typescript": "16.0.0"
|
|
144
155
|
},
|
|
145
156
|
"engines": {
|
|
146
157
|
"node": "22.x || 24.x",
|
|
@@ -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/http.js
CHANGED
|
@@ -75,9 +75,6 @@ export class TsAuthHttp {
|
|
|
75
75
|
? JSON.stringify(options.body)
|
|
76
76
|
: options.formBody?.toString();
|
|
77
77
|
let attempt = 0;
|
|
78
|
-
let refreshed = false;
|
|
79
|
-
/** @type {string | undefined} */
|
|
80
|
-
let refreshedToken;
|
|
81
78
|
while (true) {
|
|
82
79
|
signal?.throwIfAborted();
|
|
83
80
|
let responseReceived = false;
|
|
@@ -87,11 +84,10 @@ export class TsAuthHttp {
|
|
|
87
84
|
const oidc = options.routeName.startsWith("tsauth.oidc.");
|
|
88
85
|
const token = oidc
|
|
89
86
|
? undefined
|
|
90
|
-
: (
|
|
91
|
-
(await withCancellation(
|
|
87
|
+
: await withCancellation(
|
|
92
88
|
resolveAccessToken(this.#accessToken),
|
|
93
89
|
signal,
|
|
94
|
-
)
|
|
90
|
+
);
|
|
95
91
|
if (token !== undefined && token.length > 0)
|
|
96
92
|
headers.set(
|
|
97
93
|
"authorization",
|
|
@@ -133,19 +129,31 @@ export class TsAuthHttp {
|
|
|
133
129
|
? "application/json"
|
|
134
130
|
: "application/x-www-form-urlencoded",
|
|
135
131
|
);
|
|
132
|
+
/** @type {RequestInit & { duplex?: "half" }} */
|
|
133
|
+
const init = {
|
|
134
|
+
method: options.method,
|
|
135
|
+
headers,
|
|
136
|
+
redirect: "manual",
|
|
137
|
+
...(signal === undefined ? {} : { signal }),
|
|
138
|
+
...(body === undefined ? {} : { body }),
|
|
139
|
+
};
|
|
140
|
+
if (options.method !== "GET" && typeof process !== "undefined" && process.release?.name === "node") {
|
|
141
|
+
// Native Node fetch replays buffered mutations on 421, even with SDK retries disabled.
|
|
142
|
+
// Empty DELETEs also need a one-shot body. Keep JSON/form bytes and media types unchanged.
|
|
143
|
+
const bytes = body === undefined ? undefined : new TextEncoder().encode(body);
|
|
144
|
+
init.body = new ReadableStream({ start(controller) {
|
|
145
|
+
if (bytes !== undefined && bytes.byteLength > 0) controller.enqueue(bytes);
|
|
146
|
+
controller.close();
|
|
147
|
+
} });
|
|
148
|
+
init.duplex = "half";
|
|
149
|
+
}
|
|
136
150
|
const request = new Request(
|
|
137
151
|
buildUrl(
|
|
138
152
|
this.config.apiBaseUrl,
|
|
139
153
|
options.path,
|
|
140
154
|
options.query,
|
|
141
155
|
),
|
|
142
|
-
|
|
143
|
-
method: options.method,
|
|
144
|
-
headers,
|
|
145
|
-
redirect: "manual",
|
|
146
|
-
...(signal === undefined ? {} : { signal }),
|
|
147
|
-
...(body === undefined ? {} : { body }),
|
|
148
|
-
},
|
|
156
|
+
init,
|
|
149
157
|
);
|
|
150
158
|
signal?.throwIfAborted();
|
|
151
159
|
fetchStarted = true;
|
|
@@ -206,22 +214,6 @@ export class TsAuthHttp {
|
|
|
206
214
|
/** @type {TResponse} */ (parsed.value),
|
|
207
215
|
);
|
|
208
216
|
}
|
|
209
|
-
if (
|
|
210
|
-
response.status === 401 &&
|
|
211
|
-
!oidc &&
|
|
212
|
-
!refreshed &&
|
|
213
|
-
typeof this.#accessToken === "function"
|
|
214
|
-
) {
|
|
215
|
-
const next = await withCancellation(
|
|
216
|
-
resolveAccessToken(this.#accessToken),
|
|
217
|
-
signal,
|
|
218
|
-
);
|
|
219
|
-
if (next !== undefined && next.length > 0 && next !== token) {
|
|
220
|
-
refreshed = true;
|
|
221
|
-
refreshedToken = next;
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
217
|
if (
|
|
226
218
|
options.method === "GET" &&
|
|
227
219
|
attempt < this.config.maxRetryAttempts &&
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createTerraBaseV1Client as createTerraBaseClient, TerraBaseV1Client as TerraBaseClient } from "./v1/terrabase-v1.js";
|
|
2
2
|
export { terraScaleReleaseBlockers, TerraScaleCapabilityUnavailableError } from "./management-contracts.js";
|
|
3
|
-
// Project-scoped TerraBase API
|
|
3
|
+
// Project-scoped TerraBase API, including current free collection shape reads.
|
|
4
4
|
export {
|
|
5
5
|
createTerraBaseV1Client,
|
|
6
6
|
TerraBaseV1Client,
|
package/src/ts-auth.js
CHANGED
|
@@ -2086,6 +2086,13 @@ export class TsAuthInstanceClient {
|
|
|
2086
2086
|
* readonly grant_type: "client_credentials";
|
|
2087
2087
|
* readonly scope: string;
|
|
2088
2088
|
* readonly resource: string;
|
|
2089
|
+
* readonly project?: never;
|
|
2090
|
+
* }
|
|
2091
|
+
* | {
|
|
2092
|
+
* readonly grant_type: "client_credentials";
|
|
2093
|
+
* readonly project: import("./ids.js").EntityId;
|
|
2094
|
+
* readonly scope?: string | undefined;
|
|
2095
|
+
* readonly resource?: never;
|
|
2089
2096
|
* }
|
|
2090
2097
|
* | {
|
|
2091
2098
|
* readonly grant_type: "urn:ietf:params:oauth:grant-type:device_code";
|
|
@@ -2245,6 +2252,8 @@ export class TsAuthOidcClient {
|
|
|
2245
2252
|
|
|
2246
2253
|
/**
|
|
2247
2254
|
* client_secret_post only. Omitted resource retains the stored code/device/refresh binding.
|
|
2255
|
+
* Platform API keys use explicit project and optional subset scope, without resource
|
|
2256
|
+
* (producer PlatformGrants.ApiKeyAsync and platform-api.openapi.yaml ClientCredentialsGrant).
|
|
2248
2257
|
* @param {string} clientId
|
|
2249
2258
|
* @param {string} clientSecret
|
|
2250
2259
|
* @param {TsAuthOidcTokenRequest} request
|
|
@@ -2259,7 +2268,7 @@ export class TsAuthOidcClient {
|
|
|
2259
2268
|
: request.grant_type === "refresh_token"
|
|
2260
2269
|
? ["grant_type", "refresh_token", "scope", "resource"]
|
|
2261
2270
|
: request.grant_type === "client_credentials"
|
|
2262
|
-
? ["grant_type", "scope", "resource"]
|
|
2271
|
+
? ["grant_type", "scope", "project" in request ? "project" : "resource"]
|
|
2263
2272
|
: request.grant_type ===
|
|
2264
2273
|
"urn:ietf:params:oauth:grant-type:device_code"
|
|
2265
2274
|
? ["grant_type", "device_code", "resource"]
|
|
@@ -2289,13 +2298,24 @@ export class TsAuthOidcClient {
|
|
|
2289
2298
|
if (request.scope !== undefined)
|
|
2290
2299
|
form.set("scope", requireOauthScope(request.scope));
|
|
2291
2300
|
} else if (request.grant_type === "client_credentials") {
|
|
2292
|
-
|
|
2293
|
-
|
|
2301
|
+
if ("project" in request) {
|
|
2302
|
+
form.set("project", requireId(request.project, "project"));
|
|
2303
|
+
if (request.scope !== undefined) form.set("scope", requireOauthScope(request.scope));
|
|
2304
|
+
} else {
|
|
2305
|
+
form.set("resource", requireResourceAudience(request.resource));
|
|
2306
|
+
form.set("scope", requireOauthScope(request.scope));
|
|
2307
|
+
}
|
|
2294
2308
|
} else {
|
|
2295
2309
|
form.set("device_code", requireText(request.device_code, "device_code"));
|
|
2296
2310
|
}
|
|
2311
|
+
const projectScope = request.grant_type === "client_credentials" && "project" in request ? request.scope : undefined;
|
|
2312
|
+
const pending = this.#postForm(`${this.#slug}/oauth/token`, "tsauth.oidc.token", form);
|
|
2297
2313
|
return oauthResponse(
|
|
2298
|
-
|
|
2314
|
+
// TokenBody follows RFC 6749 §5.1: omitted scope equals the explicit requested scope.
|
|
2315
|
+
// Snapshot it before dispatch; never infer a default or normalize other grant contracts.
|
|
2316
|
+
projectScope === undefined ? pending : pending.then(result =>
|
|
2317
|
+
result.ok && oauthRecord(result.value) && !Object.hasOwn(result.value, "scope")
|
|
2318
|
+
? { ...result, value: { ...result.value, scope: projectScope } } : result),
|
|
2299
2319
|
/** @returns {value is TsAuthOidcTokenResponse} */
|
|
2300
2320
|
(value) => isTokenResponse(value, grantType),
|
|
2301
2321
|
);
|
package/src/v1/system.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { decodeDocument } from "./decode.js";
|
|
2
2
|
import { requireId } from "../ids.js";
|
|
3
3
|
import { requireDocumentId, requireName } from "./ids.js";
|
|
4
|
-
import { flag, list, nullable, numeric, record, text } from "./management-decode.js";
|
|
4
|
+
import { flag, list, nullable, numeric, optional, record, text } from "./management-decode.js";
|
|
5
5
|
import { members } from "./management-validation.js";
|
|
6
6
|
import { decodeCollection, decodeQueryPage } from "./terrabase-v1.js";
|
|
7
7
|
import { V1Transport } from "./transport.js";
|
|
8
8
|
/** @import { V1RequestOptions } from "./terrabase-v1.js" */
|
|
9
|
-
|
|
9
|
+
// Public probes omit build metadata; only the internal listener emits it.
|
|
10
|
+
const status = record({ status: text, version: text, region: text,
|
|
11
|
+
storageFormat: optional(nullable(text)), revision: optional(nullable(text)), machine: optional(nullable(text)) });
|
|
10
12
|
const replication = record({ region: text, status: text, maxLagSeconds: nullable(numeric),
|
|
11
13
|
peers: list(record({ region: text, status: text, lagSeconds: nullable(numeric) })) });
|
|
12
14
|
const databases = record({ items: list(record({ name: text, id: requireId, homeRegion: text, replicaRegions: list(text), allRegions: flag })) });
|
|
@@ -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/ts-auth.d.ts
CHANGED
|
@@ -1103,6 +1103,13 @@ export class TsAuthInstanceClient {
|
|
|
1103
1103
|
* readonly grant_type: "client_credentials";
|
|
1104
1104
|
* readonly scope: string;
|
|
1105
1105
|
* readonly resource: string;
|
|
1106
|
+
* readonly project?: never;
|
|
1107
|
+
* }
|
|
1108
|
+
* | {
|
|
1109
|
+
* readonly grant_type: "client_credentials";
|
|
1110
|
+
* readonly project: import("./ids.js").EntityId;
|
|
1111
|
+
* readonly scope?: string | undefined;
|
|
1112
|
+
* readonly resource?: never;
|
|
1106
1113
|
* }
|
|
1107
1114
|
* | {
|
|
1108
1115
|
* readonly grant_type: "urn:ietf:params:oauth:grant-type:device_code";
|
|
@@ -1206,6 +1213,8 @@ export class TsAuthOidcClient {
|
|
|
1206
1213
|
deviceAuthorization(clientId: string, clientSecret: string, request?: TsAuthOidcDeviceAuthorizationRequest): Promise<TerraScaleResult<TsAuthOidcDeviceAuthorizationResponse>>;
|
|
1207
1214
|
/**
|
|
1208
1215
|
* client_secret_post only. Omitted resource retains the stored code/device/refresh binding.
|
|
1216
|
+
* Platform API keys use explicit project and optional subset scope, without resource
|
|
1217
|
+
* (producer PlatformGrants.ApiKeyAsync and platform-api.openapi.yaml ClientCredentialsGrant).
|
|
1209
1218
|
* @param {string} clientId
|
|
1210
1219
|
* @param {string} clientSecret
|
|
1211
1220
|
* @param {TsAuthOidcTokenRequest} request
|
|
@@ -1766,6 +1775,12 @@ export type TsAuthOidcTokenRequest = {
|
|
|
1766
1775
|
readonly grant_type: "client_credentials";
|
|
1767
1776
|
readonly scope: string;
|
|
1768
1777
|
readonly resource: string;
|
|
1778
|
+
readonly project?: never;
|
|
1779
|
+
} | {
|
|
1780
|
+
readonly grant_type: "client_credentials";
|
|
1781
|
+
readonly project: import("./ids.js").EntityId;
|
|
1782
|
+
readonly scope?: string | undefined;
|
|
1783
|
+
readonly resource?: never;
|
|
1769
1784
|
} | {
|
|
1770
1785
|
readonly grant_type: "urn:ietf:params:oauth:grant-type:device_code";
|
|
1771
1786
|
readonly device_code: string;
|
package/types/v1/system.d.ts
CHANGED
|
@@ -59,9 +59,9 @@ declare const status: import("./management-decode.js").Decoder<Readonly<{
|
|
|
59
59
|
status: string;
|
|
60
60
|
version: string;
|
|
61
61
|
region: string;
|
|
62
|
-
storageFormat: string;
|
|
63
|
-
revision: string | null;
|
|
64
|
-
machine: string;
|
|
62
|
+
storageFormat: string | null | undefined;
|
|
63
|
+
revision: string | null | undefined;
|
|
64
|
+
machine: string | null | undefined;
|
|
65
65
|
}>>;
|
|
66
66
|
declare const replication: import("./management-decode.js").Decoder<Readonly<{
|
|
67
67
|
region: string;
|