@milaboratories/pl-client 3.7.0 → 3.8.1

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 (79) hide show
  1. package/dist/core/capabilities.cjs +9 -0
  2. package/dist/core/capabilities.cjs.map +1 -0
  3. package/dist/core/capabilities.d.ts +24 -0
  4. package/dist/core/capabilities.d.ts.map +1 -0
  5. package/dist/core/capabilities.js +9 -0
  6. package/dist/core/capabilities.js.map +1 -0
  7. package/dist/core/client.cjs +7 -25
  8. package/dist/core/client.cjs.map +1 -1
  9. package/dist/core/client.d.ts +2 -3
  10. package/dist/core/client.d.ts.map +1 -1
  11. package/dist/core/client.js +7 -25
  12. package/dist/core/client.js.map +1 -1
  13. package/dist/core/ll_client.cjs +153 -7
  14. package/dist/core/ll_client.cjs.map +1 -1
  15. package/dist/core/ll_client.d.ts +26 -0
  16. package/dist/core/ll_client.d.ts.map +1 -1
  17. package/dist/core/ll_client.js +153 -7
  18. package/dist/core/ll_client.js.map +1 -1
  19. package/dist/core/transaction.cjs +4 -2
  20. package/dist/core/transaction.cjs.map +1 -1
  21. package/dist/core/transaction.d.ts.map +1 -1
  22. package/dist/core/transaction.js +4 -2
  23. package/dist/core/transaction.js.map +1 -1
  24. package/dist/core/unauth_client.cjs +33 -1
  25. package/dist/core/unauth_client.cjs.map +1 -1
  26. package/dist/core/unauth_client.d.ts +19 -0
  27. package/dist/core/unauth_client.d.ts.map +1 -1
  28. package/dist/core/unauth_client.js +33 -1
  29. package/dist/core/unauth_client.js.map +1 -1
  30. package/dist/index.cjs +2 -0
  31. package/dist/index.d.ts +2 -1
  32. package/dist/index.js +2 -1
  33. package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.cjs.map +1 -1
  34. package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.js.map +1 -1
  35. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs +1101 -135
  36. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs.map +1 -1
  37. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.cjs +49 -10
  38. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.cjs.map +1 -1
  39. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.d.ts +61 -1
  40. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.d.ts.map +1 -1
  41. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.js +49 -10
  42. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.js.map +1 -1
  43. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts +414 -12
  44. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts.map +1 -1
  45. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js +1101 -135
  46. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js.map +1 -1
  47. package/dist/proto-grpc/google/protobuf/timestamp.cjs.map +1 -1
  48. package/dist/proto-grpc/google/protobuf/timestamp.d.ts +8 -9
  49. package/dist/proto-grpc/google/protobuf/timestamp.d.ts.map +1 -1
  50. package/dist/proto-grpc/google/protobuf/timestamp.js.map +1 -1
  51. package/dist/proto-grpc/google/rpc/code.cjs.map +1 -1
  52. package/dist/proto-grpc/google/rpc/code.js.map +1 -1
  53. package/dist/proto-rest/plapi.d.ts +247 -12
  54. package/dist/proto-rest/plapi.d.ts.map +1 -1
  55. package/dist/util/pl.cjs.map +1 -1
  56. package/dist/util/pl.js.map +1 -1
  57. package/package.json +4 -4
  58. package/src/core/capabilities.ts +26 -0
  59. package/src/core/client.ts +11 -29
  60. package/src/core/ll_client.test.ts +16 -3
  61. package/src/core/ll_client.ts +187 -8
  62. package/src/core/ll_transaction.test.ts +15 -9
  63. package/src/core/transaction.ts +2 -0
  64. package/src/core/unauth_client.ts +42 -3
  65. package/src/core/unauth_client_branch.test.ts +69 -0
  66. package/src/index.ts +1 -0
  67. package/src/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.ts +1 -1
  68. package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.ts +85 -10
  69. package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.ts +1313 -101
  70. package/src/proto-grpc/google/api/http.ts +1 -1
  71. package/src/proto-grpc/google/protobuf/descriptor.ts +7 -240
  72. package/src/proto-grpc/google/protobuf/timestamp.ts +8 -9
  73. package/src/proto-grpc/google/protobuf/wrappers.ts +4 -38
  74. package/src/proto-grpc/google/rpc/code.ts +1 -1
  75. package/src/proto-grpc/google/rpc/error_details.ts +5 -5
  76. package/src/proto-grpc/google/rpc/http.ts +1 -1
  77. package/src/proto-grpc/google/rpc/status.ts +1 -1
  78. package/src/proto-rest/plapi.ts +263 -12
  79. package/src/util/pl.ts +5 -0
@@ -21,6 +21,7 @@ import { parsePlJwt } from "../util/pl";
21
21
  import { type Dispatcher, interceptors } from "undici";
22
22
  import type { Middleware } from "openapi-fetch";
23
23
  import { inferAuthRefreshTime } from "./auth";
24
+ import { hasCapability, type BackendCapability } from "./capabilities";
24
25
  import { defaultHttpDispatcher } from "@milaboratories/pl-http";
25
26
  import type { WireClientProvider, WireClientProviderFactory, WireConnection } from "./wire";
26
27
  import { parseHttpAuth } from "@milaboratories/pl-model-common";
@@ -47,6 +48,19 @@ export interface PlCallOps {
47
48
  abortSignal?: AbortSignal;
48
49
  }
49
50
 
51
+ // Parses leading "<major>.<minor>.<patch>" from a version string like
52
+ // "3.1.1" or "3.1.1-rc1" and returns true if the parsed version is >= target.
53
+ // Returns false for unparseable versions (safer to assume an old backend).
54
+ function isVersionAtLeast(version: string, target: [number, number, number]): boolean {
55
+ const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version);
56
+ if (!match) return false;
57
+ const parsed: [number, number, number] = [Number(match[1]), Number(match[2]), Number(match[3])];
58
+ for (let i = 0; i < 3; i++) {
59
+ if (parsed[i] !== target[i]) return parsed[i] > target[i];
60
+ }
61
+ return true;
62
+ }
63
+
50
64
  class WireClientProviderImpl<Client> implements WireClientProvider<Client> {
51
65
  private client: Client | undefined = undefined;
52
66
 
@@ -78,6 +92,10 @@ export class LLPlClient implements WireClientProviderFactory {
78
92
  /** Threshold after which auth info refresh is required */
79
93
  private refreshTimestamp?: number;
80
94
 
95
+ /** Cached Ping response. Populated by build() before it returns; refreshed by every later ping(). */
96
+ private _serverInfo?: grpcTypes.MaintenanceAPI_Ping_Response;
97
+ private _authMethodsSync?: grpcTypes.AuthAPI_ListMethods_Response;
98
+
81
99
  private _status: PlConnectionStatus = "OK";
82
100
  private readonly statusListener?: PlConnectionStatusListener;
83
101
 
@@ -112,6 +130,15 @@ export class LLPlClient implements WireClientProviderFactory {
112
130
  if (ops.useAutoDetectWireProtocol) {
113
131
  await pl.detectOptimalWireProtocol();
114
132
  }
133
+
134
+ // Guarantee a ping happened so capability-gated paths (login, refresh) can branch synchronously.
135
+ // In the autodetect path the loop's last successful ping already populated _serverInfo via the
136
+ // side-effect in ping(); this fallback covers the path where autodetect is disabled.
137
+ if (!pl._serverInfo) await pl.ping();
138
+
139
+ // Guarantee authMethods happened so client can make weighted decision on which auth method to use.
140
+ if (!pl._authMethodsSync) await pl.authMethods();
141
+
115
142
  return pl;
116
143
  }
117
144
 
@@ -320,9 +347,12 @@ export class LLPlClient implements WireClientProviderFactory {
320
347
  /** null means anonymous connection */
321
348
  public get authUser(): string | null {
322
349
  if (!this.authenticated) throw new Error("Client is not authenticated");
323
- if (this.authInformation?.jwtToken)
350
+ if (this.authInformation?.jwtToken) {
351
+ if (this.hasCapability("auth:v2")) {
352
+ return parsePlJwt(this.authInformation?.jwtToken).sub;
353
+ }
324
354
  return parsePlJwt(this.authInformation?.jwtToken).user.login;
325
- else return null;
355
+ } else return null;
326
356
  }
327
357
 
328
358
  private updateStatus(newStatus: PlConnectionStatus) {
@@ -354,7 +384,10 @@ export class LLPlClient implements WireClientProviderFactory {
354
384
  this.authRefreshInProgress = true;
355
385
  void (async () => {
356
386
  try {
357
- const token = await this.getJwtToken(BigInt(this.conf.authTTLSeconds));
387
+ const ttl = BigInt(this.conf.authTTLSeconds);
388
+ const token = this.hasCapability("auth:v2")
389
+ ? await this.refreshToken({ ttlSeconds: ttl })
390
+ : await this.getJwtToken(ttl);
358
391
  this.authInformation = { jwtToken: token };
359
392
  this.refreshTimestamp = inferAuthRefreshTime(
360
393
  this.authInformation,
@@ -491,10 +524,108 @@ export class LLPlClient implements WireClientProviderFactory {
491
524
  }
492
525
  }
493
526
 
527
+ /** Login via username/password. Returns a fresh JWT. Backend creates a new session per call. */
528
+ public async loginBasic(
529
+ user: string,
530
+ password: string,
531
+ opts: { ttlSeconds?: bigint; role?: AuthAPI_Role } = {},
532
+ ): Promise<string> {
533
+ const cl = this.clientProvider.get();
534
+ const ttl = opts.ttlSeconds ?? BigInt(this.conf.authTTLSeconds);
535
+ const role = opts.role ?? AuthAPI_Role.UNSPECIFIED;
536
+
537
+ if (cl instanceof GrpcPlApiClient) {
538
+ return (
539
+ await cl.login({
540
+ credentials: {
541
+ oneofKind: "basic",
542
+ basic: { login: user, password },
543
+ },
544
+ expiration: { seconds: ttl, nanos: 0 },
545
+ requestedRole: role,
546
+ }).response
547
+ ).token;
548
+ } else {
549
+ const resp = cl.POST("/v1/auth/login", {
550
+ // openapi-typescript generated all body fields as required, but Login.Request
551
+ // has a credentials oneof — only one of `basic`/`token` is sent. Cast around it.
552
+ body: {
553
+ basic: { login: user, password },
554
+ expiration: `${ttl}s`,
555
+ requestedRole: role,
556
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
557
+ } as any,
558
+ });
559
+ return notEmpty((await resp).data, "REST: empty response for login request").token;
560
+ }
561
+ }
562
+
563
+ /** Login via opaque bearer token (controller pre-shared secret, OIDC id-token, etc.).
564
+ * String input is UTF-8 encoded. Returns a fresh Platforma JWT. */
565
+ public async loginWithToken(
566
+ token: Uint8Array | string,
567
+ opts: { ttlSeconds?: bigint; role?: AuthAPI_Role } = {},
568
+ ): Promise<string> {
569
+ const cl = this.clientProvider.get();
570
+ const ttl = opts.ttlSeconds ?? BigInt(this.conf.authTTLSeconds);
571
+ const role = opts.role ?? AuthAPI_Role.UNSPECIFIED;
572
+ const bytes = typeof token === "string" ? Buffer.from(token, "utf8") : token;
573
+
574
+ if (cl instanceof GrpcPlApiClient) {
575
+ return (
576
+ await cl.login({
577
+ credentials: {
578
+ oneofKind: "token",
579
+ token: { token: bytes },
580
+ },
581
+ expiration: { seconds: ttl, nanos: 0 },
582
+ requestedRole: role,
583
+ }).response
584
+ ).token;
585
+ } else {
586
+ const resp = cl.POST("/v1/auth/login", {
587
+ // openapi-typescript marks all body fields as required, but Login.Request has a oneof.
588
+ // REST encodes `bytes` as a base64 string.
589
+ body: {
590
+ token: { token: Buffer.from(bytes).toString("base64") },
591
+ expiration: `${ttl}s`,
592
+ requestedRole: role,
593
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
594
+ } as any,
595
+ });
596
+ return notEmpty((await resp).data, "REST: empty response for login request").token;
597
+ }
598
+ }
599
+
600
+ /** Refresh the current JWT, preserving session id and role. */
601
+ public async refreshToken(opts: { ttlSeconds?: bigint } = {}): Promise<string> {
602
+ const cl = this.clientProvider.get();
603
+ const ttl = opts.ttlSeconds ?? BigInt(this.conf.authTTLSeconds);
604
+ const currentToken = notEmpty(
605
+ this.authInformation?.jwtToken,
606
+ "refreshToken called without a current JWT",
607
+ );
608
+
609
+ if (cl instanceof GrpcPlApiClient) {
610
+ return (
611
+ await cl.refreshToken({
612
+ token: currentToken,
613
+ expiration: { seconds: ttl, nanos: 0 },
614
+ }).response
615
+ ).token;
616
+ } else {
617
+ const resp = cl.POST("/v1/auth/refresh", {
618
+ body: { token: currentToken, expiration: `${ttl}s` },
619
+ });
620
+ return notEmpty((await resp).data, "REST: empty response for refresh request").token;
621
+ }
622
+ }
623
+
494
624
  public async ping(): Promise<grpcTypes.MaintenanceAPI_Ping_Response> {
495
625
  const cl = this.clientProvider.get();
626
+ let resp: grpcTypes.MaintenanceAPI_Ping_Response;
496
627
  if (cl instanceof GrpcPlApiClient) {
497
- return (await cl.ping({})).response;
628
+ resp = (await cl.ping({})).response;
498
629
  } else {
499
630
  // The REST ping response predates the `capabilities` field (proto field 9).
500
631
  // Old servers omit it; treat absence as empty capability list.
@@ -502,12 +633,32 @@ export class LLPlClient implements WireClientProviderFactory {
502
633
  (await cl.GET("/v1/ping")).data,
503
634
  "REST: empty response for ping request",
504
635
  );
505
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
506
- return {
636
+ resp = {
507
637
  ...(pingData as unknown as grpcTypes.MaintenanceAPI_Ping_Response),
638
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
508
639
  capabilities: (pingData as any).capabilities ?? [],
509
640
  };
510
641
  }
642
+ this._serverInfo = resp;
643
+ return resp;
644
+ }
645
+
646
+ /** Cached Ping response. Always populated post-build(); throws if accessed earlier. */
647
+ public get serverInfo(): grpcTypes.MaintenanceAPI_Ping_Response {
648
+ if (!this._serverInfo) {
649
+ throw new Error("LLPlClient.serverInfo accessed before build() completed");
650
+ }
651
+ return this._serverInfo;
652
+ }
653
+
654
+ /** Synchronous capability check against the cached Ping response. */
655
+ public hasCapability(capability: BackendCapability): boolean {
656
+ return hasCapability(this.serverInfo.capabilities, capability);
657
+ }
658
+
659
+ /** True if the backend implements the setDefaultColor TX request. */
660
+ public get supportsSetDefaultColor(): boolean {
661
+ return isVersionAtLeast(this.serverInfo.coreVersion, [3, 3, 0]);
511
662
  }
512
663
 
513
664
  /**
@@ -592,14 +743,42 @@ export class LLPlClient implements WireClientProviderFactory {
592
743
 
593
744
  public async authMethods(): Promise<grpcTypes.AuthAPI_ListMethods_Response> {
594
745
  const cl = this.clientProvider.get();
746
+ let resp: grpcTypes.AuthAPI_ListMethods_Response;
595
747
  if (cl instanceof GrpcPlApiClient) {
596
- return (await cl.authMethods({})).response;
748
+ resp = (await cl.authMethods({})).response;
597
749
  } else {
598
- return notEmpty(
750
+ const wsResponse = notEmpty(
599
751
  (await cl.GET("/v1/auth/methods")).data,
600
752
  "REST: empty response for auth methods request",
601
753
  );
754
+ // OpenAPI schema flattens the protobuf oneof into `{ basic?, token? }`,
755
+ // while protobuf-ts models it as a discriminated union. Reshape per item.
756
+ resp = {
757
+ methods: (wsResponse.methods ?? []).map((m): grpcTypes.AuthAPI_ListMethods_MethodInfo => {
758
+ const base = { id: m.id, description: m.description };
759
+ if (m.basic !== undefined) {
760
+ return { ...base, method: { oneofKind: "basic", basic: m.basic } };
761
+ }
762
+ if (m.token !== undefined) {
763
+ return { ...base, method: { oneofKind: "token", token: m.token } };
764
+ }
765
+ if (m.sso !== undefined) {
766
+ return { ...base, method: { oneofKind: "sso", sso: m.sso } };
767
+ }
768
+ return { ...base, method: { oneofKind: undefined } };
769
+ }),
770
+ };
771
+ }
772
+
773
+ this._authMethodsSync = resp;
774
+ return resp;
775
+ }
776
+
777
+ public get authMethodsSync(): grpcTypes.AuthAPI_ListMethods_Response {
778
+ if (!this._authMethodsSync) {
779
+ throw new Error("LLPlClient.authMethodsSync accessed before build() completed");
602
780
  }
781
+ return this._authMethodsSync;
603
782
  }
604
783
 
605
784
  public async getUserRoot(
@@ -113,11 +113,14 @@ test("check timeout error type (active)", async () => {
113
113
  );
114
114
  expect(openResponse.txOpen.tx?.isValid).toBeTruthy();
115
115
 
116
- // Set default color so resource creation succeeds in strict mode
117
- await tx.send(
118
- { oneofKind: "setDefaultColor", setDefaultColor: { colorProof: rootSig } },
119
- false,
120
- );
116
+ // Set default color so resource creation succeeds in strict mode.
117
+ // Skip on older backends that don't implement the setDefaultColor TX request.
118
+ if (client.supportsSetDefaultColor) {
119
+ await tx.send(
120
+ { oneofKind: "setDefaultColor", setDefaultColor: { colorProof: rootSig } },
121
+ false,
122
+ );
123
+ }
121
124
 
122
125
  const rData = Uint8Array.from([
123
126
  (Math.random() * 256) & 0xff,
@@ -185,10 +188,13 @@ test("check is abort error (active)", async () => {
185
188
  expect(openResponse.txOpen.tx?.isValid).toBeTruthy();
186
189
 
187
190
  // Set default color so resource creation succeeds in strict mode
188
- await tx.send(
189
- { oneofKind: "setDefaultColor", setDefaultColor: { colorProof: rootSig } },
190
- false,
191
- );
191
+ // Skip on older backends that don't implement the setDefaultColor TX request.
192
+ if (client.supportsSetDefaultColor) {
193
+ await tx.send(
194
+ { oneofKind: "setDefaultColor", setDefaultColor: { colorProof: rootSig } },
195
+ false,
196
+ );
197
+ }
192
198
 
193
199
  const rData = Uint8Array.from([
194
200
  Math.random() & 0xff,
@@ -702,6 +702,7 @@ export class PlTransaction {
702
702
  resourceGet: {
703
703
  ...this.toSignedResourceId(rId),
704
704
  loadFields: loadFields,
705
+ showSoftDeletes: false,
705
706
  },
706
707
  },
707
708
  (r) => protoToResource(notEmpty(r.resourceGet.resource)),
@@ -1136,6 +1137,7 @@ export class PlTransaction {
1136
1137
  traverseStopRules: opts?.traverseStopRules,
1137
1138
  includeKv: opts?.includeKv ?? false,
1138
1139
  maxDepth: opts?.maxDepth,
1140
+ showSoftDeletes: false,
1139
1141
  },
1140
1142
  });
1141
1143
 
@@ -6,6 +6,7 @@ import type {
6
6
  import { LLPlClient } from "./ll_client";
7
7
  import { type MiLogger, notEmpty } from "@milaboratories/ts-helpers";
8
8
  import { UnauthenticatedError } from "./errors";
9
+ import type { BackendCapability } from "./capabilities";
9
10
 
10
11
  /** Primarily used for initial authentication (login) */
11
12
  export class UnauthenticatedPlClient {
@@ -35,11 +36,49 @@ export class UnauthenticatedPlClient {
35
36
  return (await this.authMethods()).methods.length > 0;
36
37
  }
37
38
 
39
+ public hasCapability(capability: BackendCapability): boolean {
40
+ return this.ll.hasCapability(capability);
41
+ }
42
+
43
+ /** Classifies the advertised authentication methods by credential scheme.
44
+ * On legacy backends (no auth:v2) the typed oneof is empty; callers fall through to {@link login}
45
+ * which uses the legacy GetJWTToken path. */
46
+ public get supportedAuthSchemes(): { basic: boolean; token: boolean } {
47
+ const result = { basic: false, token: false };
48
+ for (const m of this.ll.authMethodsSync.methods) {
49
+ if (m.method.oneofKind === "basic") result.basic = true;
50
+ else if (m.method.oneofKind === "token") result.token = true;
51
+ }
52
+ return result;
53
+ }
54
+
55
+ /** Login with username+password.
56
+ *
57
+ * On auth:v2 backends the client inspects the advertised AuthMethods:
58
+ * - if basic auth is offered, sends {@link LLPlClient.loginBasic};
59
+ * - if only token auth is offered, treats `password` as an opaque bearer token and
60
+ * sends {@link LLPlClient.loginWithToken} (so deployments configured for static-token
61
+ * auth still log in without the caller switching methods).
62
+ *
63
+ * On legacy backends (no auth:v2) it falls through to GetJWTToken with the Basic header,
64
+ * preserving original behavior. */
38
65
  public async login(user: string, password: string): Promise<AuthInformation> {
39
66
  try {
40
- const token = await this.ll.getJwtToken(BigInt(this.ll.conf.authTTLSeconds), {
41
- authorization: "Basic " + Buffer.from(user + ":" + password).toString("base64"),
42
- });
67
+ let token: string;
68
+ if (this.ll.hasCapability("auth:v2")) {
69
+ const schemes = this.supportedAuthSchemes;
70
+ if (schemes.basic) {
71
+ token = await this.ll.loginBasic(user, password);
72
+ } else if (schemes.token) {
73
+ token = await this.ll.loginWithToken(password);
74
+ } else {
75
+ throw new Error("backend advertises no supported authentication methods");
76
+ }
77
+ } else {
78
+ token = await this.ll.getJwtToken(BigInt(this.ll.conf.authTTLSeconds), {
79
+ authorization: "Basic " + Buffer.from(user + ":" + password).toString("base64"),
80
+ });
81
+ }
43
82
  const jwtToken = notEmpty(token);
44
83
  if (jwtToken === "") throw new Error("empty token");
45
84
  return { jwtToken };
@@ -0,0 +1,69 @@
1
+ import { test, expect, vi } from "vitest";
2
+ import { UnauthenticatedPlClient } from "./unauth_client";
3
+ import type { BackendCapability } from "./capabilities";
4
+
5
+ type Scheme = "basic" | "token" | "none";
6
+
7
+ function makeStub(opts: { hasAuthV2: boolean; scheme?: Scheme }) {
8
+ const scheme = opts.scheme ?? "basic";
9
+ const methods =
10
+ scheme === "basic"
11
+ ? [{ id: "basic", method: { oneofKind: "basic", basic: {} } }]
12
+ : scheme === "token"
13
+ ? [{ id: "token", method: { oneofKind: "token", token: {} } }]
14
+ : [];
15
+ const ll = {
16
+ hasCapability: vi.fn(
17
+ (capability: BackendCapability) => capability === "auth:v2" && opts.hasAuthV2,
18
+ ),
19
+ loginBasic: vi.fn().mockResolvedValue("jwt-from-loginBasic"),
20
+ loginWithToken: vi.fn().mockResolvedValue("jwt-from-loginWithToken"),
21
+ getJwtToken: vi.fn().mockResolvedValue("jwt-from-getJwtToken"),
22
+ authMethodsSync: { methods },
23
+ conf: { authTTLSeconds: 100 },
24
+ };
25
+ const client = Object.assign(Object.create(UnauthenticatedPlClient.prototype) as object, { ll });
26
+ return { client: client as UnauthenticatedPlClient, ll };
27
+ }
28
+
29
+ test("login routes to loginBasic when backend advertises auth:v2 + basic scheme", async () => {
30
+ const { client, ll } = makeStub({ hasAuthV2: true, scheme: "basic" });
31
+
32
+ const info = await client.login("alice", "pw");
33
+
34
+ expect(ll.loginBasic).toHaveBeenCalledWith("alice", "pw");
35
+ expect(ll.loginWithToken).not.toHaveBeenCalled();
36
+ expect(ll.getJwtToken).not.toHaveBeenCalled();
37
+ expect(info.jwtToken).toBe("jwt-from-loginBasic");
38
+ });
39
+
40
+ test("login routes to loginWithToken when backend advertises auth:v2 + token-only scheme", async () => {
41
+ const { client, ll } = makeStub({ hasAuthV2: true, scheme: "token" });
42
+
43
+ const info = await client.login("alice", "opaque-token");
44
+
45
+ expect(ll.loginWithToken).toHaveBeenCalledWith("opaque-token");
46
+ expect(ll.loginBasic).not.toHaveBeenCalled();
47
+ expect(ll.getJwtToken).not.toHaveBeenCalled();
48
+ expect(info.jwtToken).toBe("jwt-from-loginWithToken");
49
+ });
50
+
51
+ test("login falls back to getJwtToken when backend lacks auth:v2", async () => {
52
+ const { client, ll } = makeStub({ hasAuthV2: false });
53
+
54
+ const info = await client.login("alice", "pw");
55
+
56
+ expect(ll.getJwtToken).toHaveBeenCalledWith(BigInt(100), {
57
+ authorization: expect.stringMatching(/^Basic /),
58
+ });
59
+ expect(ll.loginBasic).not.toHaveBeenCalled();
60
+ expect(ll.loginWithToken).not.toHaveBeenCalled();
61
+ expect(info.jwtToken).toBe("jwt-from-getJwtToken");
62
+ });
63
+
64
+ test("hasCapability proxies to underlying LLPlClient", () => {
65
+ const { client, ll } = makeStub({ hasAuthV2: true });
66
+ expect(client.hasCapability("auth:v2")).toBe(true);
67
+ expect(client.hasCapability("treeFilter:v2")).toBe(false);
68
+ expect(ll.hasCapability).toHaveBeenCalledWith("auth:v2");
69
+ });
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ export * from "./core/errors";
8
8
  export * from "./core/default_client";
9
9
  export * from "./core/unauth_client";
10
10
  export * from "./core/auth";
11
+ export * from "./core/capabilities";
11
12
  export * from "./core/final";
12
13
  export * from "./core/tree_filter";
13
14
  export * from "./core/user_resources";
@@ -2,7 +2,7 @@
2
2
  // @generated from protobuf file "github.com/googleapis/googleapis/google/rpc/status.proto" (package "google.rpc", syntax proto3)
3
3
  // tslint:disable
4
4
  //
5
- // Copyright 2026 Google LLC
5
+ // Copyright 2025 Google LLC
6
6
  //
7
7
  // Licensed under the Apache License, Version 2.0 (the "License");
8
8
  // you may not use this file except in compliance with the License.
@@ -25,6 +25,12 @@ import type { AuthAPI_GrantAccess_Response } from "./api";
25
25
  import type { AuthAPI_GrantAccess_Request } from "./api";
26
26
  import type { AuthAPI_GetSessionInfo_Response } from "./api";
27
27
  import type { AuthAPI_GetSessionInfo_Request } from "./api";
28
+ import type { AuthAPI_RefreshToken_Response } from "./api";
29
+ import type { AuthAPI_RefreshToken_Request } from "./api";
30
+ import type { AuthAPI_BeginSSOLogin_Response } from "./api";
31
+ import type { AuthAPI_BeginSSOLogin_Request } from "./api";
32
+ import type { AuthAPI_Login_Response } from "./api";
33
+ import type { AuthAPI_Login_Request } from "./api";
28
34
  import type { AuthAPI_GetJWTToken_Response } from "./api";
29
35
  import type { AuthAPI_GetJWTToken_Request } from "./api";
30
36
  import type { AuthAPI_ListMethods_Response } from "./api";
@@ -213,9 +219,39 @@ export interface IPlatformClient {
213
219
  */
214
220
  authMethods(input: AuthAPI_ListMethods_Request, options?: RpcOptions): UnaryCall<AuthAPI_ListMethods_Request, AuthAPI_ListMethods_Response>;
215
221
  /**
222
+ * Deprecated: Use Login for session creation and role transitions,
223
+ * and RefreshToken for token renewal. Backends implementing this API always return
224
+ * codes.Unimplemented. Kept here so clients can still call old backends.
225
+ *
226
+ * @deprecated
216
227
  * @generated from protobuf rpc: GetJWTToken
217
228
  */
218
229
  getJWTToken(input: AuthAPI_GetJWTToken_Request, options?: RpcOptions): UnaryCall<AuthAPI_GetJWTToken_Request, AuthAPI_GetJWTToken_Response>;
230
+ /**
231
+ * Login authenticates with the given credentials and returns a new Platforma JWT.
232
+ * Every Login call creates a new session. Use RefreshToken to renew an existing one.
233
+ * This method is public: no Authorization header is required.
234
+ *
235
+ * @generated from protobuf rpc: Login
236
+ */
237
+ login(input: AuthAPI_Login_Request, options?: RpcOptions): UnaryCall<AuthAPI_Login_Request, AuthAPI_Login_Response>;
238
+ /**
239
+ * BeginSSOLogin returns a fresh one-time nonce that the desktop must place
240
+ * into the OIDC auth-request before redirecting to the IdP. Used by the SSO
241
+ * login flow. This method is public: no Authorization header is required.
242
+ *
243
+ * @generated from protobuf rpc: BeginSSOLogin
244
+ */
245
+ beginSSOLogin(input: AuthAPI_BeginSSOLogin_Request, options?: RpcOptions): UnaryCall<AuthAPI_BeginSSOLogin_Request, AuthAPI_BeginSSOLogin_Response>;
246
+ /**
247
+ * RefreshToken accepts a valid Platforma JWT and re-issues it with the same
248
+ * session ID and role. Only the token expiration may be changed.
249
+ * Workflow-scoped tokens cannot be refreshed; call Login instead.
250
+ * This method is public: no Authorization header is required.
251
+ *
252
+ * @generated from protobuf rpc: RefreshToken
253
+ */
254
+ refreshToken(input: AuthAPI_RefreshToken_Request, options?: RpcOptions): UnaryCall<AuthAPI_RefreshToken_Request, AuthAPI_RefreshToken_Response>;
219
255
  /**
220
256
  * @generated from protobuf rpc: GetSessionInfo
221
257
  */
@@ -479,38 +515,77 @@ export class PlatformClient implements IPlatformClient, ServiceInfo {
479
515
  return stackIntercept<AuthAPI_ListMethods_Request, AuthAPI_ListMethods_Response>("unary", this._transport, method, opt, input);
480
516
  }
481
517
  /**
518
+ * Deprecated: Use Login for session creation and role transitions,
519
+ * and RefreshToken for token renewal. Backends implementing this API always return
520
+ * codes.Unimplemented. Kept here so clients can still call old backends.
521
+ *
522
+ * @deprecated
482
523
  * @generated from protobuf rpc: GetJWTToken
483
524
  */
484
525
  getJWTToken(input: AuthAPI_GetJWTToken_Request, options?: RpcOptions): UnaryCall<AuthAPI_GetJWTToken_Request, AuthAPI_GetJWTToken_Response> {
485
526
  const method = this.methods[23], opt = this._transport.mergeOptions(options);
486
527
  return stackIntercept<AuthAPI_GetJWTToken_Request, AuthAPI_GetJWTToken_Response>("unary", this._transport, method, opt, input);
487
528
  }
529
+ /**
530
+ * Login authenticates with the given credentials and returns a new Platforma JWT.
531
+ * Every Login call creates a new session. Use RefreshToken to renew an existing one.
532
+ * This method is public: no Authorization header is required.
533
+ *
534
+ * @generated from protobuf rpc: Login
535
+ */
536
+ login(input: AuthAPI_Login_Request, options?: RpcOptions): UnaryCall<AuthAPI_Login_Request, AuthAPI_Login_Response> {
537
+ const method = this.methods[24], opt = this._transport.mergeOptions(options);
538
+ return stackIntercept<AuthAPI_Login_Request, AuthAPI_Login_Response>("unary", this._transport, method, opt, input);
539
+ }
540
+ /**
541
+ * BeginSSOLogin returns a fresh one-time nonce that the desktop must place
542
+ * into the OIDC auth-request before redirecting to the IdP. Used by the SSO
543
+ * login flow. This method is public: no Authorization header is required.
544
+ *
545
+ * @generated from protobuf rpc: BeginSSOLogin
546
+ */
547
+ beginSSOLogin(input: AuthAPI_BeginSSOLogin_Request, options?: RpcOptions): UnaryCall<AuthAPI_BeginSSOLogin_Request, AuthAPI_BeginSSOLogin_Response> {
548
+ const method = this.methods[25], opt = this._transport.mergeOptions(options);
549
+ return stackIntercept<AuthAPI_BeginSSOLogin_Request, AuthAPI_BeginSSOLogin_Response>("unary", this._transport, method, opt, input);
550
+ }
551
+ /**
552
+ * RefreshToken accepts a valid Platforma JWT and re-issues it with the same
553
+ * session ID and role. Only the token expiration may be changed.
554
+ * Workflow-scoped tokens cannot be refreshed; call Login instead.
555
+ * This method is public: no Authorization header is required.
556
+ *
557
+ * @generated from protobuf rpc: RefreshToken
558
+ */
559
+ refreshToken(input: AuthAPI_RefreshToken_Request, options?: RpcOptions): UnaryCall<AuthAPI_RefreshToken_Request, AuthAPI_RefreshToken_Response> {
560
+ const method = this.methods[26], opt = this._transport.mergeOptions(options);
561
+ return stackIntercept<AuthAPI_RefreshToken_Request, AuthAPI_RefreshToken_Response>("unary", this._transport, method, opt, input);
562
+ }
488
563
  /**
489
564
  * @generated from protobuf rpc: GetSessionInfo
490
565
  */
491
566
  getSessionInfo(input: AuthAPI_GetSessionInfo_Request, options?: RpcOptions): UnaryCall<AuthAPI_GetSessionInfo_Request, AuthAPI_GetSessionInfo_Response> {
492
- const method = this.methods[24], opt = this._transport.mergeOptions(options);
567
+ const method = this.methods[27], opt = this._transport.mergeOptions(options);
493
568
  return stackIntercept<AuthAPI_GetSessionInfo_Request, AuthAPI_GetSessionInfo_Response>("unary", this._transport, method, opt, input);
494
569
  }
495
570
  /**
496
571
  * @generated from protobuf rpc: GrantAccess
497
572
  */
498
573
  grantAccess(input: AuthAPI_GrantAccess_Request, options?: RpcOptions): UnaryCall<AuthAPI_GrantAccess_Request, AuthAPI_GrantAccess_Response> {
499
- const method = this.methods[25], opt = this._transport.mergeOptions(options);
574
+ const method = this.methods[28], opt = this._transport.mergeOptions(options);
500
575
  return stackIntercept<AuthAPI_GrantAccess_Request, AuthAPI_GrantAccess_Response>("unary", this._transport, method, opt, input);
501
576
  }
502
577
  /**
503
578
  * @generated from protobuf rpc: RevokeAccess
504
579
  */
505
580
  revokeAccess(input: AuthAPI_RevokeAccess_Request, options?: RpcOptions): UnaryCall<AuthAPI_RevokeAccess_Request, AuthAPI_RevokeAccess_Response> {
506
- const method = this.methods[26], opt = this._transport.mergeOptions(options);
581
+ const method = this.methods[29], opt = this._transport.mergeOptions(options);
507
582
  return stackIntercept<AuthAPI_RevokeAccess_Request, AuthAPI_RevokeAccess_Response>("unary", this._transport, method, opt, input);
508
583
  }
509
584
  /**
510
585
  * @generated from protobuf rpc: ListGrants
511
586
  */
512
587
  listGrants(input: AuthAPI_ListGrants_Request, options?: RpcOptions): ServerStreamingCall<AuthAPI_ListGrants_Request, AuthAPI_ListGrants_Response> {
513
- const method = this.methods[27], opt = this._transport.mergeOptions(options);
588
+ const method = this.methods[30], opt = this._transport.mergeOptions(options);
514
589
  return stackIntercept<AuthAPI_ListGrants_Request, AuthAPI_ListGrants_Response>("serverStreaming", this._transport, method, opt, input);
515
590
  }
516
591
  /**
@@ -521,21 +596,21 @@ export class PlatformClient implements IPlatformClient, ServiceInfo {
521
596
  * @generated from protobuf rpc: MintSignature
522
597
  */
523
598
  mintSignature(input: AuthAPI_MintSignature_Request, options?: RpcOptions): UnaryCall<AuthAPI_MintSignature_Request, AuthAPI_MintSignature_Response> {
524
- const method = this.methods[28], opt = this._transport.mergeOptions(options);
599
+ const method = this.methods[31], opt = this._transport.mergeOptions(options);
525
600
  return stackIntercept<AuthAPI_MintSignature_Request, AuthAPI_MintSignature_Response>("unary", this._transport, method, opt, input);
526
601
  }
527
602
  /**
528
603
  * @generated from protobuf rpc: GetUserRoot
529
604
  */
530
605
  getUserRoot(input: AuthAPI_GetUserRoot_Request, options?: RpcOptions): UnaryCall<AuthAPI_GetUserRoot_Request, AuthAPI_GetUserRoot_Response> {
531
- const method = this.methods[29], opt = this._transport.mergeOptions(options);
606
+ const method = this.methods[32], opt = this._transport.mergeOptions(options);
532
607
  return stackIntercept<AuthAPI_GetUserRoot_Request, AuthAPI_GetUserRoot_Response>("unary", this._transport, method, opt, input);
533
608
  }
534
609
  /**
535
610
  * @generated from protobuf rpc: ListUserResources
536
611
  */
537
612
  listUserResources(input: AuthAPI_ListUserResources_Request, options?: RpcOptions): ServerStreamingCall<AuthAPI_ListUserResources_Request, AuthAPI_ListUserResources_Response> {
538
- const method = this.methods[30], opt = this._transport.mergeOptions(options);
613
+ const method = this.methods[33], opt = this._transport.mergeOptions(options);
539
614
  return stackIntercept<AuthAPI_ListUserResources_Request, AuthAPI_ListUserResources_Response>("serverStreaming", this._transport, method, opt, input);
540
615
  }
541
616
  /**
@@ -546,7 +621,7 @@ export class PlatformClient implements IPlatformClient, ServiceInfo {
546
621
  * @generated from protobuf rpc: ListResourceTypes
547
622
  */
548
623
  listResourceTypes(input: MiscAPI_ListResourceTypes_Request, options?: RpcOptions): UnaryCall<MiscAPI_ListResourceTypes_Request, MiscAPI_ListResourceTypes_Response> {
549
- const method = this.methods[31], opt = this._transport.mergeOptions(options);
624
+ const method = this.methods[34], opt = this._transport.mergeOptions(options);
550
625
  return stackIntercept<MiscAPI_ListResourceTypes_Request, MiscAPI_ListResourceTypes_Response>("unary", this._transport, method, opt, input);
551
626
  }
552
627
  /**
@@ -557,14 +632,14 @@ export class PlatformClient implements IPlatformClient, ServiceInfo {
557
632
  * @generated from protobuf rpc: Ping
558
633
  */
559
634
  ping(input: MaintenanceAPI_Ping_Request, options?: RpcOptions): UnaryCall<MaintenanceAPI_Ping_Request, MaintenanceAPI_Ping_Response> {
560
- const method = this.methods[32], opt = this._transport.mergeOptions(options);
635
+ const method = this.methods[35], opt = this._transport.mergeOptions(options);
561
636
  return stackIntercept<MaintenanceAPI_Ping_Request, MaintenanceAPI_Ping_Response>("unary", this._transport, method, opt, input);
562
637
  }
563
638
  /**
564
639
  * @generated from protobuf rpc: License
565
640
  */
566
641
  license(input: MaintenanceAPI_License_Request, options?: RpcOptions): UnaryCall<MaintenanceAPI_License_Request, MaintenanceAPI_License_Response> {
567
- const method = this.methods[33], opt = this._transport.mergeOptions(options);
642
+ const method = this.methods[36], opt = this._transport.mergeOptions(options);
568
643
  return stackIntercept<MaintenanceAPI_License_Request, MaintenanceAPI_License_Response>("unary", this._transport, method, opt, input);
569
644
  }
570
645
  }