@terrascale/sdk 0.0.9 → 0.0.11
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 +15 -0
- package/README.md +47 -50
- package/package.json +1 -1
- package/src/http.js +21 -29
- package/src/index.js +1 -1
- package/src/ts-auth.js +24 -4
- package/src/v1/shape-subscription.js +33 -8
- package/src/v1/system.js +4 -2
- package/types/ts-auth.d.ts +15 -0
- package/types/v1/system.d.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.11 — read replica isolation and freshness
|
|
4
|
+
|
|
5
|
+
Persisted shape replicas bind to their service, project, database, collection and
|
|
6
|
+
filter. Authorization denial clears local documents and freshness; transient
|
|
7
|
+
disconnection marks the view stale while retrying the same checkpoint. Closing
|
|
8
|
+
a subscription settles even when a replica store never finishes loading or saving.
|
|
9
|
+
|
|
10
|
+
## 0.0.10 — production contract fixes
|
|
11
|
+
|
|
12
|
+
The named hosted-auth token operation accepts explicit project API-key grants
|
|
13
|
+
without a resource parameter. Public health probes accept omitted private build
|
|
14
|
+
metadata. Hosted-auth operations return authentication denials without replaying
|
|
15
|
+
requests with a changed credential. README qualification commands follow the
|
|
16
|
+
binary-free HTTP test policy and document the existing TypeScript model tooling.
|
|
17
|
+
|
|
3
18
|
## 0.0.9 — declared JSON Schema models
|
|
4
19
|
|
|
5
20
|
Node tooling at `@terrascale/sdk/codegen` generates document, create and
|
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
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
|
);
|
|
@@ -51,6 +51,15 @@ function deepFreeze(value) {
|
|
|
51
51
|
}
|
|
52
52
|
// setTimeout overflows above 2^31-1 ms and fires at once, so a long Retry-After is waited in chunks.
|
|
53
53
|
const maximumTimerMilliseconds = 2 ** 31 - 1;
|
|
54
|
+
/** @template T @param {T | PromiseLike<T>} operation @param {AbortSignal} signal @returns {Promise<T>} */
|
|
55
|
+
function waitForStore(operation, signal) {
|
|
56
|
+
signal.throwIfAborted();
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
|
|
59
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
60
|
+
void Promise.resolve(operation).then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
54
63
|
/** @param {number} milliseconds @param {AbortSignal} signal @returns {Promise<void>} */
|
|
55
64
|
function sleep(milliseconds, signal) {
|
|
56
65
|
return new Promise((resolve, reject) => {
|
|
@@ -92,7 +101,7 @@ export class V1ShapeSubscription {
|
|
|
92
101
|
#onUpdate;
|
|
93
102
|
#replica;
|
|
94
103
|
/** @type {string} */
|
|
95
|
-
#scope;
|
|
104
|
+
#scope = "";
|
|
96
105
|
#external;
|
|
97
106
|
#onExternalAbort = () => this.close();
|
|
98
107
|
#done;
|
|
@@ -117,8 +126,6 @@ export class V1ShapeSubscription {
|
|
|
117
126
|
this.#where = where === undefined ? undefined : requireFilter(parseExactJson(stringifyExactJson(requireFilter(where), "shape filter")));
|
|
118
127
|
this.#onUpdate = onUpdate;
|
|
119
128
|
this.#replica = replica;
|
|
120
|
-
// A replica restores only the identical shape it was saved from.
|
|
121
|
-
this.#scope = stringifyExactJson([this.#params.database, this.#params.collection, this.#where ?? null], "shape scope");
|
|
122
129
|
this.#external = signal;
|
|
123
130
|
if (signal?.aborted === true) this.close();
|
|
124
131
|
else signal?.addEventListener("abort", this.#onExternalAbort);
|
|
@@ -136,11 +143,15 @@ export class V1ShapeSubscription {
|
|
|
136
143
|
async #run() {
|
|
137
144
|
const signal = this.#controller.signal;
|
|
138
145
|
let retries = 0;
|
|
146
|
+
/** @type {import("./shapes.js").V1ShapeRequest | undefined} */
|
|
147
|
+
let retryRequest;
|
|
139
148
|
try {
|
|
149
|
+
// Bind persisted authorization views to the service and project as well as the shape.
|
|
150
|
+
this.#scope = stringifyExactJson([await this.#transport.url("readFreeShape", this.#params, signal), this.#where ?? null], "shape scope");
|
|
140
151
|
if (this.#replica !== undefined) await this.#restore(signal);
|
|
141
152
|
for (;;) {
|
|
142
153
|
signal.throwIfAborted();
|
|
143
|
-
const request = this.#request();
|
|
154
|
+
const request = retryRequest ?? this.#request();
|
|
144
155
|
let page;
|
|
145
156
|
try {
|
|
146
157
|
page = await readShapePage(this.#transport, this.#params, request, signal);
|
|
@@ -148,16 +159,28 @@ export class V1ShapeSubscription {
|
|
|
148
159
|
// Shape reads are replay-safe GETs: a lost connection keeps state and resumes at the same position.
|
|
149
160
|
const lost = error instanceof RequestTransportError || error instanceof BodyTransportError;
|
|
150
161
|
if (lost || (error instanceof TerraBaseV1Error && isRetryable(error))) {
|
|
162
|
+
retryRequest = request;
|
|
163
|
+
if (this.#upToDate) { this.#upToDate = false; this.#onUpdate?.(this); }
|
|
151
164
|
retries = Math.min(retries + 1, maximumBackoffAttempt);
|
|
152
165
|
await sleep(retryDelay(retries, lost ? undefined : error.retryAfter), signal);
|
|
153
166
|
continue;
|
|
154
167
|
}
|
|
168
|
+
retryRequest = undefined;
|
|
155
169
|
if (!(error instanceof TerraBaseV1Error)) throw error;
|
|
170
|
+
if (error.status === 401 || error.status === 403) {
|
|
171
|
+
// Denial takes precedence over callback/storage failures. A stuck store
|
|
172
|
+
// cannot postpone the terminal auth error after local state is cleared.
|
|
173
|
+
try { this.#reset(); } catch { /* preserve the authorization failure */ }
|
|
174
|
+
try { void Promise.resolve(this.#replica?.save(undefined)).catch(() => {}); }
|
|
175
|
+
catch { /* a synchronous store failure must not mask denial either */ }
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
156
178
|
if (error.status !== 409 || error.code !== "must_refetch") throw error;
|
|
157
179
|
this.#recover(request, error);
|
|
158
|
-
if (this.#handle === undefined) await this.#replica?.save(undefined);
|
|
180
|
+
if (this.#handle === undefined) await waitForStore(this.#replica?.save(undefined), signal);
|
|
159
181
|
continue;
|
|
160
182
|
}
|
|
183
|
+
retryRequest = undefined;
|
|
161
184
|
retries = 0;
|
|
162
185
|
this.#commit(page);
|
|
163
186
|
await this.#persist(signal);
|
|
@@ -212,7 +235,8 @@ export class V1ShapeSubscription {
|
|
|
212
235
|
}
|
|
213
236
|
/** @param {AbortSignal} signal */
|
|
214
237
|
async #restore(signal) {
|
|
215
|
-
|
|
238
|
+
signal.throwIfAborted();
|
|
239
|
+
const text = await waitForStore(this.#replica?.load(), signal);
|
|
216
240
|
signal.throwIfAborted();
|
|
217
241
|
if (text === undefined) return;
|
|
218
242
|
/** @type {any} */
|
|
@@ -236,8 +260,9 @@ export class V1ShapeSubscription {
|
|
|
236
260
|
async #persist(signal) {
|
|
237
261
|
// Snapshot pages are not resumable positions; keep the previous replica until the snapshot completes.
|
|
238
262
|
if (this.#replica === undefined || this.#handle === undefined || this.#offset.startsWith("snap.")) return;
|
|
239
|
-
|
|
240
|
-
|
|
263
|
+
signal.throwIfAborted();
|
|
264
|
+
await waitForStore(this.#replica.save(stringifyExactJson({ format: 1, scope: this.#scope, handle: this.#handle, offset: this.#offset,
|
|
265
|
+
cursor: this.#cursor ?? null, resume: this.#resume ?? null, documents: [...this.#documents] }, "shape replica")), signal);
|
|
241
266
|
signal.throwIfAborted();
|
|
242
267
|
}
|
|
243
268
|
/** @param {import("./shapes.js").V1ShapePage} page */
|
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 })) });
|
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;
|