@terrascale/sdk 0.0.1 → 0.0.3

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +46 -17
  3. package/package.json +5 -2
  4. package/sdk-v1-route-manifest.json +7593 -1175
  5. package/src/database-codec.js +21 -26
  6. package/src/database-types.js +11 -11
  7. package/src/database-view.js +6 -6
  8. package/src/http.js +26 -5
  9. package/src/ids.js +22 -0
  10. package/src/index.js +2 -1
  11. package/src/local/better-auth.js +17 -6
  12. package/src/local/index.js +2 -2
  13. package/src/local/legacy-client.js +5 -5
  14. package/src/local/router.js +12 -12
  15. package/src/local/store.js +29 -28
  16. package/src/local/test-environment.js +3 -2
  17. package/src/postgres.js +5 -4
  18. package/src/react/core.js +2 -2
  19. package/src/ts-auth-discovery.js +17 -0
  20. package/src/ts-auth-ids.js +122 -0
  21. package/src/ts-auth.js +75 -72
  22. package/src/v1/decode.js +30 -10
  23. package/src/v1/ids.js +10 -62
  24. package/src/v1/routes.js +5 -5
  25. package/src/v1/shapes.js +6 -7
  26. package/src/v1/terrabase-v1.js +24 -16
  27. package/src/v1/transport.js +6 -4
  28. package/types/database-codec.d.ts +1 -6
  29. package/types/database-types.d.ts +11 -11
  30. package/types/http.d.ts +1 -0
  31. package/types/ids.d.ts +10 -0
  32. package/types/index.d.ts +2 -1
  33. package/types/local/store.d.ts +10 -0
  34. package/types/local/test-environment.d.ts +2 -2
  35. package/types/postgres.d.ts +4 -4
  36. package/types/ts-auth-discovery.d.ts +8 -0
  37. package/types/ts-auth-ids.d.ts +14 -0
  38. package/types/ts-auth.d.ts +99 -99
  39. package/types/v1/decode.d.ts +27 -11
  40. package/types/v1/ids.d.ts +5 -17
  41. package/types/v1/shapes.d.ts +2 -2
  42. package/types/v1/terrabase-v1.d.ts +3 -3
  43. package/types/v1/transport.d.ts +2 -2
package/src/ts-auth.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { TsAuthHttp } from "./http.js";
2
2
  import { err } from "./result.js";
3
+ import { isValidId, requireId } from "./ids.js";
4
+ import { requireDeadLetterId } from "./ts-auth-ids.js";
3
5
  /** @import { TerraScaleResult } from "./result.js" */
4
6
 
5
7
  /** @typedef {import("./config.js").TsAuthRuntimeConfig} TsAuthRuntimeConfig */
@@ -27,7 +29,7 @@ import { err } from "./result.js";
27
29
 
28
30
  /**
29
31
  * @typedef {{
30
- * readonly id: string;
32
+ * readonly id: import("./ids.js").EntityId;
31
33
  * readonly email: string | null;
32
34
  * readonly email_verified: boolean;
33
35
  * readonly username: string | null;
@@ -43,7 +45,7 @@ import { err } from "./result.js";
43
45
 
44
46
  /**
45
47
  * @typedef {{
46
- * readonly id: string;
48
+ * readonly id: import("./ids.js").EntityId;
47
49
  * readonly status: string;
48
50
  * readonly created_at: number; // Unix seconds.
49
51
  * readonly expires_at: number; // Unix seconds.
@@ -55,7 +57,7 @@ import { err } from "./result.js";
55
57
 
56
58
  /**
57
59
  * @typedef {{
58
- * readonly id: string;
60
+ * readonly id: import("./ids.js").EntityId;
59
61
  * readonly type: string;
60
62
  * readonly confirmed: boolean;
61
63
  * readonly createdAt: number; // Unix seconds.
@@ -64,7 +66,7 @@ import { err } from "./result.js";
64
66
 
65
67
  /**
66
68
  * @typedef {{
67
- * readonly id: string;
69
+ * readonly id: import("./ids.js").EntityId;
68
70
  * readonly nickname: string | null;
69
71
  * readonly algorithm: string;
70
72
  * readonly createdAt: number; // Unix seconds.
@@ -74,7 +76,7 @@ import { err } from "./result.js";
74
76
 
75
77
  /**
76
78
  * @typedef {{
77
- * readonly id: string;
79
+ * readonly id: import("./ids.js").EntityId;
78
80
  * readonly name: string;
79
81
  * readonly slug: string;
80
82
  * readonly created_at: number; // Unix seconds.
@@ -83,7 +85,7 @@ import { err } from "./result.js";
83
85
 
84
86
  /**
85
87
  * @typedef {{
86
- * readonly id: string;
88
+ * readonly id: import("./ids.js").EntityId;
87
89
  * readonly role: string;
88
90
  * readonly created_at: number; // Unix seconds.
89
91
  * readonly organization: TsAuthOrganization | null;
@@ -320,7 +322,7 @@ export class TsAuthUsersClient {
320
322
  ...(options.limit === undefined ? {} : { limit: options.limit }),
321
323
  ...(options.startingAfter === undefined
322
324
  ? {}
323
- : { starting_after: options.startingAfter }),
325
+ : { starting_after: requireId(options.startingAfter, "startingAfter") }),
324
326
  },
325
327
  ...(options.signal === undefined ? {} : { signal: options.signal }),
326
328
  });
@@ -586,7 +588,7 @@ export class TsAuthSessionsClient {
586
588
 
587
589
  /**
588
590
  * @typedef {{
589
- * readonly id: string;
591
+ * readonly id: import("./ids.js").EntityId;
590
592
  * readonly name: string;
591
593
  * readonly api_key: string; // One-time full sk_ bearer; only the creating call returns it.
592
594
  * readonly scopes: readonly string[];
@@ -595,7 +597,7 @@ export class TsAuthSessionsClient {
595
597
 
596
598
  /**
597
599
  * @typedef {{
598
- * readonly id: string;
600
+ * readonly id: import("./ids.js").EntityId;
599
601
  * readonly name: string;
600
602
  * readonly scopes: readonly string[];
601
603
  * readonly created_at: number; // Unix seconds.
@@ -674,9 +676,9 @@ export class TsAuthApiKeysClient {
674
676
 
675
677
  /**
676
678
  * @typedef {{
677
- * readonly id: string;
678
- * readonly organization_id: string;
679
- * readonly user_id: string;
679
+ * readonly id: import("./ids.js").EntityId;
680
+ * readonly organization_id: import("./ids.js").EntityId;
681
+ * readonly user_id: import("./ids.js").EntityId;
680
682
  * readonly role: string;
681
683
  * readonly created_at: number; // Unix seconds.
682
684
  * }} TsAuthMembership
@@ -685,7 +687,7 @@ export class TsAuthApiKeysClient {
685
687
  /**
686
688
  * @typedef {TsAuthMembership & {
687
689
  * readonly user: {
688
- * readonly id: string;
690
+ * readonly id: import("./ids.js").EntityId;
689
691
  * readonly email: string;
690
692
  * readonly name: string;
691
693
  * } | null;
@@ -694,8 +696,8 @@ export class TsAuthApiKeysClient {
694
696
 
695
697
  /**
696
698
  * @typedef {{
697
- * readonly id: string;
698
- * readonly organization_id: string;
699
+ * readonly id: import("./ids.js").EntityId;
700
+ * readonly organization_id: import("./ids.js").EntityId;
699
701
  * readonly email: string;
700
702
  * readonly role: string;
701
703
  * readonly status: string;
@@ -706,14 +708,14 @@ export class TsAuthApiKeysClient {
706
708
 
707
709
  /**
708
710
  * @typedef {{
709
- * readonly id: string;
710
- * readonly organization_id: string;
711
+ * readonly id: import("./ids.js").EntityId;
712
+ * readonly organization_id: import("./ids.js").EntityId;
711
713
  * readonly domain: string;
712
714
  * readonly created_at: number; // Unix seconds.
713
715
  * }} TsAuthOrganizationDomain
714
716
  */
715
717
 
716
- /** @typedef {{ readonly org_id: string }} TsAuthDomainEnrollment */
718
+ /** @typedef {{ readonly org_id: import("./ids.js").EntityId }} TsAuthDomainEnrollment */
717
719
 
718
720
  /**
719
721
  * @typedef {{
@@ -730,7 +732,7 @@ export class TsAuthApiKeysClient {
730
732
 
731
733
  /**
732
734
  * @typedef {{
733
- * readonly user_id: string;
735
+ * readonly user_id: import("./ids.js").EntityId;
734
736
  * readonly role: string;
735
737
  * }} TsAuthCreateMembershipRequest
736
738
  */
@@ -750,7 +752,7 @@ export class TsAuthApiKeysClient {
750
752
 
751
753
  /**
752
754
  * @typedef {{
753
- * readonly user_id: string;
755
+ * readonly user_id: import("./ids.js").EntityId;
754
756
  * }} TsAuthEnrollRequest
755
757
  */
756
758
 
@@ -844,7 +846,7 @@ export class TsAuthOrganizationsClient {
844
846
  * @returns {Promise<TerraScaleResult<TsAuthMembership>>}
845
847
  */
846
848
  createMembership(organizationId, request) {
847
- requireText(request?.user_id, "user_id");
849
+ requireId(request?.user_id, "user_id");
848
850
  requireText(request?.role, "role");
849
851
  return this.#http.request({
850
852
  method: "POST",
@@ -982,7 +984,7 @@ export class TsAuthOrganizationsClient {
982
984
  * @returns {Promise<TerraScaleResult<TsAuthDomainEnrollment>>}
983
985
  */
984
986
  enrollDomain(domain, request) {
985
- requireText(request?.user_id, "user_id");
987
+ requireId(request?.user_id, "user_id");
986
988
  return this.#http.request({
987
989
  method: "POST",
988
990
  path: `v1/${this.#slug}/domains/${segment(domain, "domain")}/enroll`,
@@ -1028,8 +1030,8 @@ export class TsAuthOrganizationsClient {
1028
1030
  */
1029
1031
  /**
1030
1032
  * @typedef {{
1031
- * readonly id: string;
1032
- * readonly client_id: string;
1033
+ * readonly id: import("./ids.js").EntityId;
1034
+ * readonly client_id: import("./ids.js").EntityId;
1033
1035
  * readonly name: string;
1034
1036
  * readonly redirect_uris: readonly string[];
1035
1037
  * readonly post_logout_redirect_uris: readonly string[];
@@ -1146,7 +1148,7 @@ export class TsAuthOauthClientsClient {
1146
1148
  /**
1147
1149
  * @typedef {{
1148
1150
  * readonly audience: string;
1149
- * readonly allowed_client_ids: readonly string[];
1151
+ * readonly allowed_client_ids: readonly import("./ids.js").EntityId[];
1150
1152
  * readonly allowed_grant_types: readonly TsAuthOauthGrantType[];
1151
1153
  * readonly allowed_scopes: readonly string[];
1152
1154
  * }} TsAuthCreateOauthResourceRequest
@@ -1154,7 +1156,7 @@ export class TsAuthOauthClientsClient {
1154
1156
  /**
1155
1157
  * @typedef {{
1156
1158
  * readonly expected_auth_epoch: number;
1157
- * readonly allowed_client_ids?: readonly string[] | undefined;
1159
+ * readonly allowed_client_ids?: readonly import("./ids.js").EntityId[] | undefined;
1158
1160
  * readonly allowed_grant_types?: readonly TsAuthOauthGrantType[] | undefined;
1159
1161
  * readonly allowed_scopes?: readonly string[] | undefined;
1160
1162
  * readonly enabled?: boolean | undefined;
@@ -1163,8 +1165,8 @@ export class TsAuthOauthClientsClient {
1163
1165
  */
1164
1166
  /**
1165
1167
  * @typedef {TsAuthCreateOauthResourceRequest & {
1166
- * readonly id: string;
1167
- * readonly introspection_client_id: string;
1168
+ * readonly id: import("./ids.js").EntityId;
1169
+ * readonly introspection_client_id: import("./ids.js").EntityId;
1168
1170
  * readonly token_endpoint_auth_method: "client_secret_basic";
1169
1171
  * readonly auth_epoch: number;
1170
1172
  * readonly enabled: boolean;
@@ -1265,11 +1267,11 @@ export class TsAuthOauthResourcesClient {
1265
1267
 
1266
1268
  /**
1267
1269
  * @typedef {{
1268
- * readonly id: string;
1270
+ * readonly id: import("./ids.js").EntityId;
1269
1271
  * readonly type: "oidc" | "saml";
1270
1272
  * readonly name: string;
1271
1273
  * readonly enabled: boolean;
1272
- * readonly org_id: string | null;
1274
+ * readonly org_id: import("./ids.js").EntityId | null;
1273
1275
  * readonly issuer_url: string | null;
1274
1276
  * readonly client_id: string | null;
1275
1277
  * readonly authorization_endpoint: string | null;
@@ -1342,7 +1344,7 @@ export class TsAuthConnectionsClient {
1342
1344
  requireText(request?.name, "name");
1343
1345
  if (request.type === "oidc") {
1344
1346
  requireAbsoluteHttpUrl(request.issuer_url, "issuer_url");
1345
- requireText(request.client_id, "client_id");
1347
+ requireText(request.client_id, "client_id"); // This client belongs to the external IdP.
1346
1348
  requireText(request.client_secret, "client_secret");
1347
1349
  } else {
1348
1350
  requireText(request.idp_entity_id, "idp_entity_id");
@@ -1399,16 +1401,16 @@ export class TsAuthConnectionsClient {
1399
1401
 
1400
1402
  /**
1401
1403
  * @typedef {{
1402
- * readonly id: string;
1403
- * readonly org_id: string | null;
1404
+ * readonly id: import("./ids.js").EntityId;
1405
+ * readonly org_id: import("./ids.js").EntityId | null;
1404
1406
  * readonly token: string; // One-time SCIM bearer; only create and rotate return it.
1405
1407
  * }} TsAuthDirectoryWithToken
1406
1408
  */
1407
1409
 
1408
1410
  /**
1409
1411
  * @typedef {{
1410
- * readonly id: string;
1411
- * readonly org_id: string | null;
1412
+ * readonly id: import("./ids.js").EntityId;
1413
+ * readonly org_id: import("./ids.js").EntityId | null;
1412
1414
  * readonly created_at: number; // Unix seconds.
1413
1415
  * }} TsAuthDirectory
1414
1416
  */
@@ -1439,7 +1441,7 @@ export class TsAuthDirectoriesClient {
1439
1441
  * @returns {Promise<TerraScaleResult<TsAuthDirectoryWithToken>>}
1440
1442
  */
1441
1443
  create(request = {}) {
1442
- if (request.org_id !== undefined) requireText(request.org_id, "org_id");
1444
+ if (request.org_id !== undefined) requireId(request.org_id, "org_id");
1443
1445
  return this.#http.request({
1444
1446
  method: "POST",
1445
1447
  path: `v1/${this.#slug}/directories`,
@@ -1488,7 +1490,7 @@ export class TsAuthDirectoriesClient {
1488
1490
 
1489
1491
  /**
1490
1492
  * @typedef {{
1491
- * readonly id: string;
1493
+ * readonly id: import("./ids.js").EntityId;
1492
1494
  * readonly name: string;
1493
1495
  * readonly claims: Record<string, string>;
1494
1496
  * readonly lifetime_seconds: number;
@@ -1600,12 +1602,12 @@ export class TsAuthJwtTemplatesClient {
1600
1602
 
1601
1603
  /**
1602
1604
  * @param {string} name
1603
- * @param {{ readonly user_id: string }} request
1605
+ * @param {{ readonly user_id: import("./ids.js").EntityId }} request
1604
1606
  * @returns {Promise<TerraScaleResult<TsAuthJwtTokenResponse>>}
1605
1607
  */
1606
1608
  issueToken(name, request) {
1607
1609
  validateJwtTemplateName(name);
1608
- requireText(request?.user_id, "user_id");
1610
+ requireId(request?.user_id, "user_id");
1609
1611
  return this.#http.request({
1610
1612
  method: "POST",
1611
1613
  path: `v1/${this.#slug}/jwt-templates/${segment(name, "template name")}/tokens`,
@@ -1619,7 +1621,7 @@ export class TsAuthJwtTemplatesClient {
1619
1621
 
1620
1622
  /**
1621
1623
  * @typedef {{
1622
- * readonly id: string;
1624
+ * readonly id: import("./ids.js").EntityId;
1623
1625
  * readonly url: string;
1624
1626
  * readonly events: readonly string[];
1625
1627
  * readonly signing_secret: string; // One-time HMAC key for verifying deliveries; only create returns it.
@@ -1628,7 +1630,7 @@ export class TsAuthJwtTemplatesClient {
1628
1630
 
1629
1631
  /**
1630
1632
  * @typedef {{
1631
- * readonly id: string;
1633
+ * readonly id: import("./ids.js").EntityId;
1632
1634
  * readonly url: string;
1633
1635
  * readonly events: readonly string[];
1634
1636
  * readonly enabled: boolean;
@@ -1638,9 +1640,9 @@ export class TsAuthJwtTemplatesClient {
1638
1640
 
1639
1641
  /**
1640
1642
  * @typedef {{
1641
- * readonly id: string; // endpointId:eventId
1642
- * readonly endpoint_id: string;
1643
- * readonly event_id: string;
1643
+ * readonly id: `${import("./ids.js").EntityId}:${import("./ids.js").EntityId}`; // endpointId:eventId delivery selector.
1644
+ * readonly endpoint_id: import("./ids.js").EntityId;
1645
+ * readonly event_id: import("./ids.js").EntityId;
1644
1646
  * readonly attempts: number;
1645
1647
  * readonly last_error?: string | undefined;
1646
1648
  * readonly updated_at: number; // Unix seconds.
@@ -1741,7 +1743,7 @@ export class TsAuthWebhooksClient {
1741
1743
  replayDeadLetter(entryId) {
1742
1744
  return this.#http.request({
1743
1745
  method: "POST",
1744
- path: `v1/${this.#slug}/webhooks/dead-letter/${segment(entryId, "dead-letter id")}/replay`,
1746
+ path: `v1/${this.#slug}/webhooks/dead-letter/${encodeURIComponent(requireDeadLetterId(entryId))}/replay`,
1745
1747
  routeName: "tsauth.webhooks.replayDeadLetter",
1746
1748
  });
1747
1749
  }
@@ -1751,10 +1753,10 @@ export class TsAuthWebhooksClient {
1751
1753
 
1752
1754
  /**
1753
1755
  * @typedef {{
1754
- * readonly id: string;
1755
- * readonly instance_id: string;
1756
+ * readonly id: import("./ids.js").EntityId;
1757
+ * readonly instance_id: import("./ids.js").EntityId;
1756
1758
  * readonly type: string;
1757
- * readonly actor_id?: string | undefined; // Omitted from the wire when null.
1759
+ * readonly actor_id?: import("./ids.js").EntityId | undefined; // Omitted from the wire when null.
1758
1760
  * readonly data: Record<string, unknown>;
1759
1761
  * readonly created_at: number; // Unix seconds.
1760
1762
  * readonly integrity_version: number;
@@ -1918,7 +1920,7 @@ export class TsAuthBillingClient {
1918
1920
 
1919
1921
  /**
1920
1922
  * @typedef {{
1921
- * readonly id: string;
1923
+ * readonly id: import("./ids.js").EntityId;
1922
1924
  * readonly slug: string;
1923
1925
  * readonly name: string;
1924
1926
  * readonly logo_url?: string | null | undefined;
@@ -2033,7 +2035,7 @@ export class TsAuthInstanceClient {
2033
2035
 
2034
2036
  /**
2035
2037
  * @typedef {{
2036
- * readonly client_id: string;
2038
+ * readonly client_id: import("./ids.js").EntityId;
2037
2039
  * readonly redirect_uri: string;
2038
2040
  * readonly response_type: "code";
2039
2041
  * readonly scope?: string | undefined;
@@ -2108,20 +2110,20 @@ export class TsAuthInstanceClient {
2108
2110
  * readonly active: true;
2109
2111
  * readonly token_type: "Bearer";
2110
2112
  * readonly iss: string;
2111
- * readonly sub: string;
2113
+ * readonly sub: import("./ids.js").EntityId;
2112
2114
  * readonly aud: string;
2113
- * readonly client_id: string;
2115
+ * readonly client_id: import("./ids.js").EntityId;
2114
2116
  * readonly scope: string;
2115
2117
  * readonly exp: number;
2116
2118
  * readonly iat: number;
2117
- * readonly sid: string | null;
2119
+ * readonly sid: import("./ids.js").EntityId | null;
2118
2120
  * readonly principal_kind: "user" | "service";
2119
2121
  * }} TsAuthOidcAccessIntrospection
2120
2122
  */
2121
2123
  /**
2122
2124
  * @typedef {Omit<TsAuthOidcAccessIntrospection, "token_type" | "principal_kind"> & {
2123
2125
  * readonly token_type: "refresh_token";
2124
- * readonly family_id: string;
2126
+ * readonly family_id: import("./ids.js").EntityId;
2125
2127
  * }} TsAuthOidcRefreshIntrospection
2126
2128
  */
2127
2129
  /**
@@ -2132,7 +2134,7 @@ export class TsAuthInstanceClient {
2132
2134
  /**
2133
2135
  * @typedef {Omit<TsAuthOidcAccessIntrospection, "sid" | "principal_kind"> &
2134
2136
  * (
2135
- * | { readonly principal_kind: "user"; readonly sid: string }
2137
+ * | { readonly principal_kind: "user"; readonly sid: import("./ids.js").EntityId }
2136
2138
  * | { readonly principal_kind: "service"; readonly sid: null }
2137
2139
  * )} TsAuthOidcResourceAccessIntrospection
2138
2140
  */
@@ -2144,14 +2146,14 @@ export class TsAuthInstanceClient {
2144
2146
  * @typedef {{
2145
2147
  * readonly issuer: string; // Configured canonical provider issuer, independently trusted by the caller.
2146
2148
  * readonly audience: string; // Exact registered resource URI; never derived from the response.
2147
- * readonly introspection_client_id: string;
2149
+ * readonly introspection_client_id: import("./ids.js").EntityId;
2148
2150
  * readonly introspection_secret: string;
2149
2151
  * }} TsAuthOidcResourceCredentials
2150
2152
  */
2151
2153
 
2152
2154
  /**
2153
2155
  * @typedef {{
2154
- * readonly sub: string;
2156
+ * readonly sub: import("./ids.js").EntityId;
2155
2157
  * readonly email?: string | undefined;
2156
2158
  * readonly email_verified?: boolean | undefined;
2157
2159
  * readonly name?: string | undefined;
@@ -2358,6 +2360,7 @@ export class TsAuthOidcClient {
2358
2360
  routeName: "tsauth.oidc.resourceIntrospect",
2359
2361
  formBody: form,
2360
2362
  resourceIntrospectionBasic: basic,
2363
+ issuer: expectedIssuer,
2361
2364
  }),
2362
2365
  /** @returns {value is TsAuthOidcResourceIntrospection} */
2363
2366
  (value) => {
@@ -2725,6 +2728,7 @@ const utf8Encoder = new TextEncoder();
2725
2728
  * @returns {string}
2726
2729
  */
2727
2730
  function segment(value, name) {
2731
+ if (name.endsWith(" id")) return requireId(value, name);
2728
2732
  if (
2729
2733
  typeof value !== "string" ||
2730
2734
  value.length === 0 ||
@@ -2839,7 +2843,7 @@ function validateJwtTemplateClaims(claims) {
2839
2843
  */
2840
2844
  function clientCredentialForm(clientId, clientSecret) {
2841
2845
  return new URLSearchParams({
2842
- client_id: requireText(clientId, "clientId"),
2846
+ client_id: requireId(clientId, "clientId"),
2843
2847
  client_secret: requireText(clientSecret, "clientSecret"),
2844
2848
  });
2845
2849
  }
@@ -3165,7 +3169,7 @@ function validateOauthResourceRequest(request, update) {
3165
3169
  ]);
3166
3170
  if (!update) requireResourceAudience(request.audience);
3167
3171
  for (const [field, accepts] of /** @type {const} */ ([
3168
- ["allowed_client_ids", oauthText],
3172
+ ["allowed_client_ids", isValidId],
3169
3173
  ["allowed_grant_types", oauthGrant],
3170
3174
  ["allowed_scopes", oauthScopeToken],
3171
3175
  ])) {
@@ -3197,7 +3201,7 @@ function validateAuthorizationRequest(request) {
3197
3201
  "code_challenge_method",
3198
3202
  "prompt",
3199
3203
  ]);
3200
- requireText(request.client_id, "client_id");
3204
+ requireId(request.client_id, "client_id");
3201
3205
  requireText(request.redirect_uri, "redirect_uri");
3202
3206
  if (request.response_type !== "code")
3203
3207
  throw new TypeError("response_type must be code.");
@@ -3226,7 +3230,8 @@ function validateAuthorizationRequest(request) {
3226
3230
  * @returns {string}
3227
3231
  */
3228
3232
  function resourceBasic(clientId, secret) {
3229
- if (!oauthText(clientId) || !oauthText(secret))
3233
+ requireId(clientId, "introspection_client_id");
3234
+ if (!oauthText(secret))
3230
3235
  throw new TypeError(
3231
3236
  "Resource introspection credentials must be nonempty well-formed text.",
3232
3237
  );
@@ -3263,7 +3268,7 @@ const clientViewFields = [
3263
3268
  function isOauthClientView(value) {
3264
3269
  return (
3265
3270
  exactOauthFields(value, clientViewFields) &&
3266
- [value.id, value.client_id, value.name].every(oauthText) &&
3271
+ isValidId(value.id) && isValidId(value.client_id) && oauthText(value.name) &&
3267
3272
  [
3268
3273
  value.redirect_uris,
3269
3274
  value.post_logout_redirect_uris,
@@ -3323,12 +3328,12 @@ const resourceViewFields = [
3323
3328
  function isOauthResourceView(value) {
3324
3329
  return (
3325
3330
  exactOauthFields(value, resourceViewFields) &&
3326
- oauthText(value.id) &&
3331
+ isValidId(value.id) &&
3327
3332
  validResourceAudience(value.audience) &&
3328
- oauthStrings(value.allowed_client_ids) &&
3333
+ Array.isArray(value.allowed_client_ids) && value.allowed_client_ids.every(isValidId) &&
3329
3334
  oauthGrantsList(value.allowed_grant_types) &&
3330
3335
  oauthStrings(value.allowed_scopes) &&
3331
- oauthText(value.introspection_client_id) &&
3336
+ isValidId(value.introspection_client_id) &&
3332
3337
  value.token_endpoint_auth_method === "client_secret_basic" &&
3333
3338
  exactUnsignedInteger(value.auth_epoch) &&
3334
3339
  typeof value.enabled === "boolean" &&
@@ -3441,13 +3446,13 @@ function isAccessIntrospection(value) {
3441
3446
  exactOauthFields(value, [...introspectionFields, "principal_kind"]) &&
3442
3447
  value.active === true &&
3443
3448
  value.token_type === "Bearer" &&
3444
- [value.iss, value.sub, value.aud, value.client_id].every(oauthText) &&
3449
+ [value.iss, value.aud].every(oauthText) && [value.sub, value.client_id].every(isValidId) &&
3445
3450
  typeof value.scope === "string" &&
3446
3451
  exactUnsignedInteger(value.exp) &&
3447
3452
  exactUnsignedInteger(value.iat) &&
3448
3453
  value.exp > value.iat &&
3449
3454
  ((value.principal_kind === "user" &&
3450
- (value.sid === null || oauthText(value.sid))) ||
3455
+ (value.sid === null || isValidId(value.sid))) ||
3451
3456
  (value.principal_kind === "service" &&
3452
3457
  value.sid === null &&
3453
3458
  value.sub === value.client_id))
@@ -3461,7 +3466,7 @@ function isResourceAccessIntrospection(value) {
3461
3466
  return (
3462
3467
  isAccessIntrospection(value) &&
3463
3468
  (value.principal_kind === "user"
3464
- ? oauthText(value.sid)
3469
+ ? isValidId(value.sid)
3465
3470
  : value.sid === null && value.sub === value.client_id)
3466
3471
  );
3467
3472
  }
@@ -3474,14 +3479,12 @@ function isRefreshIntrospection(value) {
3474
3479
  exactOauthFields(value, [...introspectionFields, "family_id"]) &&
3475
3480
  value.active === true &&
3476
3481
  value.token_type === "refresh_token" &&
3477
- [value.iss, value.sub, value.aud, value.client_id, value.family_id].every(
3478
- oauthText,
3479
- ) &&
3482
+ [value.iss, value.aud].every(oauthText) && [value.sub, value.client_id, value.family_id].every(isValidId) &&
3480
3483
  typeof value.scope === "string" &&
3481
3484
  exactUnsignedInteger(value.exp) &&
3482
3485
  exactUnsignedInteger(value.iat) &&
3483
3486
  value.exp > value.iat &&
3484
- (value.sid === null || oauthText(value.sid))
3487
+ (value.sid === null || isValidId(value.sid))
3485
3488
  );
3486
3489
  }
3487
3490
  /**
package/src/v1/decode.js CHANGED
@@ -1,18 +1,24 @@
1
1
  // Typed decoding of TerraBase v1 response bodies.
2
- import { parseU64, requireVersion } from "./ids.js";
2
+ import { parseU64, requireDocumentId, 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 {import("../ids.js").EntityId} id
12
+ * @property {string} version
13
+ * @property {"value" | "conflict" | (string & {})} state
14
+ * @property {JsonObject} document
15
+ * @property {readonly JsonValue[]} [siblings] Live versions and tombstones on a causal conflict.
10
16
  */
11
17
 
12
18
  /**
13
19
  * The result of a document write.
14
20
  * @typedef {object} V1WriteResult
15
- * @property {string} id
21
+ * @property {import("../ids.js").EntityId} id
16
22
  * @property {string} version For a delete, the tombstone's version.
17
23
  * @property {string} offset The database commit offset (decimal u64), usable with shape `waitFor`.
18
24
  * @property {V1Durability} durability
@@ -37,6 +43,13 @@ export function object(value, what) {
37
43
  return isJsonObject(value) ? value : malformed(what);
38
44
  }
39
45
 
46
+ /** @param {JsonValue | undefined} value @returns {JsonObject} */
47
+ export function documentObject(value) {
48
+ const document = object(value, "document body");
49
+ if (Object.hasOwn(document, "id")) malformed("document body: the entity ID belongs in the document key");
50
+ return document;
51
+ }
52
+
40
53
  /**
41
54
  * @param {JsonValue | undefined} value
42
55
  * @param {string} what
@@ -78,7 +91,7 @@ export function array(value, what) {
78
91
  * @returns {V1Durability}
79
92
  */
80
93
  export function durability(value) {
81
- return value === "local_durable" ? value : malformed("durability");
94
+ return string(value, "durability");
82
95
  }
83
96
 
84
97
  /**
@@ -87,10 +100,17 @@ export function durability(value) {
87
100
  */
88
101
  export function decodeDocument(value) {
89
102
  const raw = object(value, "document");
90
- const id = string(raw.id, "document id");
103
+ const id = requireDocumentId(raw.id);
91
104
  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") });
105
+ const state = string(raw.state, "document state");
106
+ const document = documentObject(raw.document);
107
+ if (raw.siblings === undefined) return Object.freeze({ id, version, state, document });
108
+ const siblings = array(raw.siblings, "document siblings");
109
+ for (const sibling of siblings) {
110
+ const entry = object(sibling, "document sibling");
111
+ if (entry.deleted !== true || entry.document !== undefined) documentObject(entry.document);
112
+ }
113
+ return Object.freeze({ id, version, state, document, siblings: Object.freeze(siblings) });
94
114
  }
95
115
 
96
116
  /**
@@ -100,7 +120,7 @@ export function decodeDocument(value) {
100
120
  export function decodeWrite(response) {
101
121
  const raw = object(response.value, "write result");
102
122
  return Object.freeze({
103
- id: string(raw.id, "write id"),
123
+ id: requireDocumentId(raw.id),
104
124
  version: requireVersion(raw.version),
105
125
  offset: u64(raw.offset, "write offset"),
106
126
  durability: durability(raw.durability),