@structured-id/ui-core 1.2.0 → 1.3.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 (101) hide show
  1. package/README.md +9 -5
  2. package/dist/browser-vp/index.d.cts +0 -11
  3. package/dist/browser-vp/index.d.ts +0 -11
  4. package/dist/index.cjs +13721 -9514
  5. package/dist/index.d.cts +4362 -2375
  6. package/dist/index.d.ts +4362 -2375
  7. package/dist/index.js +13937 -9730
  8. package/package.json +3 -3
  9. package/src/browser-vp/crypto.ts +0 -1
  10. package/src/browser-vp/index.ts +0 -2
  11. package/src/browser-vp/prf.ts +0 -2
  12. package/src/browser-vp/storage.ts +0 -2
  13. package/src/browser-vp/types.ts +0 -2
  14. package/src/browser-vp/vp.ts +0 -2
  15. package/src/composables/index.ts +6 -1
  16. package/src/composables/useAccountTransport.test.ts +121 -0
  17. package/src/composables/useAccountTransport.ts +69 -0
  18. package/src/composables/useBffAuth.ts +44 -34
  19. package/src/generated/google/api/httpbody.ts +159 -0
  20. package/src/generated/google/protobuf/any.ts +326 -0
  21. package/src/generated/google/rpc/error_details.ts +1386 -0
  22. package/src/generated/google/rpc/status.ts +127 -0
  23. package/src/generated/sid/v1/account/account.ts +1 -1
  24. package/src/generated/sid/v1/admin/admin.client.ts +15 -12
  25. package/src/generated/sid/v1/admin/admin.ts +24 -89
  26. package/src/generated/sid/v1/admin/enrollment.client.ts +24 -12
  27. package/src/generated/sid/v1/admin/enrollment.ts +65 -3
  28. package/src/generated/sid/v1/admin/events.client.ts +8 -8
  29. package/src/generated/sid/v1/admin/flow.ts +1 -1
  30. package/src/generated/sid/v1/admin/keys.ts +1 -1
  31. package/src/generated/sid/v1/admin/notifications.client.ts +12 -6
  32. package/src/generated/sid/v1/admin/notifications.ts +11 -70
  33. package/src/generated/sid/v1/admin/operations.ts +20 -35
  34. package/src/generated/sid/v1/admin/organizations.client.ts +2 -2
  35. package/src/generated/sid/v1/admin/organizations.ts +15 -32
  36. package/src/generated/sid/v1/admin/provisioning.client.ts +232 -0
  37. package/src/generated/sid/v1/admin/provisioning.ts +1383 -0
  38. package/src/generated/sid/v1/admin/scopes.ts +1 -1
  39. package/src/generated/sid/v1/admin/security.client.ts +22 -24
  40. package/src/generated/sid/v1/admin/security.ts +13 -22
  41. package/src/generated/sid/v1/admin/settings.client.ts +29 -18
  42. package/src/generated/sid/v1/admin/settings.ts +2 -82
  43. package/src/generated/sid/v1/attestation/attestation.client.ts +6 -6
  44. package/src/generated/sid/v1/attestation/attestation.ts +8 -8
  45. package/src/generated/sid/v1/authn/auth.client.ts +101 -58
  46. package/src/generated/sid/v1/authn/auth.ts +842 -414
  47. package/src/generated/sid/v1/authn/flows.ts +0 -12
  48. package/src/generated/sid/v1/authn/issuer.client.ts +489 -0
  49. package/src/generated/sid/v1/authn/issuer.ts +1600 -0
  50. package/src/generated/sid/v1/authn/password_history.client.ts +57 -0
  51. package/src/generated/sid/v1/authn/password_history.ts +578 -0
  52. package/src/generated/sid/v1/authn/system_integration.client.ts +73 -0
  53. package/src/generated/sid/v1/authn/system_integration.ts +218 -0
  54. package/src/generated/sid/v1/authz/authz.ts +1498 -317
  55. package/src/generated/sid/v1/authz/forward_auth.client.ts +97 -0
  56. package/src/generated/sid/v1/authz/forward_auth.ts +79 -0
  57. package/src/generated/sid/v1/authz/governance.client.ts +2 -2
  58. package/src/generated/sid/v1/authz/governance.ts +61 -4
  59. package/src/generated/sid/v1/common/errors.ts +532 -6
  60. package/src/generated/sid/v1/events/event_stream.client.ts +2 -2
  61. package/src/generated/sid/v1/events/event_stream.ts +2 -1
  62. package/src/generated/sid/v1/identity/identity.ts +98 -58
  63. package/src/generated/sid/v1/ids/ids.ts +395 -0
  64. package/src/generated/sid/v1/machine/machine_user.ts +0 -36
  65. package/src/generated/sid/v1/machine/pat.client.ts +2 -19
  66. package/src/generated/sid/v1/machine/pat.ts +1 -186
  67. package/src/generated/sid/v1/projects/projects.client.ts +137 -19
  68. package/src/generated/sid/v1/projects/projects.ts +2105 -405
  69. package/src/generated/sid/v1/scim/scim.client.ts +248 -2
  70. package/src/generated/sid/v1/scim/scim.ts +153 -14
  71. package/src/generated/sid/v1/test/test.ts +1 -1
  72. package/src/i18n/index.ts +0 -11
  73. package/src/index.ts +36 -2
  74. package/src/package-exports.test.ts +18 -0
  75. package/src/profile/components/SidLoginForm.vue +5 -2
  76. package/src/profile/components/SidRegistrationForm.test.ts +58 -1
  77. package/src/profile/components/SidRegistrationForm.vue +110 -12
  78. package/src/profile/components/index.ts +4 -0
  79. package/src/profile/composables/index.ts +2 -1
  80. package/src/profile/composables/useAccountApi.ts +20 -7
  81. package/src/profile/composables/useAuth.test.ts +453 -0
  82. package/src/profile/composables/useAuth.ts +414 -0
  83. package/src/profile/composables/useAuthApi.opaque.test.ts +18 -0
  84. package/src/profile/composables/useAuthApi.ts +188 -20
  85. package/src/profile/composables/useEnrollmentApi.test.ts +33 -0
  86. package/src/profile/composables/useEnrollmentApi.ts +26 -0
  87. package/src/profile/composables/useIdentityApi.ts +17 -7
  88. package/src/profile/composables/useLoginSuccess.test.ts +97 -3
  89. package/src/profile/composables/useLoginSuccess.ts +80 -22
  90. package/src/profile/composables/useSecurityLevel.test.ts +10 -3
  91. package/src/profile/composables/useSecurityLevel.ts +12 -3
  92. package/src/quasar/SidPrincipalInput.vue +4 -7
  93. package/src/utils/normalizePrincipal.test.ts +19 -5
  94. package/src/utils/normalizePrincipal.ts +7 -9
  95. package/src/utils/rpcRefusal.test.ts +151 -0
  96. package/src/utils/rpcRefusal.ts +252 -0
  97. package/src/utils/validators.test.ts +9 -0
  98. package/src/utils/validators.ts +14 -2
  99. package/src/composables/useAuth.ts +0 -40
  100. package/src/profile/composables/useOpaqueAuth.test.ts +0 -290
  101. package/src/profile/composables/useOpaqueAuth.ts +0 -191
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@structured-id/ui-core",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -19,7 +19,7 @@
19
19
  "exports": {
20
20
  ".": {
21
21
  "types": "./dist/index.d.ts",
22
- "import": "./dist/index.js",
22
+ "import": "./src/index.ts",
23
23
  "require": "./dist/index.cjs"
24
24
  },
25
25
  "./proto": {
@@ -94,6 +94,6 @@
94
94
  "optional": true
95
95
  }
96
96
  },
97
- "packageManager": "yarn@4.12.0",
97
+ "packageManager": "yarn@4.14.1",
98
98
  "license": "AGPL-3.0-only"
99
99
  }
@@ -2,7 +2,6 @@
2
2
  * Cryptographic primitives for Browser VP.
3
3
  *
4
4
  * All operations use Web Crypto API. Keys are non-extractable.
5
- * See arch/auth/browser-vp.md § Key derivation.
6
5
  */
7
6
 
8
7
  import type { DerivedKeys } from "./types";
@@ -10,8 +10,6 @@
10
10
  * - **Strategy B (device-key):** Random WebCrypto key, wrapped for sync.
11
11
  * Each browser = separate device. Fallback for non-PRF browsers.
12
12
  *
13
- * See arch/auth/browser-vp.md for full specification.
14
- *
15
13
  * Reference: Prudnikov, D. (2026). "Client-Side Verifiable Presentation System
16
14
  * with Passkey-Derived Persistent Keys and Pairwise Service Bindings."
17
15
  * doi:10.5281/zenodo.19387768 — https://doi.org/10.5281/zenodo.19387768
@@ -4,8 +4,6 @@
4
4
  * PRF (Pseudo-Random Function) extension allows deriving
5
5
  * deterministic secrets from a passkey. Same passkey on any
6
6
  * synced device produces the same PRF output → same keys.
7
- *
8
- * See arch/auth/browser-vp.md § Strategy A: Passkey-Derived PPK.
9
7
  */
10
8
 
11
9
  const PRF_EVAL_LABEL = "sid-ppk-v1";
@@ -4,8 +4,6 @@
4
4
  * Database: sid-browser-vp
5
5
  * ├── bindings — per-service binding entries (key: service_sector)
6
6
  * └── profile-state — profile state + encrypted blob (key: profile_id)
7
- *
8
- * See arch/auth/browser-vp.md § IndexedDB Storage Schema.
9
7
  */
10
8
 
11
9
  import type { ProfileState, BindingEntry } from "./types";
@@ -1,7 +1,5 @@
1
1
  /**
2
2
  * Browser VP types.
3
- *
4
- * See arch/auth/browser-vp.md for full specification.
5
3
  */
6
4
 
7
5
  /** VP strategy — automatically selected based on browser capabilities. */
@@ -3,8 +3,6 @@
3
3
  *
4
4
  * Builds and signs VPs using cached keys and profile blob.
5
5
  * VP creation is fully offline (~50ms with biometric cache).
6
- *
7
- * See arch/auth/browser-vp.md § VP Creation.
8
6
  */
9
7
 
10
8
  import type { VpRequest, VerifiablePresentation, BindingEntry } from "./types";
@@ -1,4 +1,3 @@
1
- export { useAuth } from "./useAuth";
2
1
  export {
3
2
  useGrpc,
4
3
  initGrpc,
@@ -7,4 +6,10 @@ export {
7
6
  getTransportType,
8
7
  type TransportType,
9
8
  } from "./useGrpc";
9
+ export {
10
+ initAccountApi,
11
+ getAccountTransport,
12
+ closeAccountTransport,
13
+ type AccountApiConfig,
14
+ } from "./useAccountTransport";
10
15
  export { useSession, type SessionCallbacks } from "./useSession";
@@ -0,0 +1,121 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import type {
3
+ MethodInfo,
4
+ RpcInterceptor,
5
+ RpcOptions,
6
+ } from "@protobuf-ts/runtime-rpc";
7
+
8
+ const created: Array<{
9
+ baseUrl: string;
10
+ fetchInit?: RequestInit;
11
+ interceptors?: RpcInterceptor[];
12
+ }> = [];
13
+
14
+ vi.mock("@protobuf-ts/grpcweb-transport", () => ({
15
+ GrpcWebFetchTransport: vi.fn().mockImplementation((options) => {
16
+ created.push(options);
17
+ return { type: "grpc-web-transport" };
18
+ }),
19
+ }));
20
+
21
+ import {
22
+ initAccountApi,
23
+ getAccountTransport,
24
+ closeAccountTransport,
25
+ } from "./useAccountTransport";
26
+ import { bffLogout, checkBffSession, initBff } from "./useBffAuth";
27
+
28
+ /** Run the transport's interceptor over a call and return the options it passed on. */
29
+ function intercepted(options: RpcOptions): RpcOptions {
30
+ const [interceptor] = created[created.length - 1].interceptors ?? [];
31
+ let passed: RpcOptions | undefined;
32
+ interceptor.interceptUnary!(
33
+ (_method, _input, opts) => {
34
+ passed = opts;
35
+ return {} as never;
36
+ },
37
+ {} as MethodInfo,
38
+ {},
39
+ options,
40
+ );
41
+ return passed!;
42
+ }
43
+
44
+ /** The BFF's answer to the page's session check: the user with the session's token. */
45
+ function bffSession(csrfToken: string) {
46
+ vi.stubGlobal(
47
+ "fetch",
48
+ vi.fn().mockResolvedValue({
49
+ ok: true,
50
+ status: 200,
51
+ json: async () => ({ sub: "up_1", csrf_token: csrfToken }),
52
+ }),
53
+ );
54
+ }
55
+
56
+ /** The BFF's answer without a session. */
57
+ function noBffSession() {
58
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 401 }));
59
+ }
60
+
61
+ describe("account API transport", () => {
62
+ beforeEach(async () => {
63
+ created.length = 0;
64
+ closeAccountTransport();
65
+ initBff("");
66
+ noBffSession();
67
+ await checkBffSession();
68
+ });
69
+
70
+ afterEach(() => {
71
+ vi.unstubAllGlobals();
72
+ });
73
+
74
+ // The account API is reached only through the BFF on the page's own origin:
75
+ // its cookie rides along same-origin, and no other origin gets it.
76
+ it("goes to the BFF's API route with same-origin credentials", () => {
77
+ initAccountApi({ baseUrl: "/api" });
78
+ expect(created[0].baseUrl).toBe("/api");
79
+ expect(created[0].fetchInit).toEqual({ credentials: "same-origin" });
80
+ });
81
+
82
+ // Every call echoes the session's CSRF token, which the BFF hands the page
83
+ // with the session check: the BFF refuses a call without it, which is what
84
+ // stops a foreign page driving the account API.
85
+ it("sends the CSRF token the BFF gave with the session on every call", async () => {
86
+ initAccountApi({ baseUrl: "/api" });
87
+ bffSession("token-1");
88
+ await checkBffSession();
89
+ const options = intercepted({ meta: { other: "kept" } });
90
+ expect(options.meta).toEqual({ other: "kept", "x-csrf-token": "token-1" });
91
+ });
92
+
93
+ // No session, no header: the call goes out and the BFF answers it (401 or
94
+ // 403), rather than the page inventing a token.
95
+ it("sends no CSRF header without a session", () => {
96
+ initAccountApi({ baseUrl: "/api" });
97
+ const options = intercepted({ meta: {} });
98
+ expect(options.meta).toEqual({});
99
+ });
100
+
101
+ // Sign-out ends the session; its token goes with it.
102
+ it("forgets the token at sign-out", async () => {
103
+ initAccountApi({ baseUrl: "/api" });
104
+ bffSession("token-1");
105
+ await checkBffSession();
106
+ await bffLogout();
107
+ const options = intercepted({ meta: {} });
108
+ expect(options.meta).toEqual({});
109
+ });
110
+
111
+ it("is unusable before it is set up", () => {
112
+ expect(() => getAccountTransport()).toThrow(
113
+ "account API transport not initialized",
114
+ );
115
+ });
116
+
117
+ it("is the one set up", () => {
118
+ initAccountApi({ baseUrl: "/api" });
119
+ expect(getAccountTransport()).toEqual({ type: "grpc-web-transport" });
120
+ });
121
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The account API transport: gRPC-Web through the BFF (sid-auth-proxy) on the
3
+ * page's own origin. The BFF holds the session's tokens and adds the Bearer
4
+ * token itself; the page sends only its session cookie and, on every call,
5
+ * the CSRF token the BFF set (double submit).
6
+ *
7
+ * Sign-in ceremonies are not account API calls: they go to SID's own sign-in
8
+ * surface through `getTransport()`, where the IdP's session lives.
9
+ */
10
+ import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
11
+ import type { RpcInterceptor, RpcTransport } from "@protobuf-ts/runtime-rpc";
12
+ import { bffCsrfToken } from "./useBffAuth";
13
+
14
+ /** Where the account API is reached. */
15
+ export interface AccountApiConfig {
16
+ /** The BFF's API route, usually `/api` on the page's own origin. */
17
+ baseUrl: string;
18
+ }
19
+
20
+ let _account: RpcTransport | null = null;
21
+
22
+ /** The CSRF token of the BFF session, added to every call's metadata. */
23
+ const csrf: RpcInterceptor = {
24
+ interceptUnary(next, method, input, options) {
25
+ const token = bffCsrfToken();
26
+ return next(
27
+ method,
28
+ input,
29
+ token
30
+ ? { ...options, meta: { ...options.meta, "x-csrf-token": token } }
31
+ : options,
32
+ );
33
+ },
34
+ interceptServerStreaming(next, method, input, options) {
35
+ const token = bffCsrfToken();
36
+ return next(
37
+ method,
38
+ input,
39
+ token
40
+ ? { ...options, meta: { ...options.meta, "x-csrf-token": token } }
41
+ : options,
42
+ );
43
+ },
44
+ };
45
+
46
+ /** Set up the account API transport. Call once at app startup. */
47
+ export function initAccountApi(config: AccountApiConfig): void {
48
+ _account = new GrpcWebFetchTransport({
49
+ baseUrl: config.baseUrl,
50
+ // The BFF is on this origin; its cookie must not travel anywhere else.
51
+ fetchInit: { credentials: "same-origin" },
52
+ interceptors: [csrf],
53
+ });
54
+ }
55
+
56
+ /** The account API transport. Throws if it was not set up. */
57
+ export function getAccountTransport(): RpcTransport {
58
+ if (!_account) {
59
+ throw new Error(
60
+ "account API transport not initialized. Call initAccountApi() at app startup.",
61
+ );
62
+ }
63
+ return _account;
64
+ }
65
+
66
+ /** Drop the account API transport (tests, app teardown). */
67
+ export function closeAccountTransport(): void {
68
+ _account = null;
69
+ }
@@ -1,22 +1,22 @@
1
1
  /**
2
2
  * BFF (Backend-For-Frontend) session management composable.
3
3
  *
4
- * After migration from localStorage to cookie-based auth (#536i-3):
5
- * - Tokens are stored server-side in sid-auth-proxy BFF sessions
6
- * - SPA only holds a non-httpOnly CSRF cookie + httpOnly session cookie
7
- * - gRPC calls go through BFF proxy which injects Bearer header
8
- * - Client JS never sees the access token
4
+ * The tokens live in sid-auth-proxy's BFF session; the page holds only the
5
+ * BFF's httpOnly session cookie. gRPC calls go through the BFF, which adds
6
+ * the Bearer token itself, so the page never sees an access token. The
7
+ * session's CSRF token comes with the session check and every state-changing
8
+ * call echoes it in `X-CSRF-Token`.
9
9
  *
10
10
  * Usage:
11
- * 1. Call `initBff(bffBaseUrl)` at app boot (e.g., VITE_BFF_URL)
12
- * 2. Call `checkBffSession()` to verify session + populate auth store
13
- * 3. Call `startBffLogin()` to redirect user to PKCE auth flow
14
- * 4. Call `bffLogout()` to clear session (sends X-CSRF-Token header)
11
+ * 1. Call `initBff(bffBaseUrl)` at app boot
12
+ * 2. Call `checkBffSession()` to verify the session + populate the auth store
13
+ * 3. Call `startBffLogin()` to redirect the user to the PKCE auth flow
14
+ * 4. Call `bffLogout()` to clear the session
15
15
  */
16
16
 
17
17
  /** User info returned from BFF /auth/userinfo endpoint. */
18
18
  export interface BffUserInfo {
19
- /** Subject identifier (ProfileId for CE, PersonaId for SaaS). */
19
+ /** Opaque subject identifier; its form depends on the issuer, so it is never parsed as a profile id. */
20
20
  sub: string;
21
21
  /** Email address (if available in session claims). */
22
22
  email?: string | null;
@@ -30,20 +30,24 @@ export interface BffUserInfo {
30
30
  acr?: string | null;
31
31
  /** Role assignments. */
32
32
  roles?: string[] | null;
33
+ /** The session's CSRF token, echoed in `X-CSRF-Token` by every state-changing call. */
34
+ csrf_token?: string | null;
33
35
  }
34
36
 
35
37
  let _bffBaseUrl: string | null = null;
36
- let _bffCookieName: string = "__Host-sid-bff";
38
+ /**
39
+ * The CSRF token of the session the page last checked. Browser-only state:
40
+ * the session check runs in the browser, never during server rendering.
41
+ */
42
+ let _csrfToken: string | null = null;
37
43
 
38
44
  /**
39
45
  * Initialize BFF configuration. Call once at app boot.
40
46
  *
41
- * @param baseUrl BFF base URL (e.g., "https://app.structured.id" or VITE_BFF_URL)
42
- * @param cookieName BFF session cookie name (default: "__Host-sid-bff")
47
+ * @param baseUrl BFF base URL: "" for the page's own origin, where the BFF usually is
43
48
  */
44
- export function initBff(baseUrl: string, cookieName?: string): void {
49
+ export function initBff(baseUrl: string): void {
45
50
  _bffBaseUrl = baseUrl;
46
- if (cookieName) _bffCookieName = cookieName;
47
51
  }
48
52
 
49
53
  /** Get the configured BFF base URL, or null if not initialized. */
@@ -52,35 +56,37 @@ export function getBffBaseUrl(): string | null {
52
56
  }
53
57
 
54
58
  /**
55
- * Read the CSRF token from the non-httpOnly CSRF cookie.
56
- * The BFF sets a cookie `{cookieName}_csrf=<token>; SameSite=Lax` (NOT httpOnly)
57
- * so JS can read it for double-submit CSRF protection.
59
+ * The CSRF token the BFF gave with the session check, or null without a
60
+ * checked session. The BFF refuses a state-changing call without it.
58
61
  */
59
- function readCsrfToken(): string | null {
60
- const name = `${_bffCookieName}_csrf=`;
61
- const cookies = document.cookie.split("; ");
62
- const found = cookies.find((c) => c.startsWith(name));
63
- return found ? (found.split("=")[1] ?? null) : null;
62
+ export function bffCsrfToken(): string | null {
63
+ return _csrfToken;
64
64
  }
65
65
 
66
66
  /**
67
67
  * Check if the BFF session is valid by calling GET /auth/userinfo.
68
68
  *
69
- * Returns user info if session is active, null if unauthenticated.
69
+ * Returns user info if session is active, null if unauthenticated, and
70
+ * keeps the session's CSRF token for the calls that follow.
70
71
  * The browser automatically sends the httpOnly session cookie.
72
+ * An empty base URL is the page's own origin, where the BFF usually is.
71
73
  */
72
74
  export async function checkBffSession(
73
75
  bffBaseUrl?: string,
74
76
  ): Promise<BffUserInfo | null> {
75
77
  const base = bffBaseUrl ?? _bffBaseUrl;
76
- if (!base) return null;
78
+ if (base === null) return null;
77
79
  try {
78
80
  const resp = await fetch(`${base}/auth/userinfo`, {
79
81
  credentials: "include",
80
82
  });
81
- if (resp.status === 401 || resp.status === 404) return null;
82
- if (!resp.ok) return null;
83
- return (await resp.json()) as BffUserInfo;
83
+ if (!resp.ok) {
84
+ _csrfToken = null;
85
+ return null;
86
+ }
87
+ const info = (await resp.json()) as BffUserInfo;
88
+ _csrfToken = info.csrf_token ?? null;
89
+ return info;
84
90
  } catch {
85
91
  return null;
86
92
  }
@@ -93,19 +99,21 @@ export async function checkBffSession(
93
99
  * back to the SPA. The SPA should call checkBffSession() on load to restore state.
94
100
  *
95
101
  * @param bffBaseUrl Override BFF base URL (uses configured URL if not provided)
96
- * @param postLoginRedirect URL to redirect to after successful auth
102
+ * @param postLoginRedirect Path on the BFF's origin to return to after
103
+ * sign-in (`/account/security?tab=keys`), sent as the BFF's `rd`
104
+ * parameter; the BFF returns anything else to its root
97
105
  */
98
106
  export function startBffLogin(
99
107
  bffBaseUrl?: string,
100
108
  postLoginRedirect?: string,
101
109
  ): void {
102
110
  const base = bffBaseUrl ?? _bffBaseUrl;
103
- if (!base) {
111
+ if (base === null) {
104
112
  console.warn("[BFF] Not initialized. Call initBff() at app boot.");
105
113
  return;
106
114
  }
107
115
  const url = postLoginRedirect
108
- ? `${base}/auth/login?post_login_redirect=${encodeURIComponent(postLoginRedirect)}`
116
+ ? `${base}/auth/login?rd=${encodeURIComponent(postLoginRedirect)}`
109
117
  : `${base}/auth/login`;
110
118
  window.location.href = url;
111
119
  }
@@ -114,14 +122,16 @@ export function startBffLogin(
114
122
  * Log out by calling POST /auth/logout with X-CSRF-Token header.
115
123
  *
116
124
  * The BFF validates the CSRF header, clears server-side session,
117
- * and sends Set-Cookie headers to clear both cookies.
125
+ * and sends a Set-Cookie header to clear the session cookie. The token
126
+ * dies with the session either way.
118
127
  *
119
128
  * @param bffBaseUrl Override BFF base URL
120
129
  */
121
130
  export async function bffLogout(bffBaseUrl?: string): Promise<void> {
122
131
  const base = bffBaseUrl ?? _bffBaseUrl;
123
- if (!base) return;
124
- const csrf = readCsrfToken();
132
+ if (base === null) return;
133
+ const csrf = _csrfToken;
134
+ _csrfToken = null;
125
135
  try {
126
136
  await fetch(`${base}/auth/logout`, {
127
137
  method: "POST",
@@ -0,0 +1,159 @@
1
+ // @generated by protobuf-ts 2.11.1 with parameter client_generic,generate_dependencies
2
+ // @generated from protobuf file "google/api/httpbody.proto" (package "google.api", syntax proto3)
3
+ // tslint:disable
4
+ //
5
+ // Copyright 2026 Google LLC
6
+ //
7
+ // Licensed under the Apache License, Version 2.0 (the "License");
8
+ // you may not use this file except in compliance with the License.
9
+ // You may obtain a copy of the License at
10
+ //
11
+ // http://www.apache.org/licenses/LICENSE-2.0
12
+ //
13
+ // Unless required by applicable law or agreed to in writing, software
14
+ // distributed under the License is distributed on an "AS IS" BASIS,
15
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ // See the License for the specific language governing permissions and
17
+ // limitations under the License.
18
+ //
19
+ import type { BinaryWriteOptions } from "@protobuf-ts/runtime";
20
+ import type { IBinaryWriter } from "@protobuf-ts/runtime";
21
+ import { WireType } from "@protobuf-ts/runtime";
22
+ import type { BinaryReadOptions } from "@protobuf-ts/runtime";
23
+ import type { IBinaryReader } from "@protobuf-ts/runtime";
24
+ import { UnknownFieldHandler } from "@protobuf-ts/runtime";
25
+ import type { PartialMessage } from "@protobuf-ts/runtime";
26
+ import { reflectionMergePartial } from "@protobuf-ts/runtime";
27
+ import { MessageType } from "@protobuf-ts/runtime";
28
+ import { Any } from "../protobuf/any";
29
+ /**
30
+ * Message that represents an arbitrary HTTP body. It should only be used for
31
+ * payload formats that can't be represented as JSON, such as raw binary or
32
+ * an HTML page.
33
+ *
34
+ *
35
+ * This message can be used both in streaming and non-streaming API methods in
36
+ * the request as well as the response.
37
+ *
38
+ * It can be used as a top-level request field, which is convenient if one
39
+ * wants to extract parameters from either the URL or HTTP template into the
40
+ * request fields and also want access to the raw HTTP body.
41
+ *
42
+ * Example:
43
+ *
44
+ * message GetResourceRequest {
45
+ * // A unique request id.
46
+ * string request_id = 1;
47
+ *
48
+ * // The raw HTTP body is bound to this field.
49
+ * google.api.HttpBody http_body = 2;
50
+ *
51
+ * }
52
+ *
53
+ * service ResourceService {
54
+ * rpc GetResource(GetResourceRequest)
55
+ * returns (google.api.HttpBody);
56
+ * rpc UpdateResource(google.api.HttpBody)
57
+ * returns (google.protobuf.Empty);
58
+ *
59
+ * }
60
+ *
61
+ * Example with streaming methods:
62
+ *
63
+ * service CaldavService {
64
+ * rpc GetCalendar(stream google.api.HttpBody)
65
+ * returns (stream google.api.HttpBody);
66
+ * rpc UpdateCalendar(stream google.api.HttpBody)
67
+ * returns (stream google.api.HttpBody);
68
+ *
69
+ * }
70
+ *
71
+ * Use of this type only changes how the request and response bodies are
72
+ * handled, all other features will continue to work unchanged.
73
+ *
74
+ * @generated from protobuf message google.api.HttpBody
75
+ */
76
+ export interface HttpBody {
77
+ /**
78
+ * The HTTP Content-Type header value specifying the content type of the body.
79
+ *
80
+ * @generated from protobuf field: string content_type = 1
81
+ */
82
+ contentType: string;
83
+ /**
84
+ * The HTTP request/response body as raw binary.
85
+ *
86
+ * @generated from protobuf field: bytes data = 2
87
+ */
88
+ data: Uint8Array;
89
+ /**
90
+ * Application specific response metadata. Must be set in the first response
91
+ * for streaming APIs.
92
+ *
93
+ * @generated from protobuf field: repeated google.protobuf.Any extensions = 3
94
+ */
95
+ extensions: Any[];
96
+ }
97
+ // @generated message type with reflection information, may provide speed optimized methods
98
+ class HttpBody$Type extends MessageType<HttpBody> {
99
+ constructor() {
100
+ super("google.api.HttpBody", [
101
+ { no: 1, name: "content_type", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
102
+ { no: 2, name: "data", kind: "scalar", T: 12 /*ScalarType.BYTES*/ },
103
+ { no: 3, name: "extensions", kind: "message", repeat: 2 /*RepeatType.UNPACKED*/, T: () => Any }
104
+ ]);
105
+ }
106
+ create(value?: PartialMessage<HttpBody>): HttpBody {
107
+ const message = globalThis.Object.create((this.messagePrototype!));
108
+ message.contentType = "";
109
+ message.data = new Uint8Array(0);
110
+ message.extensions = [];
111
+ if (value !== undefined)
112
+ reflectionMergePartial<HttpBody>(this, message, value);
113
+ return message;
114
+ }
115
+ internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: HttpBody): HttpBody {
116
+ let message = target ?? this.create(), end = reader.pos + length;
117
+ while (reader.pos < end) {
118
+ let [fieldNo, wireType] = reader.tag();
119
+ switch (fieldNo) {
120
+ case /* string content_type */ 1:
121
+ message.contentType = reader.string();
122
+ break;
123
+ case /* bytes data */ 2:
124
+ message.data = reader.bytes();
125
+ break;
126
+ case /* repeated google.protobuf.Any extensions */ 3:
127
+ message.extensions.push(Any.internalBinaryRead(reader, reader.uint32(), options));
128
+ break;
129
+ default:
130
+ let u = options.readUnknownField;
131
+ if (u === "throw")
132
+ throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
133
+ let d = reader.skip(wireType);
134
+ if (u !== false)
135
+ (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
136
+ }
137
+ }
138
+ return message;
139
+ }
140
+ internalBinaryWrite(message: HttpBody, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
141
+ /* string content_type = 1; */
142
+ if (message.contentType !== "")
143
+ writer.tag(1, WireType.LengthDelimited).string(message.contentType);
144
+ /* bytes data = 2; */
145
+ if (message.data.length)
146
+ writer.tag(2, WireType.LengthDelimited).bytes(message.data);
147
+ /* repeated google.protobuf.Any extensions = 3; */
148
+ for (let i = 0; i < message.extensions.length; i++)
149
+ Any.internalBinaryWrite(message.extensions[i], writer.tag(3, WireType.LengthDelimited).fork(), options).join();
150
+ let u = options.writeUnknownFields;
151
+ if (u !== false)
152
+ (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
153
+ return writer;
154
+ }
155
+ }
156
+ /**
157
+ * @generated MessageType for protobuf message google.api.HttpBody
158
+ */
159
+ export const HttpBody = new HttpBody$Type();