@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 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`). Current source compatibility is pinned at
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), with 25 current
104
- source-backed routes and one retained historical shape route. Current generated
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()` remains compatible with the historical
261
- `6cb9745…` release. Its route is absent from the audited current server source;
262
- do not use it as current producer evidence. SSE changes are a separate database
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. Current generated OpenAPI,
358
- live endpoint availability, and automatic region discovery remain unqualified.
359
- No TerraBase binary is downloaded or run by these tests. The existing historical
360
- `shape()` method keeps its contract. Manifest/object delivery and 302 redirects
361
- are deferred to FR-22; vector `presigned-manifest` is explicitly skipped until its
362
- delivery contract is ready upstream.
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. SDK boundary tests must run the real C# TerraBase
380
- release binary, pinned by immutable version and SHA-256, with an isolated
381
- allocation. A fixture, mocked transport, SQLite SDK runtime, frozen Rust
382
- contract archive or typed build cannot establish producer compatibility.
383
-
384
- The v1 route manifest and producer contract evidence must agree with the C#
385
- release under test. Keep live tests environment-gated; report missing local
386
- configuration as skipped, never as successful integration evidence. Required
387
- release qualification must supply the verified binary and real producer inputs.
388
- Never commit keys, cookies, environment files or real tenant data.
389
-
390
- The immutable release pin is
391
- [`scripts/ci/terrabase-release.json`](scripts/ci/terrabase-release.json): C#
392
- TerraBase 0.1.0, release `ci-6cb97456b9b52a92a61856601cc025c4f2833518-run-16729`,
393
- with binary SHA-256
394
- `cc2db35b18c385c6d58036c6c94e59460b0e12b0d5616da2222713efbe666700`.
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.9",
3
+ "version": "0.0.11",
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",
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
- : (refreshedToken ??
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; shape support is retained for the historical release.
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
- form.set("resource", requireResourceAudience(request.resource));
2293
- form.set("scope", requireOauthScope(request.scope));
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
- this.#postForm(`${this.#slug}/oauth/token`, "tsauth.oidc.token", form),
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
- const text = await this.#replica?.load();
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
- await this.#replica.save(stringifyExactJson({ format: 1, scope: this.#scope, handle: this.#handle, offset: this.#offset,
240
- cursor: this.#cursor ?? null, resume: this.#resume ?? null, documents: [...this.#documents] }, "shape replica"));
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
- const status = record({ status: text, version: text, region: text, storageFormat: text, revision: nullable(text), machine: text });
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 })) });
@@ -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;
@@ -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;