@milaboratories/pl-client 3.14.7 → 3.16.0

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 (114) hide show
  1. package/dist/core/PromiseTracker.d.ts +1 -2
  2. package/dist/core/PromiseTracker.d.ts.map +1 -1
  3. package/dist/core/abstract_stream.d.ts +2 -3
  4. package/dist/core/abstract_stream.d.ts.map +1 -1
  5. package/dist/core/auth.d.ts +2 -4
  6. package/dist/core/auth.d.ts.map +1 -1
  7. package/dist/core/cache.d.ts +1 -3
  8. package/dist/core/cache.d.ts.map +1 -1
  9. package/dist/core/capabilities.d.ts +2 -3
  10. package/dist/core/capabilities.d.ts.map +1 -1
  11. package/dist/core/client.cjs +7 -0
  12. package/dist/core/client.cjs.map +1 -1
  13. package/dist/core/client.d.ts +8 -5
  14. package/dist/core/client.d.ts.map +1 -1
  15. package/dist/core/client.js +7 -0
  16. package/dist/core/client.js.map +1 -1
  17. package/dist/core/config.d.ts +29 -31
  18. package/dist/core/config.d.ts.map +1 -1
  19. package/dist/core/default_client.d.ts +2 -4
  20. package/dist/core/default_client.d.ts.map +1 -1
  21. package/dist/core/driver.d.ts +4 -6
  22. package/dist/core/driver.d.ts.map +1 -1
  23. package/dist/core/errors.d.ts +20 -22
  24. package/dist/core/errors.d.ts.map +1 -1
  25. package/dist/core/final.d.ts +4 -5
  26. package/dist/core/final.d.ts.map +1 -1
  27. package/dist/core/ll_client.cjs +45 -18
  28. package/dist/core/ll_client.cjs.map +1 -1
  29. package/dist/core/ll_client.d.ts +21 -10
  30. package/dist/core/ll_client.d.ts.map +1 -1
  31. package/dist/core/ll_client.js +45 -18
  32. package/dist/core/ll_client.js.map +1 -1
  33. package/dist/core/ll_transaction.d.ts +4 -6
  34. package/dist/core/ll_transaction.d.ts.map +1 -1
  35. package/dist/core/stat.d.ts +2 -3
  36. package/dist/core/stat.d.ts.map +1 -1
  37. package/dist/core/transaction.cjs +2 -1
  38. package/dist/core/transaction.cjs.map +1 -1
  39. package/dist/core/transaction.d.ts +30 -32
  40. package/dist/core/transaction.d.ts.map +1 -1
  41. package/dist/core/transaction.js +2 -1
  42. package/dist/core/transaction.js.map +1 -1
  43. package/dist/core/tree_filter.d.ts +14 -10
  44. package/dist/core/tree_filter.d.ts.map +1 -1
  45. package/dist/core/types.d.ts +54 -55
  46. package/dist/core/types.d.ts.map +1 -1
  47. package/dist/core/unauth_client.cjs +79 -32
  48. package/dist/core/unauth_client.cjs.map +1 -1
  49. package/dist/core/unauth_client.d.ts +53 -20
  50. package/dist/core/unauth_client.d.ts.map +1 -1
  51. package/dist/core/unauth_client.js +79 -32
  52. package/dist/core/unauth_client.js.map +1 -1
  53. package/dist/core/user_resources.cjs +23 -0
  54. package/dist/core/user_resources.cjs.map +1 -1
  55. package/dist/core/user_resources.d.ts +33 -7
  56. package/dist/core/user_resources.d.ts.map +1 -1
  57. package/dist/core/user_resources.js +23 -0
  58. package/dist/core/user_resources.js.map +1 -1
  59. package/dist/core/wire.d.ts +5 -7
  60. package/dist/core/wire.d.ts.map +1 -1
  61. package/dist/helpers/pl.d.ts +32 -34
  62. package/dist/helpers/pl.d.ts.map +1 -1
  63. package/dist/helpers/poll.d.ts +8 -9
  64. package/dist/helpers/poll.d.ts.map +1 -1
  65. package/dist/helpers/tx_helpers.d.ts +2 -4
  66. package/dist/helpers/tx_helpers.d.ts.map +1 -1
  67. package/dist/index.d.ts +3 -3
  68. package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.d.ts +2 -4
  69. package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.d.ts.map +1 -1
  70. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs +310 -15
  71. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs.map +1 -1
  72. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.cjs +19 -3
  73. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.cjs.map +1 -1
  74. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.d.ts +29 -5
  75. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.d.ts.map +1 -1
  76. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.js +19 -3
  77. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.js.map +1 -1
  78. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts +2336 -429
  79. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts.map +1 -1
  80. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js +309 -16
  81. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js.map +1 -1
  82. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.d.ts +51 -30
  83. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.d.ts.map +1 -1
  84. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.d.ts +7 -9
  85. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.d.ts.map +1 -1
  86. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/resource_types.d.ts +6 -8
  87. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/resource_types.d.ts.map +1 -1
  88. package/dist/proto-grpc/google/protobuf/any.d.ts +2 -4
  89. package/dist/proto-grpc/google/protobuf/any.d.ts.map +1 -1
  90. package/dist/proto-grpc/google/protobuf/duration.d.ts +2 -4
  91. package/dist/proto-grpc/google/protobuf/duration.d.ts.map +1 -1
  92. package/dist/proto-grpc/google/protobuf/timestamp.d.ts +2 -4
  93. package/dist/proto-grpc/google/protobuf/timestamp.d.ts.map +1 -1
  94. package/dist/proto-grpc/google/rpc/code.d.ts +1 -2
  95. package/dist/proto-grpc/google/rpc/code.d.ts.map +1 -1
  96. package/dist/proto-rest/index.d.ts +6 -8
  97. package/dist/proto-rest/index.d.ts.map +1 -1
  98. package/dist/proto-rest/plapi.d.ts +248 -123
  99. package/dist/proto-rest/plapi.d.ts.map +1 -1
  100. package/dist/test/tcp-proxy.d.ts +3 -5
  101. package/dist/test/tcp-proxy.d.ts.map +1 -1
  102. package/dist/test/test_config.d.ts +13 -14
  103. package/dist/test/test_config.d.ts.map +1 -1
  104. package/package.json +6 -6
  105. package/src/core/client.ts +9 -0
  106. package/src/core/ll_client.ts +40 -13
  107. package/src/core/transaction.ts +2 -0
  108. package/src/core/unauth_client.test.ts +69 -1
  109. package/src/core/unauth_client.ts +108 -34
  110. package/src/core/unauth_client_branch.test.ts +416 -5
  111. package/src/core/user_resources.ts +39 -0
  112. package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.ts +34 -3
  113. package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.ts +431 -15
  114. package/src/proto-rest/plapi.ts +8 -2
@@ -26,7 +26,9 @@ var UnauthenticatedPlClient = class UnauthenticatedPlClient {
26
26
  }
27
27
  /** Classifies the advertised authentication methods by credential scheme.
28
28
  * On legacy backends (no auth:v2) the typed oneof is empty; callers fall through to {@link login}
29
- * which uses the legacy GetJWTToken path. SSO is surfaced via {@link ssoConfig}, not here. */
29
+ * which uses the legacy GetJWTToken path.
30
+ * @deprecated collapses every basic-kind method into one boolean; use {@link loginMethods} to
31
+ * see each advertised method's own id. `login()` still reads this to choose the credential path. */
30
32
  get supportedAuthSchemes() {
31
33
  const result = {
32
34
  basic: false,
@@ -36,46 +38,87 @@ var UnauthenticatedPlClient = class UnauthenticatedPlClient {
36
38
  else if (m.method.oneofKind === "token") result.token = true;
37
39
  return result;
38
40
  }
41
+ /** Builds the {@link SSOAuthMethod} projection an advertised SSO entry carries, or `undefined`
42
+ * for a flow no client here can drive. Shared by {@link ssoConfig} and {@link loginMethods},
43
+ * which each apply their own rule for an unsupported flow. */
44
+ toSSOAuthMethod(method) {
45
+ if (method.method.oneofKind !== "sso") return void 0;
46
+ const sso = method.method.sso;
47
+ if (sso.flowType !== 0) return void 0;
48
+ return {
49
+ id: method.id,
50
+ title: method.title || method.description || method.id,
51
+ description: method.description,
52
+ issuer: sso.issuer,
53
+ clientId: sso.clientId,
54
+ scopes: sso.scopes,
55
+ resource: sso.resource,
56
+ prompt: sso.prompt,
57
+ redirectPorts: sso.redirectPorts,
58
+ subjectTokenSource: sso.subjectTokenSource,
59
+ userIdClaim: sso.userIdClaim,
60
+ groupsClaim: sso.groupsClaim,
61
+ accessType: sso.accessType || void 0,
62
+ flowType: "public_pkce"
63
+ };
64
+ }
39
65
  /** Projection of the first advertised SSO method, derived from {@link authMethodsSync}.
40
- * v1: at most one SSO method per deployment, so callers do not need to discriminate. */
66
+ * v1: at most one SSO method per deployment, so callers do not need to discriminate.
67
+ * @deprecated surfaces only the first advertised SSO method; use {@link loginMethods} to see
68
+ * every advertised method, of every kind. */
41
69
  ssoConfig() {
42
70
  for (const method of this.ll.authMethodsSync.methods) {
43
71
  if (method.method.oneofKind !== "sso") continue;
44
72
  const sso = method.method.sso;
45
73
  if (sso.flowType !== 0) throw new Error(`ssoConfig: unsupported SSO flow type ${sso.flowType}`);
46
- return {
47
- id: method.id,
48
- description: method.description,
49
- issuer: sso.issuer,
50
- clientId: sso.clientId,
51
- scopes: sso.scopes,
52
- resource: sso.resource,
53
- prompt: sso.prompt,
54
- redirectPorts: sso.redirectPorts,
55
- subjectTokenSource: sso.subjectTokenSource,
56
- userIdClaim: sso.userIdClaim,
57
- groupsClaim: sso.groupsClaim,
58
- accessType: sso.accessType || void 0,
59
- flowType: "public_pkce"
60
- };
74
+ return this.toSSOAuthMethod(method);
75
+ }
76
+ }
77
+ /** Every login method the backend advertises, of every kind, in advertised order, each
78
+ * keeping its own `id`, `title`, `description` and kind. An entry with no usable method arm, or an
79
+ * SSO entry whose flow no client here can drive, is dropped rather than failing the whole
80
+ * list, which is why this accessor skips where {@link ssoConfig} throws. */
81
+ loginMethods() {
82
+ const picked = [];
83
+ for (const method of this.ll.authMethodsSync.methods) switch (method.method.oneofKind) {
84
+ case "basic":
85
+ case "token":
86
+ picked.push({
87
+ kind: method.method.oneofKind,
88
+ id: method.id,
89
+ title: method.title || method.description || method.id,
90
+ description: method.description
91
+ });
92
+ break;
93
+ case "sso": {
94
+ const sso = this.toSSOAuthMethod(method);
95
+ if (sso !== void 0) picked.push({
96
+ kind: "sso",
97
+ ...sso
98
+ });
99
+ break;
100
+ }
101
+ default: break;
61
102
  }
103
+ return picked;
62
104
  }
63
- /** Login with username+password.
105
+ /** Login with username and password.
64
106
  *
65
- * On auth:v2 backends the client inspects the advertised AuthMethods:
66
- * - if basic auth is offered, sends {@link LLPlClient.loginBasic};
67
- * - if only token auth is offered, treats `password` as an opaque bearer token and
68
- * sends {@link LLPlClient.loginWithToken} (so deployments configured for static-token
69
- * auth still log in without the caller switching methods).
107
+ * Routes to {@link LLPlClient.loginBasic} or {@link LLPlClient.loginWithToken} based on the
108
+ * advertised method `idP` names. On legacy backends, uses GetJWTToken with Basic header.
70
109
  *
71
- * On legacy backends (no auth:v2) it falls through to GetJWTToken with the Basic header,
72
- * preserving original behavior. */
73
- async login(user, password) {
110
+ * `idP` names the advertised method. A named basic-kind method routes to the basic branch;
111
+ * a named token-kind method routes to the token branch even when the backend also advertises
112
+ * a basic method — the token wire carries no selector, so the id itself is dropped there and
113
+ * only the branch choice is kept. Omitted, or naming no advertised method, falls back to
114
+ * today's first-match behavior: basic wins over token when both are advertised. */
115
+ async login(user, password, idP) {
74
116
  try {
75
117
  let token;
76
118
  if (this.ll.hasCapability("auth:v2")) {
77
119
  const schemes = this.supportedAuthSchemes;
78
- if (schemes.basic) token = await this.ll.loginBasic(user, password);
120
+ if ((idP === void 0 ? void 0 : this.loginMethods().find((m) => m.id === idP)?.kind) === "token") token = await this.ll.loginWithToken(password);
121
+ else if (schemes.basic) token = idP === void 0 ? await this.ll.loginBasic(user, password) : await this.ll.loginBasic(user, password, { idP });
79
122
  else if (schemes.token) token = await this.ll.loginWithToken(password);
80
123
  else throw new Error("backend advertises no supported authentication methods");
81
124
  } else token = await this.ll.getJwtToken(BigInt(this.ll.conf.authTTLSeconds), { authorization: "Basic " + Buffer.from(user + ":" + password).toString("base64") });
@@ -88,9 +131,11 @@ var UnauthenticatedPlClient = class UnauthenticatedPlClient {
88
131
  }
89
132
  }
90
133
  /** Request fresh server-issued login material. v1 only emits the public-PKCE flow;
91
- * desktop MUST place the returned `nonce` verbatim into the OIDC auth-request. */
92
- async beginSSOLogin() {
93
- const attempt = await this.ll.beginSSOLogin();
134
+ * desktop MUST place the returned `nonce` verbatim into the OIDC auth-request. `idP` names
135
+ * the advertised SSO method to route to; omitted, the backend keeps its current first-match
136
+ * behaviour. */
137
+ async beginSSOLogin(idP) {
138
+ const attempt = idP === void 0 ? await this.ll.beginSSOLogin() : await this.ll.beginSSOLogin(idP);
94
139
  return {
95
140
  flow: "public_pkce",
96
141
  nonce: attempt.nonce,
@@ -98,10 +143,12 @@ var UnauthenticatedPlClient = class UnauthenticatedPlClient {
98
143
  clientSecret: attempt.clientSecret
99
144
  };
100
145
  }
101
- /** Forward the verbatim IdP `/token` response body and receive a Platforma JWT. */
146
+ /** Forward the verbatim IdP `/token` response body and receive a Platforma JWT. `idP` names
147
+ * the advertised SSO method to route to; omitted, the backend keeps its current first-match
148
+ * behaviour. */
102
149
  async loginSSO(payload) {
103
150
  try {
104
- const jwtToken = await this.ll.loginSSO(payload.tokenResponse);
151
+ const jwtToken = payload.idP === void 0 ? await this.ll.loginSSO(payload.tokenResponse) : await this.ll.loginSSO(payload.tokenResponse, payload.idP);
105
152
  if (jwtToken === "") throw new Error("empty token");
106
153
  return { jwtToken };
107
154
  } catch (e) {
@@ -1 +1 @@
1
- {"version":3,"file":"unauth_client.cjs","names":["LLPlClient","UnauthenticatedError"],"sources":["../../src/core/unauth_client.ts"],"sourcesContent":["import type { AuthInformation, PlClientConfig } from \"./config\";\nimport {\n type AuthAPI_ListMethods_Response,\n type MaintenanceAPI_Ping_Response,\n AuthAPI_ListMethods_SSOAuthMethod_FlowType,\n} from \"../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api\";\nimport { LLPlClient } from \"./ll_client\";\nimport { type MiLogger, notEmpty } from \"@milaboratories/ts-helpers\";\nimport { UnauthenticatedError } from \"./errors\";\nimport type { BackendCapability } from \"./capabilities\";\n\n/** Login-flow shape advertised by the backend for a given SSO method. Extension\n * point: future flow variants (e.g. backend-driven) sit alongside `public_pkce`. */\nexport type SSOFlowType = \"public_pkce\";\n\n/** Typed projection of `AuthAPI.ListMethods.SSOAuthMethod` for desktop consumption.\n * Carries everything needed to drive the PKCE flow + diagnostics fields the renderer\n * may surface (`userIdClaim`, `groupsClaim`). */\nexport type SSOAuthMethod = {\n id: string;\n description: string;\n issuer: string;\n clientId: string;\n scopes: string;\n resource: string;\n prompt: string;\n redirectPorts: number[];\n subjectTokenSource: string;\n userIdClaim: string;\n groupsClaim: string;\n flowType: SSOFlowType;\n /** OIDC `access_type` auth-request param (\"online\" | \"offline\"). Google-specific:\n * \"offline\" makes Google issue a refresh token; other IdPs ignore it. Absent when\n * the server does not set it — never send an empty string to the IdP. */\n accessType?: string;\n};\n\n/** Server-issued material returned by {@link UnauthenticatedPlClient.beginSSOLogin}.\n * Discriminated on `flow`; future flow variants extend the union. */\nexport type SSOLoginAttempt = {\n flow: \"public_pkce\";\n nonce: string;\n expiresAt: Date;\n /** Confidential-client secret for the IdP token exchange; absent for public clients.\n * Google's OIDC has no public-client mode — it requires a client_secret at the token\n * endpoint even with PKCE — so for Google the backend forwards its secret here and the\n * desktop exchanges the code as a confidential client. */\n clientSecret?: string;\n};\n\n/** Primarily used for initial authentication (login) */\nexport class UnauthenticatedPlClient {\n public readonly ll: LLPlClient;\n\n private constructor(ll: LLPlClient) {\n this.ll = ll;\n }\n\n public static async build(\n configOrAddress: PlClientConfig | string,\n ops?: { logger?: MiLogger },\n ): Promise<UnauthenticatedPlClient> {\n const ll = await LLPlClient.build(configOrAddress, ops);\n return new UnauthenticatedPlClient(ll);\n }\n\n public async ping(): Promise<MaintenanceAPI_Ping_Response> {\n return await this.ll.ping();\n }\n\n public async authMethods(): Promise<AuthAPI_ListMethods_Response> {\n return await this.ll.authMethods();\n }\n\n public async requireAuth(): Promise<boolean> {\n return (await this.authMethods()).methods.length > 0;\n }\n\n public hasCapability(capability: BackendCapability): boolean {\n return this.ll.hasCapability(capability);\n }\n\n /** Classifies the advertised authentication methods by credential scheme.\n * On legacy backends (no auth:v2) the typed oneof is empty; callers fall through to {@link login}\n * which uses the legacy GetJWTToken path. SSO is surfaced via {@link ssoConfig}, not here. */\n public get supportedAuthSchemes(): { basic: boolean; token: boolean } {\n const result = { basic: false, token: false };\n for (const m of this.ll.authMethodsSync.methods) {\n if (m.method.oneofKind === \"basic\") result.basic = true;\n else if (m.method.oneofKind === \"token\") result.token = true;\n }\n return result;\n }\n\n /** Projection of the first advertised SSO method, derived from {@link authMethodsSync}.\n * v1: at most one SSO method per deployment, so callers do not need to discriminate. */\n public ssoConfig(): SSOAuthMethod | undefined {\n for (const method of this.ll.authMethodsSync.methods) {\n if (method.method.oneofKind !== \"sso\") continue;\n const sso = method.method.sso;\n if (sso.flowType !== AuthAPI_ListMethods_SSOAuthMethod_FlowType.PUBLIC_PKCE) {\n throw new Error(`ssoConfig: unsupported SSO flow type ${sso.flowType}`);\n }\n return {\n id: method.id,\n description: method.description,\n issuer: sso.issuer,\n clientId: sso.clientId,\n scopes: sso.scopes,\n resource: sso.resource,\n prompt: sso.prompt,\n redirectPorts: sso.redirectPorts,\n subjectTokenSource: sso.subjectTokenSource,\n userIdClaim: sso.userIdClaim,\n groupsClaim: sso.groupsClaim,\n accessType: sso.accessType || undefined,\n flowType: \"public_pkce\",\n };\n }\n return undefined;\n }\n\n /** Login with username+password.\n *\n * On auth:v2 backends the client inspects the advertised AuthMethods:\n * - if basic auth is offered, sends {@link LLPlClient.loginBasic};\n * - if only token auth is offered, treats `password` as an opaque bearer token and\n * sends {@link LLPlClient.loginWithToken} (so deployments configured for static-token\n * auth still log in without the caller switching methods).\n *\n * On legacy backends (no auth:v2) it falls through to GetJWTToken with the Basic header,\n * preserving original behavior. */\n public async login(user: string, password: string): Promise<AuthInformation> {\n try {\n let token: string;\n if (this.ll.hasCapability(\"auth:v2\")) {\n const schemes = this.supportedAuthSchemes;\n if (schemes.basic) {\n token = await this.ll.loginBasic(user, password);\n } else if (schemes.token) {\n token = await this.ll.loginWithToken(password);\n } else {\n throw new Error(\"backend advertises no supported authentication methods\");\n }\n } else {\n token = await this.ll.getJwtToken(BigInt(this.ll.conf.authTTLSeconds), {\n authorization: \"Basic \" + Buffer.from(user + \":\" + password).toString(\"base64\"),\n });\n }\n const jwtToken = notEmpty(token);\n if (jwtToken === \"\") throw new Error(\"empty token\");\n return { jwtToken };\n } catch (e: any) {\n if (e.code === \"UNAUTHENTICATED\") throw new UnauthenticatedError(e.message);\n throw new Error(e);\n }\n }\n\n /** Request fresh server-issued login material. v1 only emits the public-PKCE flow;\n * desktop MUST place the returned `nonce` verbatim into the OIDC auth-request. */\n public async beginSSOLogin(): Promise<SSOLoginAttempt> {\n const attempt = await this.ll.beginSSOLogin();\n return {\n flow: \"public_pkce\",\n nonce: attempt.nonce,\n expiresAt: attempt.expiresAt,\n clientSecret: attempt.clientSecret,\n };\n }\n\n /** Forward the verbatim IdP `/token` response body and receive a Platforma JWT. */\n public async loginSSO(payload: { tokenResponse: Uint8Array }): Promise<AuthInformation> {\n try {\n const jwtToken = await this.ll.loginSSO(payload.tokenResponse);\n if (jwtToken === \"\") throw new Error(\"empty token\");\n return { jwtToken };\n } catch (e: any) {\n if (e.code === \"UNAUTHENTICATED\") throw new UnauthenticatedError(e.message);\n throw new Error(e);\n }\n }\n}\n"],"mappings":";;;;;;AAmDA,IAAa,0BAAb,MAAa,wBAAwB;CACnC;CAEA,YAAoB,IAAgB;EAClC,KAAK,KAAK;CACZ;CAEA,aAAoB,MAClB,iBACA,KACkC;EAElC,OAAO,IAAI,wBAAwB,MADlBA,kBAAAA,WAAW,MAAM,iBAAiB,GAAG,CACjB;CACvC;CAEA,MAAa,OAA8C;EACzD,OAAO,MAAM,KAAK,GAAG,KAAK;CAC5B;CAEA,MAAa,cAAqD;EAChE,OAAO,MAAM,KAAK,GAAG,YAAY;CACnC;CAEA,MAAa,cAAgC;EAC3C,QAAQ,MAAM,KAAK,YAAY,EAAA,CAAG,QAAQ,SAAS;CACrD;CAEA,cAAqB,YAAwC;EAC3D,OAAO,KAAK,GAAG,cAAc,UAAU;CACzC;;;;CAKA,IAAW,uBAA2D;EACpE,MAAM,SAAS;GAAE,OAAO;GAAO,OAAO;EAAM;EAC5C,KAAK,MAAM,KAAK,KAAK,GAAG,gBAAgB,SACtC,IAAI,EAAE,OAAO,cAAc,SAAS,OAAO,QAAQ;OAC9C,IAAI,EAAE,OAAO,cAAc,SAAS,OAAO,QAAQ;EAE1D,OAAO;CACT;;;CAIA,YAA8C;EAC5C,KAAK,MAAM,UAAU,KAAK,GAAG,gBAAgB,SAAS;GACpD,IAAI,OAAO,OAAO,cAAc,OAAO;GACvC,MAAM,MAAM,OAAO,OAAO;GAC1B,IAAI,IAAI,aAAA,GACN,MAAM,IAAI,MAAM,wCAAwC,IAAI,UAAU;GAExE,OAAO;IACL,IAAI,OAAO;IACX,aAAa,OAAO;IACpB,QAAQ,IAAI;IACZ,UAAU,IAAI;IACd,QAAQ,IAAI;IACZ,UAAU,IAAI;IACd,QAAQ,IAAI;IACZ,eAAe,IAAI;IACnB,oBAAoB,IAAI;IACxB,aAAa,IAAI;IACjB,aAAa,IAAI;IACjB,YAAY,IAAI,cAAc,KAAA;IAC9B,UAAU;GACZ;EACF;CAEF;;;;;;;;;;;CAYA,MAAa,MAAM,MAAc,UAA4C;EAC3E,IAAI;GACF,IAAI;GACJ,IAAI,KAAK,GAAG,cAAc,SAAS,GAAG;IACpC,MAAM,UAAU,KAAK;IACrB,IAAI,QAAQ,OACV,QAAQ,MAAM,KAAK,GAAG,WAAW,MAAM,QAAQ;SAC1C,IAAI,QAAQ,OACjB,QAAQ,MAAM,KAAK,GAAG,eAAe,QAAQ;SAE7C,MAAM,IAAI,MAAM,wDAAwD;GAE5E,OACE,QAAQ,MAAM,KAAK,GAAG,YAAY,OAAO,KAAK,GAAG,KAAK,cAAc,GAAG,EACrE,eAAe,WAAW,OAAO,KAAK,OAAO,MAAM,QAAQ,CAAC,CAAC,SAAS,QAAQ,EAChF,CAAC;GAEH,MAAM,YAAA,GAAA,2BAAA,SAAA,CAAoB,KAAK;GAC/B,IAAI,aAAa,IAAI,MAAM,IAAI,MAAM,aAAa;GAClD,OAAO,EAAE,SAAS;EACpB,SAAS,GAAQ;GACf,IAAI,EAAE,SAAS,mBAAmB,MAAM,IAAIC,eAAAA,qBAAqB,EAAE,OAAO;GAC1E,MAAM,IAAI,MAAM,CAAC;EACnB;CACF;;;CAIA,MAAa,gBAA0C;EACrD,MAAM,UAAU,MAAM,KAAK,GAAG,cAAc;EAC5C,OAAO;GACL,MAAM;GACN,OAAO,QAAQ;GACf,WAAW,QAAQ;GACnB,cAAc,QAAQ;EACxB;CACF;;CAGA,MAAa,SAAS,SAAkE;EACtF,IAAI;GACF,MAAM,WAAW,MAAM,KAAK,GAAG,SAAS,QAAQ,aAAa;GAC7D,IAAI,aAAa,IAAI,MAAM,IAAI,MAAM,aAAa;GAClD,OAAO,EAAE,SAAS;EACpB,SAAS,GAAQ;GACf,IAAI,EAAE,SAAS,mBAAmB,MAAM,IAAIA,eAAAA,qBAAqB,EAAE,OAAO;GAC1E,MAAM,IAAI,MAAM,CAAC;EACnB;CACF;AACF"}
1
+ {"version":3,"file":"unauth_client.cjs","names":["LLPlClient","UnauthenticatedError"],"sources":["../../src/core/unauth_client.ts"],"sourcesContent":["import type { AuthInformation, PlClientConfig } from \"./config\";\nimport {\n type AuthAPI_ListMethods_Response,\n type MaintenanceAPI_Ping_Response,\n AuthAPI_ListMethods_SSOAuthMethod_FlowType,\n} from \"../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api\";\nimport { LLPlClient } from \"./ll_client\";\nimport { type MiLogger, notEmpty } from \"@milaboratories/ts-helpers\";\nimport { UnauthenticatedError } from \"./errors\";\nimport type { BackendCapability } from \"./capabilities\";\n\n/** Login-flow shape advertised by the backend for a given SSO method. Extension\n * point: future flow variants (e.g. backend-driven) sit alongside `public_pkce`. */\nexport type SSOFlowType = \"public_pkce\";\n\n/** Typed projection of `AuthAPI.ListMethods.SSOAuthMethod` for desktop consumption.\n * Carries everything needed to drive the PKCE flow + diagnostics fields the renderer\n * may surface (`userIdClaim`, `groupsClaim`). */\nexport type SSOAuthMethod = {\n id: string;\n title: string;\n description: string;\n issuer: string;\n clientId: string;\n scopes: string;\n resource: string;\n prompt: string;\n redirectPorts: number[];\n subjectTokenSource: string;\n userIdClaim: string;\n groupsClaim: string;\n flowType: SSOFlowType;\n /** OIDC `access_type` auth-request param (\"online\" | \"offline\"). Google-specific:\n * \"offline\" makes Google issue a refresh token; other IdPs ignore it. Absent when\n * the server does not set it — never send an empty string to the IdP. */\n accessType?: string;\n};\n\n/** Server-issued material returned by {@link UnauthenticatedPlClient.beginSSOLogin}.\n * Discriminated on `flow`; future flow variants extend the union. */\nexport type SSOLoginAttempt = {\n flow: \"public_pkce\";\n nonce: string;\n expiresAt: Date;\n /** Confidential-client secret for the IdP token exchange; absent for public clients.\n * Google's OIDC has no public-client mode — it requires a client_secret at the token\n * endpoint even with PKCE — so for Google the backend forwards its secret here and the\n * desktop exchanges the code as a confidential client. */\n clientSecret?: string;\n};\n\n/** One login method the backend advertises, of any kind. Every advertised method keeps its own\n * `id`, `title`, `description` and kind. Two same-kind methods, such as two LDAP directories, are\n * two entries a caller can name apart and never collapsed into one. See {@link UnauthenticatedPlClient.loginMethods}. */\nexport type LoginMethod =\n | { kind: \"basic\"; id: string; title: string; description: string }\n | { kind: \"token\"; id: string; title: string; description: string }\n | ({ kind: \"sso\" } & SSOAuthMethod);\n\n/** Primarily used for initial authentication (login) */\nexport class UnauthenticatedPlClient {\n public readonly ll: LLPlClient;\n\n private constructor(ll: LLPlClient) {\n this.ll = ll;\n }\n\n public static async build(\n configOrAddress: PlClientConfig | string,\n ops?: { logger?: MiLogger },\n ): Promise<UnauthenticatedPlClient> {\n const ll = await LLPlClient.build(configOrAddress, ops);\n return new UnauthenticatedPlClient(ll);\n }\n\n public async ping(): Promise<MaintenanceAPI_Ping_Response> {\n return await this.ll.ping();\n }\n\n public async authMethods(): Promise<AuthAPI_ListMethods_Response> {\n return await this.ll.authMethods();\n }\n\n public async requireAuth(): Promise<boolean> {\n return (await this.authMethods()).methods.length > 0;\n }\n\n public hasCapability(capability: BackendCapability): boolean {\n return this.ll.hasCapability(capability);\n }\n\n /** Classifies the advertised authentication methods by credential scheme.\n * On legacy backends (no auth:v2) the typed oneof is empty; callers fall through to {@link login}\n * which uses the legacy GetJWTToken path.\n * @deprecated collapses every basic-kind method into one boolean; use {@link loginMethods} to\n * see each advertised method's own id. `login()` still reads this to choose the credential path. */\n public get supportedAuthSchemes(): { basic: boolean; token: boolean } {\n const result = { basic: false, token: false };\n for (const m of this.ll.authMethodsSync.methods) {\n if (m.method.oneofKind === \"basic\") result.basic = true;\n else if (m.method.oneofKind === \"token\") result.token = true;\n }\n return result;\n }\n\n /** Builds the {@link SSOAuthMethod} projection an advertised SSO entry carries, or `undefined`\n * for a flow no client here can drive. Shared by {@link ssoConfig} and {@link loginMethods},\n * which each apply their own rule for an unsupported flow. */\n private toSSOAuthMethod(\n method: AuthAPI_ListMethods_Response[\"methods\"][number],\n ): SSOAuthMethod | undefined {\n if (method.method.oneofKind !== \"sso\") return undefined;\n const sso = method.method.sso;\n if (sso.flowType !== AuthAPI_ListMethods_SSOAuthMethod_FlowType.PUBLIC_PKCE) return undefined;\n return {\n id: method.id,\n title: method.title || method.description || method.id,\n description: method.description,\n issuer: sso.issuer,\n clientId: sso.clientId,\n scopes: sso.scopes,\n resource: sso.resource,\n prompt: sso.prompt,\n redirectPorts: sso.redirectPorts,\n subjectTokenSource: sso.subjectTokenSource,\n userIdClaim: sso.userIdClaim,\n groupsClaim: sso.groupsClaim,\n accessType: sso.accessType || undefined,\n flowType: \"public_pkce\",\n };\n }\n\n /** Projection of the first advertised SSO method, derived from {@link authMethodsSync}.\n * v1: at most one SSO method per deployment, so callers do not need to discriminate.\n * @deprecated surfaces only the first advertised SSO method; use {@link loginMethods} to see\n * every advertised method, of every kind. */\n public ssoConfig(): SSOAuthMethod | undefined {\n for (const method of this.ll.authMethodsSync.methods) {\n if (method.method.oneofKind !== \"sso\") continue;\n const sso = method.method.sso;\n if (sso.flowType !== AuthAPI_ListMethods_SSOAuthMethod_FlowType.PUBLIC_PKCE) {\n throw new Error(`ssoConfig: unsupported SSO flow type ${sso.flowType}`);\n }\n return this.toSSOAuthMethod(method);\n }\n return undefined;\n }\n\n /** Every login method the backend advertises, of every kind, in advertised order, each\n * keeping its own `id`, `title`, `description` and kind. An entry with no usable method arm, or an\n * SSO entry whose flow no client here can drive, is dropped rather than failing the whole\n * list, which is why this accessor skips where {@link ssoConfig} throws. */\n public loginMethods(): LoginMethod[] {\n const picked: LoginMethod[] = [];\n for (const method of this.ll.authMethodsSync.methods) {\n switch (method.method.oneofKind) {\n case \"basic\":\n case \"token\":\n picked.push({\n kind: method.method.oneofKind,\n id: method.id,\n title: method.title || method.description || method.id,\n description: method.description,\n });\n break;\n case \"sso\": {\n const sso = this.toSSOAuthMethod(method);\n if (sso !== undefined) picked.push({ kind: \"sso\", ...sso });\n break;\n }\n default:\n // No arm at all — a legacy backend advertising nothing pickable.\n break;\n }\n }\n return picked;\n }\n\n /** Login with username and password.\n *\n * Routes to {@link LLPlClient.loginBasic} or {@link LLPlClient.loginWithToken} based on the\n * advertised method `idP` names. On legacy backends, uses GetJWTToken with Basic header.\n *\n * `idP` names the advertised method. A named basic-kind method routes to the basic branch;\n * a named token-kind method routes to the token branch even when the backend also advertises\n * a basic method — the token wire carries no selector, so the id itself is dropped there and\n * only the branch choice is kept. Omitted, or naming no advertised method, falls back to\n * today's first-match behavior: basic wins over token when both are advertised. */\n public async login(user: string, password: string, idP?: string): Promise<AuthInformation> {\n try {\n let token: string;\n if (this.ll.hasCapability(\"auth:v2\")) {\n const schemes = this.supportedAuthSchemes;\n const namedKind =\n idP === undefined ? undefined : this.loginMethods().find((m) => m.id === idP)?.kind;\n if (namedKind === \"token\") {\n token = await this.ll.loginWithToken(password);\n } else if (schemes.basic) {\n token =\n idP === undefined\n ? await this.ll.loginBasic(user, password)\n : await this.ll.loginBasic(user, password, { idP });\n } else if (schemes.token) {\n token = await this.ll.loginWithToken(password);\n } else {\n throw new Error(\"backend advertises no supported authentication methods\");\n }\n } else {\n token = await this.ll.getJwtToken(BigInt(this.ll.conf.authTTLSeconds), {\n authorization: \"Basic \" + Buffer.from(user + \":\" + password).toString(\"base64\"),\n });\n }\n const jwtToken = notEmpty(token);\n if (jwtToken === \"\") throw new Error(\"empty token\");\n return { jwtToken };\n } catch (e: any) {\n if (e.code === \"UNAUTHENTICATED\") throw new UnauthenticatedError(e.message);\n throw new Error(e);\n }\n }\n\n /** Request fresh server-issued login material. v1 only emits the public-PKCE flow;\n * desktop MUST place the returned `nonce` verbatim into the OIDC auth-request. `idP` names\n * the advertised SSO method to route to; omitted, the backend keeps its current first-match\n * behaviour. */\n public async beginSSOLogin(idP?: string): Promise<SSOLoginAttempt> {\n const attempt =\n idP === undefined ? await this.ll.beginSSOLogin() : await this.ll.beginSSOLogin(idP);\n return {\n flow: \"public_pkce\",\n nonce: attempt.nonce,\n expiresAt: attempt.expiresAt,\n clientSecret: attempt.clientSecret,\n };\n }\n\n /** Forward the verbatim IdP `/token` response body and receive a Platforma JWT. `idP` names\n * the advertised SSO method to route to; omitted, the backend keeps its current first-match\n * behaviour. */\n public async loginSSO(payload: {\n tokenResponse: Uint8Array;\n idP?: string;\n }): Promise<AuthInformation> {\n try {\n const jwtToken =\n payload.idP === undefined\n ? await this.ll.loginSSO(payload.tokenResponse)\n : await this.ll.loginSSO(payload.tokenResponse, payload.idP);\n if (jwtToken === \"\") throw new Error(\"empty token\");\n return { jwtToken };\n } catch (e: any) {\n if (e.code === \"UNAUTHENTICATED\") throw new UnauthenticatedError(e.message);\n throw new Error(e);\n }\n }\n}\n"],"mappings":";;;;;;AA4DA,IAAa,0BAAb,MAAa,wBAAwB;CACnC;CAEA,YAAoB,IAAgB;EAClC,KAAK,KAAK;CACZ;CAEA,aAAoB,MAClB,iBACA,KACkC;EAElC,OAAO,IAAI,wBAAwB,MADlBA,kBAAAA,WAAW,MAAM,iBAAiB,GAAG,CACjB;CACvC;CAEA,MAAa,OAA8C;EACzD,OAAO,MAAM,KAAK,GAAG,KAAK;CAC5B;CAEA,MAAa,cAAqD;EAChE,OAAO,MAAM,KAAK,GAAG,YAAY;CACnC;CAEA,MAAa,cAAgC;EAC3C,QAAQ,MAAM,KAAK,YAAY,EAAA,CAAG,QAAQ,SAAS;CACrD;CAEA,cAAqB,YAAwC;EAC3D,OAAO,KAAK,GAAG,cAAc,UAAU;CACzC;;;;;;CAOA,IAAW,uBAA2D;EACpE,MAAM,SAAS;GAAE,OAAO;GAAO,OAAO;EAAM;EAC5C,KAAK,MAAM,KAAK,KAAK,GAAG,gBAAgB,SACtC,IAAI,EAAE,OAAO,cAAc,SAAS,OAAO,QAAQ;OAC9C,IAAI,EAAE,OAAO,cAAc,SAAS,OAAO,QAAQ;EAE1D,OAAO;CACT;;;;CAKA,gBACE,QAC2B;EAC3B,IAAI,OAAO,OAAO,cAAc,OAAO,OAAO,KAAA;EAC9C,MAAM,MAAM,OAAO,OAAO;EAC1B,IAAI,IAAI,aAAA,GAAqE,OAAO,KAAA;EACpF,OAAO;GACL,IAAI,OAAO;GACX,OAAO,OAAO,SAAS,OAAO,eAAe,OAAO;GACpD,aAAa,OAAO;GACpB,QAAQ,IAAI;GACZ,UAAU,IAAI;GACd,QAAQ,IAAI;GACZ,UAAU,IAAI;GACd,QAAQ,IAAI;GACZ,eAAe,IAAI;GACnB,oBAAoB,IAAI;GACxB,aAAa,IAAI;GACjB,aAAa,IAAI;GACjB,YAAY,IAAI,cAAc,KAAA;GAC9B,UAAU;EACZ;CACF;;;;;CAMA,YAA8C;EAC5C,KAAK,MAAM,UAAU,KAAK,GAAG,gBAAgB,SAAS;GACpD,IAAI,OAAO,OAAO,cAAc,OAAO;GACvC,MAAM,MAAM,OAAO,OAAO;GAC1B,IAAI,IAAI,aAAA,GACN,MAAM,IAAI,MAAM,wCAAwC,IAAI,UAAU;GAExE,OAAO,KAAK,gBAAgB,MAAM;EACpC;CAEF;;;;;CAMA,eAAqC;EACnC,MAAM,SAAwB,CAAC;EAC/B,KAAK,MAAM,UAAU,KAAK,GAAG,gBAAgB,SAC3C,QAAQ,OAAO,OAAO,WAAtB;GACE,KAAK;GACL,KAAK;IACH,OAAO,KAAK;KACV,MAAM,OAAO,OAAO;KACpB,IAAI,OAAO;KACX,OAAO,OAAO,SAAS,OAAO,eAAe,OAAO;KACpD,aAAa,OAAO;IACtB,CAAC;IACD;GACF,KAAK,OAAO;IACV,MAAM,MAAM,KAAK,gBAAgB,MAAM;IACvC,IAAI,QAAQ,KAAA,GAAW,OAAO,KAAK;KAAE,MAAM;KAAO,GAAG;IAAI,CAAC;IAC1D;GACF;GACA,SAEE;EACJ;EAEF,OAAO;CACT;;;;;;;;;;;CAYA,MAAa,MAAM,MAAc,UAAkB,KAAwC;EACzF,IAAI;GACF,IAAI;GACJ,IAAI,KAAK,GAAG,cAAc,SAAS,GAAG;IACpC,MAAM,UAAU,KAAK;IAGrB,KADE,QAAQ,KAAA,IAAY,KAAA,IAAY,KAAK,aAAa,CAAC,CAAC,MAAM,MAAM,EAAE,OAAO,GAAG,CAAC,EAAE,UAC/D,SAChB,QAAQ,MAAM,KAAK,GAAG,eAAe,QAAQ;SACxC,IAAI,QAAQ,OACjB,QACE,QAAQ,KAAA,IACJ,MAAM,KAAK,GAAG,WAAW,MAAM,QAAQ,IACvC,MAAM,KAAK,GAAG,WAAW,MAAM,UAAU,EAAE,IAAI,CAAC;SACjD,IAAI,QAAQ,OACjB,QAAQ,MAAM,KAAK,GAAG,eAAe,QAAQ;SAE7C,MAAM,IAAI,MAAM,wDAAwD;GAE5E,OACE,QAAQ,MAAM,KAAK,GAAG,YAAY,OAAO,KAAK,GAAG,KAAK,cAAc,GAAG,EACrE,eAAe,WAAW,OAAO,KAAK,OAAO,MAAM,QAAQ,CAAC,CAAC,SAAS,QAAQ,EAChF,CAAC;GAEH,MAAM,YAAA,GAAA,2BAAA,SAAA,CAAoB,KAAK;GAC/B,IAAI,aAAa,IAAI,MAAM,IAAI,MAAM,aAAa;GAClD,OAAO,EAAE,SAAS;EACpB,SAAS,GAAQ;GACf,IAAI,EAAE,SAAS,mBAAmB,MAAM,IAAIC,eAAAA,qBAAqB,EAAE,OAAO;GAC1E,MAAM,IAAI,MAAM,CAAC;EACnB;CACF;;;;;CAMA,MAAa,cAAc,KAAwC;EACjE,MAAM,UACJ,QAAQ,KAAA,IAAY,MAAM,KAAK,GAAG,cAAc,IAAI,MAAM,KAAK,GAAG,cAAc,GAAG;EACrF,OAAO;GACL,MAAM;GACN,OAAO,QAAQ;GACf,WAAW,QAAQ;GACnB,cAAc,QAAQ;EACxB;CACF;;;;CAKA,MAAa,SAAS,SAGO;EAC3B,IAAI;GACF,MAAM,WACJ,QAAQ,QAAQ,KAAA,IACZ,MAAM,KAAK,GAAG,SAAS,QAAQ,aAAa,IAC5C,MAAM,KAAK,GAAG,SAAS,QAAQ,eAAe,QAAQ,GAAG;GAC/D,IAAI,aAAa,IAAI,MAAM,IAAI,MAAM,aAAa;GAClD,OAAO,EAAE,SAAS;EACpB,SAAS,GAAQ;GACf,IAAI,EAAE,SAAS,mBAAmB,MAAM,IAAIA,eAAAA,qBAAqB,EAAE,OAAO;GAC1E,MAAM,IAAI,MAAM,CAAC;EACnB;CACF;AACF"}
@@ -3,16 +3,16 @@ import { AuthInformation, PlClientConfig } from "./config.js";
3
3
  import { BackendCapability } from "./capabilities.js";
4
4
  import { LLPlClient } from "./ll_client.js";
5
5
  import { MiLogger } from "@milaboratories/ts-helpers";
6
-
7
6
  //#region src/core/unauth_client.d.ts
8
7
  /** Login-flow shape advertised by the backend for a given SSO method. Extension
9
8
  * point: future flow variants (e.g. backend-driven) sit alongside `public_pkce`. */
10
- type SSOFlowType = "public_pkce";
9
+ export type SSOFlowType = "public_pkce";
11
10
  /** Typed projection of `AuthAPI.ListMethods.SSOAuthMethod` for desktop consumption.
12
11
  * Carries everything needed to drive the PKCE flow + diagnostics fields the renderer
13
12
  * may surface (`userIdClaim`, `groupsClaim`). */
14
- type SSOAuthMethod = {
13
+ export type SSOAuthMethod = {
15
14
  id: string;
15
+ title: string;
16
16
  description: string;
17
17
  issuer: string;
18
18
  clientId: string;
@@ -31,7 +31,7 @@ type SSOAuthMethod = {
31
31
  };
32
32
  /** Server-issued material returned by {@link UnauthenticatedPlClient.beginSSOLogin}.
33
33
  * Discriminated on `flow`; future flow variants extend the union. */
34
- type SSOLoginAttempt = {
34
+ export type SSOLoginAttempt = {
35
35
  flow: "public_pkce";
36
36
  nonce: string;
37
37
  expiresAt: Date;
@@ -41,8 +41,24 @@ type SSOLoginAttempt = {
41
41
  * desktop exchanges the code as a confidential client. */
42
42
  clientSecret?: string;
43
43
  };
44
+ /** One login method the backend advertises, of any kind. Every advertised method keeps its own
45
+ * `id`, `title`, `description` and kind. Two same-kind methods, such as two LDAP directories, are
46
+ * two entries a caller can name apart and never collapsed into one. See {@link UnauthenticatedPlClient.loginMethods}. */
47
+ export type LoginMethod = {
48
+ kind: "basic";
49
+ id: string;
50
+ title: string;
51
+ description: string;
52
+ } | {
53
+ kind: "token";
54
+ id: string;
55
+ title: string;
56
+ description: string;
57
+ } | ({
58
+ kind: "sso";
59
+ } & SSOAuthMethod);
44
60
  /** Primarily used for initial authentication (login) */
45
- declare class UnauthenticatedPlClient {
61
+ export declare class UnauthenticatedPlClient {
46
62
  readonly ll: LLPlClient;
47
63
  private constructor();
48
64
  static build(configOrAddress: PlClientConfig | string, ops?: {
@@ -54,33 +70,50 @@ declare class UnauthenticatedPlClient {
54
70
  hasCapability(capability: BackendCapability): boolean;
55
71
  /** Classifies the advertised authentication methods by credential scheme.
56
72
  * On legacy backends (no auth:v2) the typed oneof is empty; callers fall through to {@link login}
57
- * which uses the legacy GetJWTToken path. SSO is surfaced via {@link ssoConfig}, not here. */
73
+ * which uses the legacy GetJWTToken path.
74
+ * @deprecated collapses every basic-kind method into one boolean; use {@link loginMethods} to
75
+ * see each advertised method's own id. `login()` still reads this to choose the credential path. */
58
76
  get supportedAuthSchemes(): {
59
77
  basic: boolean;
60
78
  token: boolean;
61
79
  };
80
+ /** Builds the {@link SSOAuthMethod} projection an advertised SSO entry carries, or `undefined`
81
+ * for a flow no client here can drive. Shared by {@link ssoConfig} and {@link loginMethods},
82
+ * which each apply their own rule for an unsupported flow. */
83
+ private toSSOAuthMethod;
62
84
  /** Projection of the first advertised SSO method, derived from {@link authMethodsSync}.
63
- * v1: at most one SSO method per deployment, so callers do not need to discriminate. */
85
+ * v1: at most one SSO method per deployment, so callers do not need to discriminate.
86
+ * @deprecated surfaces only the first advertised SSO method; use {@link loginMethods} to see
87
+ * every advertised method, of every kind. */
64
88
  ssoConfig(): SSOAuthMethod | undefined;
65
- /** Login with username+password.
89
+ /** Every login method the backend advertises, of every kind, in advertised order, each
90
+ * keeping its own `id`, `title`, `description` and kind. An entry with no usable method arm, or an
91
+ * SSO entry whose flow no client here can drive, is dropped rather than failing the whole
92
+ * list, which is why this accessor skips where {@link ssoConfig} throws. */
93
+ loginMethods(): LoginMethod[];
94
+ /** Login with username and password.
66
95
  *
67
- * On auth:v2 backends the client inspects the advertised AuthMethods:
68
- * - if basic auth is offered, sends {@link LLPlClient.loginBasic};
69
- * - if only token auth is offered, treats `password` as an opaque bearer token and
70
- * sends {@link LLPlClient.loginWithToken} (so deployments configured for static-token
71
- * auth still log in without the caller switching methods).
96
+ * Routes to {@link LLPlClient.loginBasic} or {@link LLPlClient.loginWithToken} based on the
97
+ * advertised method `idP` names. On legacy backends, uses GetJWTToken with Basic header.
72
98
  *
73
- * On legacy backends (no auth:v2) it falls through to GetJWTToken with the Basic header,
74
- * preserving original behavior. */
75
- login(user: string, password: string): Promise<AuthInformation>;
99
+ * `idP` names the advertised method. A named basic-kind method routes to the basic branch;
100
+ * a named token-kind method routes to the token branch even when the backend also advertises
101
+ * a basic method — the token wire carries no selector, so the id itself is dropped there and
102
+ * only the branch choice is kept. Omitted, or naming no advertised method, falls back to
103
+ * today's first-match behavior: basic wins over token when both are advertised. */
104
+ login(user: string, password: string, idP?: string): Promise<AuthInformation>;
76
105
  /** Request fresh server-issued login material. v1 only emits the public-PKCE flow;
77
- * desktop MUST place the returned `nonce` verbatim into the OIDC auth-request. */
78
- beginSSOLogin(): Promise<SSOLoginAttempt>;
79
- /** Forward the verbatim IdP `/token` response body and receive a Platforma JWT. */
106
+ * desktop MUST place the returned `nonce` verbatim into the OIDC auth-request. `idP` names
107
+ * the advertised SSO method to route to; omitted, the backend keeps its current first-match
108
+ * behaviour. */
109
+ beginSSOLogin(idP?: string): Promise<SSOLoginAttempt>;
110
+ /** Forward the verbatim IdP `/token` response body and receive a Platforma JWT. `idP` names
111
+ * the advertised SSO method to route to; omitted, the backend keeps its current first-match
112
+ * behaviour. */
80
113
  loginSSO(payload: {
81
114
  tokenResponse: Uint8Array;
115
+ idP?: string;
82
116
  }): Promise<AuthInformation>;
83
117
  }
84
118
  //#endregion
85
- export { SSOAuthMethod, SSOFlowType, SSOLoginAttempt, UnauthenticatedPlClient };
86
119
  //# sourceMappingURL=unauth_client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"unauth_client.d.ts","names":[],"sources":["../../src/core/unauth_client.ts"],"mappings":";;;;;;;;;KAaY,WAAA;;;;KAKA,aAAA;EACV,EAAA;EACA,WAAA;EACA,MAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,MAAA;EACA,aAAA;EACA,kBAAA;EACA,WAAA;EACA,WAAA;EACA,QAAA,EAAU,WAAW;EAHrB;;;EAOA,UAAA;AAAA;;;KAKU,eAAA;EACV,IAAA;EACA,KAAA;EACA,SAAA,EAAW,IAAI;EAAA;;;;EAKf,YAAA;AAAA;;cAIW,uBAAA;EAAA,SACK,EAAA,EAAI,UAAA;EAAA,QAEb,WAAA;EAAA,OAIa,KAAA,CAClB,eAAA,EAAiB,cAAA,WACjB,GAAA;IAAQ,MAAA,GAAS,QAAA;EAAA,IAChB,OAAA,CAAQ,uBAAA;EAKE,IAAA,IAAQ,OAAA,CAAQ,4BAAA;EAIhB,WAAA,IAAe,OAAA,CAAQ,4BAAA;EAIvB,WAAA,IAAe,OAAA;EAIrB,aAAA,CAAc,UAAA,EAAY,iBAAA;EAZZ;;;EAAA,IAmBV,oBAAA;IAA0B,KAAA;IAAgB,KAAA;EAAA;EA+CD;;EApC7C,SAAA,IAAa,aAAA;EA2E4B;;;;;;;;;;EAvCnC,KAAA,CAAM,IAAA,UAAc,QAAA,WAAmB,OAAA,CAAQ,eAAA;EAxElD;;EAoGG,aAAA,IAAiB,OAAA,CAAQ,eAAA;EAnGnC;EA8GU,QAAA,CAAS,OAAA;IAAW,aAAA,EAAe,UAAA;EAAA,IAAe,OAAA,CAAQ,eAAA;AAAA"}
1
+ {"version":3,"file":"unauth_client.d.ts","names":[],"sources":["../../src/core/unauth_client.ts"],"mappings":";;;;;;;;YAaY;;;;YAKA;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,UAAU;;;;EAIV;;;;YAKU;EACV;EACA;EACA,WAAW;;;;;EAKX;;;;;YAMU;EACN;EAAe;EAAY;EAAe;;EAC1C;EAAe;EAAY;EAAe;;EACzC;IAAgB;;qBAGV;WACK,IAAI;UAEb;SAIa,MAClB,iBAAiB,yBACjB;IAAQ,SAAS;MAChB,QAAQ;EAKE,QAAQ,QAAQ;EAIhB,eAAe,QAAQ;EAIvB,eAAe;EAIrB,cAAc,YAAY;;;;;;MAStB;IAA0B;IAAgB;;;;;UAY7C;;;;;EA4BD,aAAa;;;;;EAgBb,gBAAgB;;;;;;;;;;;EAoCV,MAAM,cAAc,kBAAkB,eAAe,QAAQ;;;;;EAqC7D,cAAc,eAAe,QAAQ;;;;EAcrC,SAAS;IACpB,eAAe;IACf;MACE,QAAQ"}
@@ -26,7 +26,9 @@ var UnauthenticatedPlClient = class UnauthenticatedPlClient {
26
26
  }
27
27
  /** Classifies the advertised authentication methods by credential scheme.
28
28
  * On legacy backends (no auth:v2) the typed oneof is empty; callers fall through to {@link login}
29
- * which uses the legacy GetJWTToken path. SSO is surfaced via {@link ssoConfig}, not here. */
29
+ * which uses the legacy GetJWTToken path.
30
+ * @deprecated collapses every basic-kind method into one boolean; use {@link loginMethods} to
31
+ * see each advertised method's own id. `login()` still reads this to choose the credential path. */
30
32
  get supportedAuthSchemes() {
31
33
  const result = {
32
34
  basic: false,
@@ -36,46 +38,87 @@ var UnauthenticatedPlClient = class UnauthenticatedPlClient {
36
38
  else if (m.method.oneofKind === "token") result.token = true;
37
39
  return result;
38
40
  }
41
+ /** Builds the {@link SSOAuthMethod} projection an advertised SSO entry carries, or `undefined`
42
+ * for a flow no client here can drive. Shared by {@link ssoConfig} and {@link loginMethods},
43
+ * which each apply their own rule for an unsupported flow. */
44
+ toSSOAuthMethod(method) {
45
+ if (method.method.oneofKind !== "sso") return void 0;
46
+ const sso = method.method.sso;
47
+ if (sso.flowType !== 0) return void 0;
48
+ return {
49
+ id: method.id,
50
+ title: method.title || method.description || method.id,
51
+ description: method.description,
52
+ issuer: sso.issuer,
53
+ clientId: sso.clientId,
54
+ scopes: sso.scopes,
55
+ resource: sso.resource,
56
+ prompt: sso.prompt,
57
+ redirectPorts: sso.redirectPorts,
58
+ subjectTokenSource: sso.subjectTokenSource,
59
+ userIdClaim: sso.userIdClaim,
60
+ groupsClaim: sso.groupsClaim,
61
+ accessType: sso.accessType || void 0,
62
+ flowType: "public_pkce"
63
+ };
64
+ }
39
65
  /** Projection of the first advertised SSO method, derived from {@link authMethodsSync}.
40
- * v1: at most one SSO method per deployment, so callers do not need to discriminate. */
66
+ * v1: at most one SSO method per deployment, so callers do not need to discriminate.
67
+ * @deprecated surfaces only the first advertised SSO method; use {@link loginMethods} to see
68
+ * every advertised method, of every kind. */
41
69
  ssoConfig() {
42
70
  for (const method of this.ll.authMethodsSync.methods) {
43
71
  if (method.method.oneofKind !== "sso") continue;
44
72
  const sso = method.method.sso;
45
73
  if (sso.flowType !== 0) throw new Error(`ssoConfig: unsupported SSO flow type ${sso.flowType}`);
46
- return {
47
- id: method.id,
48
- description: method.description,
49
- issuer: sso.issuer,
50
- clientId: sso.clientId,
51
- scopes: sso.scopes,
52
- resource: sso.resource,
53
- prompt: sso.prompt,
54
- redirectPorts: sso.redirectPorts,
55
- subjectTokenSource: sso.subjectTokenSource,
56
- userIdClaim: sso.userIdClaim,
57
- groupsClaim: sso.groupsClaim,
58
- accessType: sso.accessType || void 0,
59
- flowType: "public_pkce"
60
- };
74
+ return this.toSSOAuthMethod(method);
75
+ }
76
+ }
77
+ /** Every login method the backend advertises, of every kind, in advertised order, each
78
+ * keeping its own `id`, `title`, `description` and kind. An entry with no usable method arm, or an
79
+ * SSO entry whose flow no client here can drive, is dropped rather than failing the whole
80
+ * list, which is why this accessor skips where {@link ssoConfig} throws. */
81
+ loginMethods() {
82
+ const picked = [];
83
+ for (const method of this.ll.authMethodsSync.methods) switch (method.method.oneofKind) {
84
+ case "basic":
85
+ case "token":
86
+ picked.push({
87
+ kind: method.method.oneofKind,
88
+ id: method.id,
89
+ title: method.title || method.description || method.id,
90
+ description: method.description
91
+ });
92
+ break;
93
+ case "sso": {
94
+ const sso = this.toSSOAuthMethod(method);
95
+ if (sso !== void 0) picked.push({
96
+ kind: "sso",
97
+ ...sso
98
+ });
99
+ break;
100
+ }
101
+ default: break;
61
102
  }
103
+ return picked;
62
104
  }
63
- /** Login with username+password.
105
+ /** Login with username and password.
64
106
  *
65
- * On auth:v2 backends the client inspects the advertised AuthMethods:
66
- * - if basic auth is offered, sends {@link LLPlClient.loginBasic};
67
- * - if only token auth is offered, treats `password` as an opaque bearer token and
68
- * sends {@link LLPlClient.loginWithToken} (so deployments configured for static-token
69
- * auth still log in without the caller switching methods).
107
+ * Routes to {@link LLPlClient.loginBasic} or {@link LLPlClient.loginWithToken} based on the
108
+ * advertised method `idP` names. On legacy backends, uses GetJWTToken with Basic header.
70
109
  *
71
- * On legacy backends (no auth:v2) it falls through to GetJWTToken with the Basic header,
72
- * preserving original behavior. */
73
- async login(user, password) {
110
+ * `idP` names the advertised method. A named basic-kind method routes to the basic branch;
111
+ * a named token-kind method routes to the token branch even when the backend also advertises
112
+ * a basic method — the token wire carries no selector, so the id itself is dropped there and
113
+ * only the branch choice is kept. Omitted, or naming no advertised method, falls back to
114
+ * today's first-match behavior: basic wins over token when both are advertised. */
115
+ async login(user, password, idP) {
74
116
  try {
75
117
  let token;
76
118
  if (this.ll.hasCapability("auth:v2")) {
77
119
  const schemes = this.supportedAuthSchemes;
78
- if (schemes.basic) token = await this.ll.loginBasic(user, password);
120
+ if ((idP === void 0 ? void 0 : this.loginMethods().find((m) => m.id === idP)?.kind) === "token") token = await this.ll.loginWithToken(password);
121
+ else if (schemes.basic) token = idP === void 0 ? await this.ll.loginBasic(user, password) : await this.ll.loginBasic(user, password, { idP });
79
122
  else if (schemes.token) token = await this.ll.loginWithToken(password);
80
123
  else throw new Error("backend advertises no supported authentication methods");
81
124
  } else token = await this.ll.getJwtToken(BigInt(this.ll.conf.authTTLSeconds), { authorization: "Basic " + Buffer.from(user + ":" + password).toString("base64") });
@@ -88,9 +131,11 @@ var UnauthenticatedPlClient = class UnauthenticatedPlClient {
88
131
  }
89
132
  }
90
133
  /** Request fresh server-issued login material. v1 only emits the public-PKCE flow;
91
- * desktop MUST place the returned `nonce` verbatim into the OIDC auth-request. */
92
- async beginSSOLogin() {
93
- const attempt = await this.ll.beginSSOLogin();
134
+ * desktop MUST place the returned `nonce` verbatim into the OIDC auth-request. `idP` names
135
+ * the advertised SSO method to route to; omitted, the backend keeps its current first-match
136
+ * behaviour. */
137
+ async beginSSOLogin(idP) {
138
+ const attempt = idP === void 0 ? await this.ll.beginSSOLogin() : await this.ll.beginSSOLogin(idP);
94
139
  return {
95
140
  flow: "public_pkce",
96
141
  nonce: attempt.nonce,
@@ -98,10 +143,12 @@ var UnauthenticatedPlClient = class UnauthenticatedPlClient {
98
143
  clientSecret: attempt.clientSecret
99
144
  };
100
145
  }
101
- /** Forward the verbatim IdP `/token` response body and receive a Platforma JWT. */
146
+ /** Forward the verbatim IdP `/token` response body and receive a Platforma JWT. `idP` names
147
+ * the advertised SSO method to route to; omitted, the backend keeps its current first-match
148
+ * behaviour. */
102
149
  async loginSSO(payload) {
103
150
  try {
104
- const jwtToken = await this.ll.loginSSO(payload.tokenResponse);
151
+ const jwtToken = payload.idP === void 0 ? await this.ll.loginSSO(payload.tokenResponse) : await this.ll.loginSSO(payload.tokenResponse, payload.idP);
105
152
  if (jwtToken === "") throw new Error("empty token");
106
153
  return { jwtToken };
107
154
  } catch (e) {
@@ -1 +1 @@
1
- {"version":3,"file":"unauth_client.js","names":[],"sources":["../../src/core/unauth_client.ts"],"sourcesContent":["import type { AuthInformation, PlClientConfig } from \"./config\";\nimport {\n type AuthAPI_ListMethods_Response,\n type MaintenanceAPI_Ping_Response,\n AuthAPI_ListMethods_SSOAuthMethod_FlowType,\n} from \"../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api\";\nimport { LLPlClient } from \"./ll_client\";\nimport { type MiLogger, notEmpty } from \"@milaboratories/ts-helpers\";\nimport { UnauthenticatedError } from \"./errors\";\nimport type { BackendCapability } from \"./capabilities\";\n\n/** Login-flow shape advertised by the backend for a given SSO method. Extension\n * point: future flow variants (e.g. backend-driven) sit alongside `public_pkce`. */\nexport type SSOFlowType = \"public_pkce\";\n\n/** Typed projection of `AuthAPI.ListMethods.SSOAuthMethod` for desktop consumption.\n * Carries everything needed to drive the PKCE flow + diagnostics fields the renderer\n * may surface (`userIdClaim`, `groupsClaim`). */\nexport type SSOAuthMethod = {\n id: string;\n description: string;\n issuer: string;\n clientId: string;\n scopes: string;\n resource: string;\n prompt: string;\n redirectPorts: number[];\n subjectTokenSource: string;\n userIdClaim: string;\n groupsClaim: string;\n flowType: SSOFlowType;\n /** OIDC `access_type` auth-request param (\"online\" | \"offline\"). Google-specific:\n * \"offline\" makes Google issue a refresh token; other IdPs ignore it. Absent when\n * the server does not set it — never send an empty string to the IdP. */\n accessType?: string;\n};\n\n/** Server-issued material returned by {@link UnauthenticatedPlClient.beginSSOLogin}.\n * Discriminated on `flow`; future flow variants extend the union. */\nexport type SSOLoginAttempt = {\n flow: \"public_pkce\";\n nonce: string;\n expiresAt: Date;\n /** Confidential-client secret for the IdP token exchange; absent for public clients.\n * Google's OIDC has no public-client mode — it requires a client_secret at the token\n * endpoint even with PKCE — so for Google the backend forwards its secret here and the\n * desktop exchanges the code as a confidential client. */\n clientSecret?: string;\n};\n\n/** Primarily used for initial authentication (login) */\nexport class UnauthenticatedPlClient {\n public readonly ll: LLPlClient;\n\n private constructor(ll: LLPlClient) {\n this.ll = ll;\n }\n\n public static async build(\n configOrAddress: PlClientConfig | string,\n ops?: { logger?: MiLogger },\n ): Promise<UnauthenticatedPlClient> {\n const ll = await LLPlClient.build(configOrAddress, ops);\n return new UnauthenticatedPlClient(ll);\n }\n\n public async ping(): Promise<MaintenanceAPI_Ping_Response> {\n return await this.ll.ping();\n }\n\n public async authMethods(): Promise<AuthAPI_ListMethods_Response> {\n return await this.ll.authMethods();\n }\n\n public async requireAuth(): Promise<boolean> {\n return (await this.authMethods()).methods.length > 0;\n }\n\n public hasCapability(capability: BackendCapability): boolean {\n return this.ll.hasCapability(capability);\n }\n\n /** Classifies the advertised authentication methods by credential scheme.\n * On legacy backends (no auth:v2) the typed oneof is empty; callers fall through to {@link login}\n * which uses the legacy GetJWTToken path. SSO is surfaced via {@link ssoConfig}, not here. */\n public get supportedAuthSchemes(): { basic: boolean; token: boolean } {\n const result = { basic: false, token: false };\n for (const m of this.ll.authMethodsSync.methods) {\n if (m.method.oneofKind === \"basic\") result.basic = true;\n else if (m.method.oneofKind === \"token\") result.token = true;\n }\n return result;\n }\n\n /** Projection of the first advertised SSO method, derived from {@link authMethodsSync}.\n * v1: at most one SSO method per deployment, so callers do not need to discriminate. */\n public ssoConfig(): SSOAuthMethod | undefined {\n for (const method of this.ll.authMethodsSync.methods) {\n if (method.method.oneofKind !== \"sso\") continue;\n const sso = method.method.sso;\n if (sso.flowType !== AuthAPI_ListMethods_SSOAuthMethod_FlowType.PUBLIC_PKCE) {\n throw new Error(`ssoConfig: unsupported SSO flow type ${sso.flowType}`);\n }\n return {\n id: method.id,\n description: method.description,\n issuer: sso.issuer,\n clientId: sso.clientId,\n scopes: sso.scopes,\n resource: sso.resource,\n prompt: sso.prompt,\n redirectPorts: sso.redirectPorts,\n subjectTokenSource: sso.subjectTokenSource,\n userIdClaim: sso.userIdClaim,\n groupsClaim: sso.groupsClaim,\n accessType: sso.accessType || undefined,\n flowType: \"public_pkce\",\n };\n }\n return undefined;\n }\n\n /** Login with username+password.\n *\n * On auth:v2 backends the client inspects the advertised AuthMethods:\n * - if basic auth is offered, sends {@link LLPlClient.loginBasic};\n * - if only token auth is offered, treats `password` as an opaque bearer token and\n * sends {@link LLPlClient.loginWithToken} (so deployments configured for static-token\n * auth still log in without the caller switching methods).\n *\n * On legacy backends (no auth:v2) it falls through to GetJWTToken with the Basic header,\n * preserving original behavior. */\n public async login(user: string, password: string): Promise<AuthInformation> {\n try {\n let token: string;\n if (this.ll.hasCapability(\"auth:v2\")) {\n const schemes = this.supportedAuthSchemes;\n if (schemes.basic) {\n token = await this.ll.loginBasic(user, password);\n } else if (schemes.token) {\n token = await this.ll.loginWithToken(password);\n } else {\n throw new Error(\"backend advertises no supported authentication methods\");\n }\n } else {\n token = await this.ll.getJwtToken(BigInt(this.ll.conf.authTTLSeconds), {\n authorization: \"Basic \" + Buffer.from(user + \":\" + password).toString(\"base64\"),\n });\n }\n const jwtToken = notEmpty(token);\n if (jwtToken === \"\") throw new Error(\"empty token\");\n return { jwtToken };\n } catch (e: any) {\n if (e.code === \"UNAUTHENTICATED\") throw new UnauthenticatedError(e.message);\n throw new Error(e);\n }\n }\n\n /** Request fresh server-issued login material. v1 only emits the public-PKCE flow;\n * desktop MUST place the returned `nonce` verbatim into the OIDC auth-request. */\n public async beginSSOLogin(): Promise<SSOLoginAttempt> {\n const attempt = await this.ll.beginSSOLogin();\n return {\n flow: \"public_pkce\",\n nonce: attempt.nonce,\n expiresAt: attempt.expiresAt,\n clientSecret: attempt.clientSecret,\n };\n }\n\n /** Forward the verbatim IdP `/token` response body and receive a Platforma JWT. */\n public async loginSSO(payload: { tokenResponse: Uint8Array }): Promise<AuthInformation> {\n try {\n const jwtToken = await this.ll.loginSSO(payload.tokenResponse);\n if (jwtToken === \"\") throw new Error(\"empty token\");\n return { jwtToken };\n } catch (e: any) {\n if (e.code === \"UNAUTHENTICATED\") throw new UnauthenticatedError(e.message);\n throw new Error(e);\n }\n }\n}\n"],"mappings":";;;;;;AAmDA,IAAa,0BAAb,MAAa,wBAAwB;CACnC;CAEA,YAAoB,IAAgB;EAClC,KAAK,KAAK;CACZ;CAEA,aAAoB,MAClB,iBACA,KACkC;EAElC,OAAO,IAAI,wBAAwB,MADlB,WAAW,MAAM,iBAAiB,GAAG,CACjB;CACvC;CAEA,MAAa,OAA8C;EACzD,OAAO,MAAM,KAAK,GAAG,KAAK;CAC5B;CAEA,MAAa,cAAqD;EAChE,OAAO,MAAM,KAAK,GAAG,YAAY;CACnC;CAEA,MAAa,cAAgC;EAC3C,QAAQ,MAAM,KAAK,YAAY,EAAA,CAAG,QAAQ,SAAS;CACrD;CAEA,cAAqB,YAAwC;EAC3D,OAAO,KAAK,GAAG,cAAc,UAAU;CACzC;;;;CAKA,IAAW,uBAA2D;EACpE,MAAM,SAAS;GAAE,OAAO;GAAO,OAAO;EAAM;EAC5C,KAAK,MAAM,KAAK,KAAK,GAAG,gBAAgB,SACtC,IAAI,EAAE,OAAO,cAAc,SAAS,OAAO,QAAQ;OAC9C,IAAI,EAAE,OAAO,cAAc,SAAS,OAAO,QAAQ;EAE1D,OAAO;CACT;;;CAIA,YAA8C;EAC5C,KAAK,MAAM,UAAU,KAAK,GAAG,gBAAgB,SAAS;GACpD,IAAI,OAAO,OAAO,cAAc,OAAO;GACvC,MAAM,MAAM,OAAO,OAAO;GAC1B,IAAI,IAAI,aAAA,GACN,MAAM,IAAI,MAAM,wCAAwC,IAAI,UAAU;GAExE,OAAO;IACL,IAAI,OAAO;IACX,aAAa,OAAO;IACpB,QAAQ,IAAI;IACZ,UAAU,IAAI;IACd,QAAQ,IAAI;IACZ,UAAU,IAAI;IACd,QAAQ,IAAI;IACZ,eAAe,IAAI;IACnB,oBAAoB,IAAI;IACxB,aAAa,IAAI;IACjB,aAAa,IAAI;IACjB,YAAY,IAAI,cAAc,KAAA;IAC9B,UAAU;GACZ;EACF;CAEF;;;;;;;;;;;CAYA,MAAa,MAAM,MAAc,UAA4C;EAC3E,IAAI;GACF,IAAI;GACJ,IAAI,KAAK,GAAG,cAAc,SAAS,GAAG;IACpC,MAAM,UAAU,KAAK;IACrB,IAAI,QAAQ,OACV,QAAQ,MAAM,KAAK,GAAG,WAAW,MAAM,QAAQ;SAC1C,IAAI,QAAQ,OACjB,QAAQ,MAAM,KAAK,GAAG,eAAe,QAAQ;SAE7C,MAAM,IAAI,MAAM,wDAAwD;GAE5E,OACE,QAAQ,MAAM,KAAK,GAAG,YAAY,OAAO,KAAK,GAAG,KAAK,cAAc,GAAG,EACrE,eAAe,WAAW,OAAO,KAAK,OAAO,MAAM,QAAQ,CAAC,CAAC,SAAS,QAAQ,EAChF,CAAC;GAEH,MAAM,WAAW,SAAS,KAAK;GAC/B,IAAI,aAAa,IAAI,MAAM,IAAI,MAAM,aAAa;GAClD,OAAO,EAAE,SAAS;EACpB,SAAS,GAAQ;GACf,IAAI,EAAE,SAAS,mBAAmB,MAAM,IAAI,qBAAqB,EAAE,OAAO;GAC1E,MAAM,IAAI,MAAM,CAAC;EACnB;CACF;;;CAIA,MAAa,gBAA0C;EACrD,MAAM,UAAU,MAAM,KAAK,GAAG,cAAc;EAC5C,OAAO;GACL,MAAM;GACN,OAAO,QAAQ;GACf,WAAW,QAAQ;GACnB,cAAc,QAAQ;EACxB;CACF;;CAGA,MAAa,SAAS,SAAkE;EACtF,IAAI;GACF,MAAM,WAAW,MAAM,KAAK,GAAG,SAAS,QAAQ,aAAa;GAC7D,IAAI,aAAa,IAAI,MAAM,IAAI,MAAM,aAAa;GAClD,OAAO,EAAE,SAAS;EACpB,SAAS,GAAQ;GACf,IAAI,EAAE,SAAS,mBAAmB,MAAM,IAAI,qBAAqB,EAAE,OAAO;GAC1E,MAAM,IAAI,MAAM,CAAC;EACnB;CACF;AACF"}
1
+ {"version":3,"file":"unauth_client.js","names":[],"sources":["../../src/core/unauth_client.ts"],"sourcesContent":["import type { AuthInformation, PlClientConfig } from \"./config\";\nimport {\n type AuthAPI_ListMethods_Response,\n type MaintenanceAPI_Ping_Response,\n AuthAPI_ListMethods_SSOAuthMethod_FlowType,\n} from \"../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api\";\nimport { LLPlClient } from \"./ll_client\";\nimport { type MiLogger, notEmpty } from \"@milaboratories/ts-helpers\";\nimport { UnauthenticatedError } from \"./errors\";\nimport type { BackendCapability } from \"./capabilities\";\n\n/** Login-flow shape advertised by the backend for a given SSO method. Extension\n * point: future flow variants (e.g. backend-driven) sit alongside `public_pkce`. */\nexport type SSOFlowType = \"public_pkce\";\n\n/** Typed projection of `AuthAPI.ListMethods.SSOAuthMethod` for desktop consumption.\n * Carries everything needed to drive the PKCE flow + diagnostics fields the renderer\n * may surface (`userIdClaim`, `groupsClaim`). */\nexport type SSOAuthMethod = {\n id: string;\n title: string;\n description: string;\n issuer: string;\n clientId: string;\n scopes: string;\n resource: string;\n prompt: string;\n redirectPorts: number[];\n subjectTokenSource: string;\n userIdClaim: string;\n groupsClaim: string;\n flowType: SSOFlowType;\n /** OIDC `access_type` auth-request param (\"online\" | \"offline\"). Google-specific:\n * \"offline\" makes Google issue a refresh token; other IdPs ignore it. Absent when\n * the server does not set it — never send an empty string to the IdP. */\n accessType?: string;\n};\n\n/** Server-issued material returned by {@link UnauthenticatedPlClient.beginSSOLogin}.\n * Discriminated on `flow`; future flow variants extend the union. */\nexport type SSOLoginAttempt = {\n flow: \"public_pkce\";\n nonce: string;\n expiresAt: Date;\n /** Confidential-client secret for the IdP token exchange; absent for public clients.\n * Google's OIDC has no public-client mode — it requires a client_secret at the token\n * endpoint even with PKCE — so for Google the backend forwards its secret here and the\n * desktop exchanges the code as a confidential client. */\n clientSecret?: string;\n};\n\n/** One login method the backend advertises, of any kind. Every advertised method keeps its own\n * `id`, `title`, `description` and kind. Two same-kind methods, such as two LDAP directories, are\n * two entries a caller can name apart and never collapsed into one. See {@link UnauthenticatedPlClient.loginMethods}. */\nexport type LoginMethod =\n | { kind: \"basic\"; id: string; title: string; description: string }\n | { kind: \"token\"; id: string; title: string; description: string }\n | ({ kind: \"sso\" } & SSOAuthMethod);\n\n/** Primarily used for initial authentication (login) */\nexport class UnauthenticatedPlClient {\n public readonly ll: LLPlClient;\n\n private constructor(ll: LLPlClient) {\n this.ll = ll;\n }\n\n public static async build(\n configOrAddress: PlClientConfig | string,\n ops?: { logger?: MiLogger },\n ): Promise<UnauthenticatedPlClient> {\n const ll = await LLPlClient.build(configOrAddress, ops);\n return new UnauthenticatedPlClient(ll);\n }\n\n public async ping(): Promise<MaintenanceAPI_Ping_Response> {\n return await this.ll.ping();\n }\n\n public async authMethods(): Promise<AuthAPI_ListMethods_Response> {\n return await this.ll.authMethods();\n }\n\n public async requireAuth(): Promise<boolean> {\n return (await this.authMethods()).methods.length > 0;\n }\n\n public hasCapability(capability: BackendCapability): boolean {\n return this.ll.hasCapability(capability);\n }\n\n /** Classifies the advertised authentication methods by credential scheme.\n * On legacy backends (no auth:v2) the typed oneof is empty; callers fall through to {@link login}\n * which uses the legacy GetJWTToken path.\n * @deprecated collapses every basic-kind method into one boolean; use {@link loginMethods} to\n * see each advertised method's own id. `login()` still reads this to choose the credential path. */\n public get supportedAuthSchemes(): { basic: boolean; token: boolean } {\n const result = { basic: false, token: false };\n for (const m of this.ll.authMethodsSync.methods) {\n if (m.method.oneofKind === \"basic\") result.basic = true;\n else if (m.method.oneofKind === \"token\") result.token = true;\n }\n return result;\n }\n\n /** Builds the {@link SSOAuthMethod} projection an advertised SSO entry carries, or `undefined`\n * for a flow no client here can drive. Shared by {@link ssoConfig} and {@link loginMethods},\n * which each apply their own rule for an unsupported flow. */\n private toSSOAuthMethod(\n method: AuthAPI_ListMethods_Response[\"methods\"][number],\n ): SSOAuthMethod | undefined {\n if (method.method.oneofKind !== \"sso\") return undefined;\n const sso = method.method.sso;\n if (sso.flowType !== AuthAPI_ListMethods_SSOAuthMethod_FlowType.PUBLIC_PKCE) return undefined;\n return {\n id: method.id,\n title: method.title || method.description || method.id,\n description: method.description,\n issuer: sso.issuer,\n clientId: sso.clientId,\n scopes: sso.scopes,\n resource: sso.resource,\n prompt: sso.prompt,\n redirectPorts: sso.redirectPorts,\n subjectTokenSource: sso.subjectTokenSource,\n userIdClaim: sso.userIdClaim,\n groupsClaim: sso.groupsClaim,\n accessType: sso.accessType || undefined,\n flowType: \"public_pkce\",\n };\n }\n\n /** Projection of the first advertised SSO method, derived from {@link authMethodsSync}.\n * v1: at most one SSO method per deployment, so callers do not need to discriminate.\n * @deprecated surfaces only the first advertised SSO method; use {@link loginMethods} to see\n * every advertised method, of every kind. */\n public ssoConfig(): SSOAuthMethod | undefined {\n for (const method of this.ll.authMethodsSync.methods) {\n if (method.method.oneofKind !== \"sso\") continue;\n const sso = method.method.sso;\n if (sso.flowType !== AuthAPI_ListMethods_SSOAuthMethod_FlowType.PUBLIC_PKCE) {\n throw new Error(`ssoConfig: unsupported SSO flow type ${sso.flowType}`);\n }\n return this.toSSOAuthMethod(method);\n }\n return undefined;\n }\n\n /** Every login method the backend advertises, of every kind, in advertised order, each\n * keeping its own `id`, `title`, `description` and kind. An entry with no usable method arm, or an\n * SSO entry whose flow no client here can drive, is dropped rather than failing the whole\n * list, which is why this accessor skips where {@link ssoConfig} throws. */\n public loginMethods(): LoginMethod[] {\n const picked: LoginMethod[] = [];\n for (const method of this.ll.authMethodsSync.methods) {\n switch (method.method.oneofKind) {\n case \"basic\":\n case \"token\":\n picked.push({\n kind: method.method.oneofKind,\n id: method.id,\n title: method.title || method.description || method.id,\n description: method.description,\n });\n break;\n case \"sso\": {\n const sso = this.toSSOAuthMethod(method);\n if (sso !== undefined) picked.push({ kind: \"sso\", ...sso });\n break;\n }\n default:\n // No arm at all — a legacy backend advertising nothing pickable.\n break;\n }\n }\n return picked;\n }\n\n /** Login with username and password.\n *\n * Routes to {@link LLPlClient.loginBasic} or {@link LLPlClient.loginWithToken} based on the\n * advertised method `idP` names. On legacy backends, uses GetJWTToken with Basic header.\n *\n * `idP` names the advertised method. A named basic-kind method routes to the basic branch;\n * a named token-kind method routes to the token branch even when the backend also advertises\n * a basic method — the token wire carries no selector, so the id itself is dropped there and\n * only the branch choice is kept. Omitted, or naming no advertised method, falls back to\n * today's first-match behavior: basic wins over token when both are advertised. */\n public async login(user: string, password: string, idP?: string): Promise<AuthInformation> {\n try {\n let token: string;\n if (this.ll.hasCapability(\"auth:v2\")) {\n const schemes = this.supportedAuthSchemes;\n const namedKind =\n idP === undefined ? undefined : this.loginMethods().find((m) => m.id === idP)?.kind;\n if (namedKind === \"token\") {\n token = await this.ll.loginWithToken(password);\n } else if (schemes.basic) {\n token =\n idP === undefined\n ? await this.ll.loginBasic(user, password)\n : await this.ll.loginBasic(user, password, { idP });\n } else if (schemes.token) {\n token = await this.ll.loginWithToken(password);\n } else {\n throw new Error(\"backend advertises no supported authentication methods\");\n }\n } else {\n token = await this.ll.getJwtToken(BigInt(this.ll.conf.authTTLSeconds), {\n authorization: \"Basic \" + Buffer.from(user + \":\" + password).toString(\"base64\"),\n });\n }\n const jwtToken = notEmpty(token);\n if (jwtToken === \"\") throw new Error(\"empty token\");\n return { jwtToken };\n } catch (e: any) {\n if (e.code === \"UNAUTHENTICATED\") throw new UnauthenticatedError(e.message);\n throw new Error(e);\n }\n }\n\n /** Request fresh server-issued login material. v1 only emits the public-PKCE flow;\n * desktop MUST place the returned `nonce` verbatim into the OIDC auth-request. `idP` names\n * the advertised SSO method to route to; omitted, the backend keeps its current first-match\n * behaviour. */\n public async beginSSOLogin(idP?: string): Promise<SSOLoginAttempt> {\n const attempt =\n idP === undefined ? await this.ll.beginSSOLogin() : await this.ll.beginSSOLogin(idP);\n return {\n flow: \"public_pkce\",\n nonce: attempt.nonce,\n expiresAt: attempt.expiresAt,\n clientSecret: attempt.clientSecret,\n };\n }\n\n /** Forward the verbatim IdP `/token` response body and receive a Platforma JWT. `idP` names\n * the advertised SSO method to route to; omitted, the backend keeps its current first-match\n * behaviour. */\n public async loginSSO(payload: {\n tokenResponse: Uint8Array;\n idP?: string;\n }): Promise<AuthInformation> {\n try {\n const jwtToken =\n payload.idP === undefined\n ? await this.ll.loginSSO(payload.tokenResponse)\n : await this.ll.loginSSO(payload.tokenResponse, payload.idP);\n if (jwtToken === \"\") throw new Error(\"empty token\");\n return { jwtToken };\n } catch (e: any) {\n if (e.code === \"UNAUTHENTICATED\") throw new UnauthenticatedError(e.message);\n throw new Error(e);\n }\n }\n}\n"],"mappings":"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"}
@@ -126,6 +126,29 @@ var UserResources = class {
126
126
  async listUsers() {
127
127
  return (await this.ll.listUsers()).map((user) => ({ login: user.login }));
128
128
  }
129
+ /**
130
+ * Deletes a user account: the record, every identity-index entry that resolves to it (its login,
131
+ * its email, and any alternative of either), every grant it holds, and its root resource — and
132
+ * with the root, everything still attached under it.
133
+ *
134
+ * Exists for the duplicate account one person can end up with — an identity that could not be
135
+ * matched across a provider cutover, or one minted before the backend started refusing two
136
+ * records for one identity. The spare record shows up wherever users are listed and its projects
137
+ * keep taking part in deduplication.
138
+ *
139
+ * Destructive and irreversible. Re-attach anything worth keeping to another user's root before
140
+ * calling — `pl-cli admin delete-user` does both steps in order. Requires admin/controller
141
+ * credentials; gRPC-only, like {@link listUsers}.
142
+ */
143
+ async deleteUser(login) {
144
+ const resp = await this.ll.deleteUser(login);
145
+ return {
146
+ userRootId: resp.userRootId === 0n ? void 0 : resp.userRootId,
147
+ userRootDeleted: resp.userRootDeleted,
148
+ revokedGrants: resp.revokedGrants,
149
+ removedIdentityIndexEntries: resp.removedIdentityIndexEntries
150
+ };
151
+ }
129
152
  async getUserRootViaRpc(opts = {}) {
130
153
  const resp = await this.ll.getUserRoot({
131
154
  login: opts.login,