@terrascale/sdk 0.0.1 → 0.0.2

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.2 — TS-650
4
+
5
+ TerraBase version tokens are opaque: `tbv1-<32 hex>` and the coming `tbv2-…`
6
+ format are both accepted (any 1-256 visible ASCII characters without quotes, so
7
+ they stay safe in `If-Match`). Responses with new members no longer throw:
8
+ `durability` passes through any label, and documents pass through any `state`,
9
+ including a causal read's `"conflict"` with its `siblings` (the LWW winner stays
10
+ in `document`). Conditional writes and error handling are unchanged.
11
+
3
12
  ## 0.0.1 — TS-498
4
13
 
5
14
  The package moves from the unscoped `terrascale` npm name, which the owner has
package/README.md CHANGED
@@ -79,7 +79,9 @@ The C# producer owns these routes:
79
79
  Collection methods are `get`, `put`, `create`, `patch`, `delete`, `query`,
80
80
  `pages` and `items`. New document IDs are canonical UUIDs; `documentIdFor`
81
81
  generates a stable UUIDv5 from a natural key. Documents return
82
- `{ id, version, state: "value", document }`. Writes return the version and a
82
+ `{ id, version, state: "value", document }`; a causal read can answer
83
+ `state: "conflict"` with `siblings`, and unknown states pass through. Versions
84
+ (`tbv1-…`, `tbv2-…`) are opaque tokens. Writes return the version and a
83
85
  decimal-string commit `offset`; preserve those opaque positions and use
84
86
  `ifMatch` or `ifNoneMatchAny` for conditional writes.
85
87
 
@@ -198,4 +200,4 @@ Hosted OAuth resource requests use one exact registered HTTPS `resource`. `oidc.
198
200
 
199
201
  `oauthClients` and `oauthResources` provide the producer's snake_case policy records and named management operations. Resource PATCH requires `expected_auth_epoch`; client PATCH accepts it. JSON epochs outside JavaScript's exact integer range are rejected. Client PATCH returns a bare client view unless `rotate_secret` is true, when it returns the client and its one-time secret. Resource create/PATCH always return the resource and nullable one-time introspection secret.
200
202
 
201
- Use `oidc.introspectResource` with independently configured `issuer`, exact `audience`, `introspection_client_id`, and `introspection_secret`. It uses resource Basic authentication only, with each credential component form encoded before Base64. An active result requires the exact configured issuer/audience, `Bearer`, integer timestamps valid at the local clock with no default skew, and the required user/service session shape. Inactive results contain only `active: false`. Resource results expose no internal epochs, refresh family, or token-use fields. The provider checks current durable token/session/resource state; an active principal or scope does not grant project ownership or management authority. Product authorization remains at the caller's existing authorization boundary.
203
+ Use `oidc.introspectResource` with independently configured `issuer`, exact `audience`, `introspection_client_id`, and `introspection_secret`. It uses resource Basic authentication only, with each credential component form encoded before Base64. An active result requires the exact configured issuer/audience, `Bearer`, integer timestamps valid at the local clock with no default skew, and the required user/service session shape. Inactive results contain only `active: false`. Resource results expose no internal epochs, refresh family, or token-use fields. The provider checks current durable token/session/resource state; an active principal or scope does not grant project ownership or management authority. Product authorization remains at the caller's existing authorization boundary. Behind the Atlas proxy `apiBaseUrl` `https://auth.terrascale.tech`, ts-auth's issuer is regional, `https://<region>.auth.terrascale.tech/<slug>` (for example `https://gru.auth.terrascale.tech/<slug>`); configure that issuer, not the proxy host. Resource introspection then calls the issuer's regional host directly, because Atlas could rank another region, which is another issuer; `useReverseProxy` keeps it on the proxy.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrascale/sdk",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
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
@@ -1,6 +1,9 @@
1
1
  import { resolveAccessToken, resolveTsAuthRuntimeConfig } from "./config.js";
2
2
  import { err, ok, sanitizeDiagnostic } from "./result.js";
3
- import { TsAuthEndpointDiscovery } from "./ts-auth-discovery.js";
3
+ import {
4
+ regionalIssuerOrigin,
5
+ TsAuthEndpointDiscovery,
6
+ } from "./ts-auth-discovery.js";
4
7
  import { parseDatabaseJson } from "./database-codec.js";
5
8
  /** @import { ResolvedTsAuthRuntimeConfig, TsAuthRuntimeConfig, FetchLike } from "./config.js" */
6
9
  /** @import { TerraScaleError, TerraScaleErrorCategory, TerraScaleResult, TerraScaleValidationIssue } from "./result.js" */
@@ -18,6 +21,7 @@ import { parseDatabaseJson } from "./database-codec.js";
18
21
  * readonly formBody?: URLSearchParams | string | undefined;
19
22
  * readonly userinfoAccessToken?: string | undefined;
20
23
  * readonly resourceIntrospectionBasic?: string | undefined;
24
+ * readonly issuer?: string | undefined; // Behind the Atlas proxy a regional issuer is called on its own host.
21
25
  * readonly signal?: AbortSignal | undefined;
22
26
  * }} TsAuthRequest
23
27
  */
@@ -85,10 +89,17 @@ export class TsAuthHttp {
85
89
  options.body !== undefined
86
90
  ? JSON.stringify(options.body)
87
91
  : options.formBody?.toString();
88
- const endpoints = await withCancellation(
89
- this.#discovery?.endpoints(signal) ?? Promise.resolve(undefined),
90
- signal,
91
- );
92
+ const issuerOrigin =
93
+ this.#discovery === undefined || options.issuer === undefined
94
+ ? undefined
95
+ : regionalIssuerOrigin(options.issuer);
96
+ const endpoints =
97
+ issuerOrigin === undefined
98
+ ? await withCancellation(
99
+ this.#discovery?.endpoints(signal) ?? Promise.resolve(undefined),
100
+ signal,
101
+ )
102
+ : [issuerOrigin];
92
103
  let endpointIndex = 0;
93
104
  let attempt = 0;
94
105
  let refreshed = false;
@@ -3,6 +3,23 @@ const AUTH_DISCOVERY_URL =
3
3
  const MAX_ENDPOINTS = 20;
4
4
  const MAX_BYTES = 65_536;
5
5
 
6
+ /**
7
+ * ts-auth issues from its regional host, https://<region>.auth.terrascale.tech.
8
+ * Behind the Atlas proxy that issuer's region is called directly: Atlas could
9
+ * rank another region, which is another issuer.
10
+ * @param {string} issuer
11
+ * @returns {string | undefined}
12
+ */
13
+ export function regionalIssuerOrigin(issuer) {
14
+ const region =
15
+ /^https:\/\/([a-z0-9][a-z0-9-]{0,15})\.auth\.terrascale\.tech\//u.exec(
16
+ issuer,
17
+ )?.[1];
18
+ return region === undefined
19
+ ? undefined
20
+ : `https://${region}.auth.terrascale.tech`;
21
+ }
22
+
6
23
  /** The public auth routing document, validated before any credentials are sent. */
7
24
  export class TsAuthEndpointDiscovery {
8
25
  /**
package/src/ts-auth.js CHANGED
@@ -2358,6 +2358,7 @@ export class TsAuthOidcClient {
2358
2358
  routeName: "tsauth.oidc.resourceIntrospect",
2359
2359
  formBody: form,
2360
2360
  resourceIntrospectionBasic: basic,
2361
+ issuer: expectedIssuer,
2361
2362
  }),
2362
2363
  /** @returns {value is TsAuthOidcResourceIntrospection} */
2363
2364
  (value) => {
package/src/v1/decode.js CHANGED
@@ -3,10 +3,16 @@ import { parseU64, requireVersion } from "./ids.js";
3
3
  import { isJsonObject } from "./json.js";
4
4
  /** @import { JsonObject, JsonValue } from "./json.js" */
5
5
 
6
- /** @typedef {"local_durable"} V1Durability */
6
+ /** Unknown future labels pass through. @typedef {"local_durable" | (string & {})} V1Durability */
7
7
  /**
8
- * C# single-writer documents always carry a value, never conflict siblings.
9
- * @typedef {{ readonly id: string; readonly version: string; readonly state: "value"; readonly document: JsonObject }} V1Document
8
+ * `document` is the value, or the LWW winner when a causal read answers `"conflict"` with `siblings`.
9
+ * Unknown future states pass through.
10
+ * @typedef {object} V1Document
11
+ * @property {string} id
12
+ * @property {string} version
13
+ * @property {"value" | "conflict" | (string & {})} state
14
+ * @property {JsonObject} document
15
+ * @property {readonly JsonValue[]} [siblings] Every live version, on a causal read with several.
10
16
  */
11
17
 
12
18
  /**
@@ -78,7 +84,7 @@ export function array(value, what) {
78
84
  * @returns {V1Durability}
79
85
  */
80
86
  export function durability(value) {
81
- return value === "local_durable" ? value : malformed("durability");
87
+ return string(value, "durability");
82
88
  }
83
89
 
84
90
  /**
@@ -89,8 +95,10 @@ export function decodeDocument(value) {
89
95
  const raw = object(value, "document");
90
96
  const id = string(raw.id, "document id");
91
97
  const version = requireVersion(raw.version);
92
- if (raw.state !== "value") return malformed("document state");
93
- return Object.freeze({ id, version, state: "value", document: object(raw.document, "document body") });
98
+ const state = string(raw.state, "document state");
99
+ const document = object(raw.document, "document body");
100
+ if (raw.siblings === undefined) return Object.freeze({ id, version, state, document });
101
+ return Object.freeze({ id, version, state, document, siblings: Object.freeze(array(raw.siblings, "document siblings")) });
94
102
  }
95
103
 
96
104
  /**
package/src/v1/ids.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // Identifiers for the TerraBase v1 Documents API (api-v1.md).
2
2
 
3
3
  const namePattern = /^[a-z][a-z0-9_-]{0,62}$/u;
4
- const versionPattern = /^tbv1-[0-9a-f]{32}$/u;
4
+ // Version tokens are opaque (tbv1-<32 hex>, tbv2-...): only RFC 9110 opaque-tag characters, safe to quote in If-Match.
5
+ const versionPattern = /^[\x21\x23-\x7e]{1,256}$/u;
5
6
  const offsetPattern = /^(?:0|[1-9]\d{0,19})$/u;
6
7
  const maximumU64 = (1n << 64n) - 1n;
7
8
 
@@ -49,7 +50,7 @@ export function requireDocumentId(id) {
49
50
  */
50
51
  export function requireVersion(version) {
51
52
  if (typeof version !== "string" || !versionPattern.test(version))
52
- throw new TypeError("TerraBase versions are tbv1- followed by 32 lowercase hex characters.");
53
+ throw new TypeError("TerraBase versions are opaque tokens of 1-256 visible ASCII characters without quotes.");
53
54
  return version;
54
55
  }
55
56
 
package/types/http.d.ts CHANGED
@@ -39,6 +39,7 @@ export type TsAuthRequest<TBody> = {
39
39
  readonly formBody?: URLSearchParams | string | undefined;
40
40
  readonly userinfoAccessToken?: string | undefined;
41
41
  readonly resourceIntrospectionBasic?: string | undefined;
42
+ readonly issuer?: string | undefined;
42
43
  readonly signal?: AbortSignal | undefined;
43
44
  };
44
45
  import type { ResolvedTsAuthRuntimeConfig } from "./config.js";
@@ -1,3 +1,11 @@
1
+ /**
2
+ * ts-auth issues from its regional host, https://<region>.auth.terrascale.tech.
3
+ * Behind the Atlas proxy that issuer's region is called directly: Atlas could
4
+ * rank another region, which is another issuer.
5
+ * @param {string} issuer
6
+ * @returns {string | undefined}
7
+ */
8
+ export function regionalIssuerOrigin(issuer: string): string | undefined;
1
9
  /** The public auth routing document, validated before any credentials are sent. */
2
10
  export class TsAuthEndpointDiscovery {
3
11
  /** @param {typeof globalThis.fetch} fetchHandler */
@@ -1,8 +1,14 @@
1
1
  /** @import { JsonObject, JsonValue } from "./json.js" */
2
- /** @typedef {"local_durable"} V1Durability */
2
+ /** Unknown future labels pass through. @typedef {"local_durable" | (string & {})} V1Durability */
3
3
  /**
4
- * C# single-writer documents always carry a value, never conflict siblings.
5
- * @typedef {{ readonly id: string; readonly version: string; readonly state: "value"; readonly document: JsonObject }} V1Document
4
+ * `document` is the value, or the LWW winner when a causal read answers `"conflict"` with `siblings`.
5
+ * Unknown future states pass through.
6
+ * @typedef {object} V1Document
7
+ * @property {string} id
8
+ * @property {string} version
9
+ * @property {"value" | "conflict" | (string & {})} state
10
+ * @property {JsonObject} document
11
+ * @property {readonly JsonValue[]} [siblings] Every live version, on a causal read with several.
6
12
  */
7
13
  /**
8
14
  * The result of a document write.
@@ -68,15 +74,23 @@ export function decodeWrite(response: {
68
74
  headers: Headers;
69
75
  value: JsonValue | undefined;
70
76
  }): V1WriteResult;
71
- export type V1Durability = "local_durable";
72
77
  /**
73
- * C# single-writer documents always carry a value, never conflict siblings.
78
+ * Unknown future labels pass through.
79
+ */
80
+ export type V1Durability = "local_durable" | (string & {});
81
+ /**
82
+ * `document` is the value, or the LWW winner when a causal read answers `"conflict"` with `siblings`.
83
+ * Unknown future states pass through.
74
84
  */
75
85
  export type V1Document = {
76
- readonly id: string;
77
- readonly version: string;
78
- readonly state: "value";
79
- readonly document: JsonObject;
86
+ id: string;
87
+ version: string;
88
+ state: "value" | "conflict" | (string & {});
89
+ document: JsonObject;
90
+ /**
91
+ * Every live version, on a causal read with several.
92
+ */
93
+ siblings?: readonly JsonValue[];
80
94
  };
81
95
  /**
82
96
  * The result of a document write.