@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.
- package/README.md +9 -5
- package/dist/browser-vp/index.d.cts +0 -11
- package/dist/browser-vp/index.d.ts +0 -11
- package/dist/index.cjs +13721 -9514
- package/dist/index.d.cts +4362 -2375
- package/dist/index.d.ts +4362 -2375
- package/dist/index.js +13937 -9730
- package/package.json +3 -3
- package/src/browser-vp/crypto.ts +0 -1
- package/src/browser-vp/index.ts +0 -2
- package/src/browser-vp/prf.ts +0 -2
- package/src/browser-vp/storage.ts +0 -2
- package/src/browser-vp/types.ts +0 -2
- package/src/browser-vp/vp.ts +0 -2
- package/src/composables/index.ts +6 -1
- package/src/composables/useAccountTransport.test.ts +121 -0
- package/src/composables/useAccountTransport.ts +69 -0
- package/src/composables/useBffAuth.ts +44 -34
- package/src/generated/google/api/httpbody.ts +159 -0
- package/src/generated/google/protobuf/any.ts +326 -0
- package/src/generated/google/rpc/error_details.ts +1386 -0
- package/src/generated/google/rpc/status.ts +127 -0
- package/src/generated/sid/v1/account/account.ts +1 -1
- package/src/generated/sid/v1/admin/admin.client.ts +15 -12
- package/src/generated/sid/v1/admin/admin.ts +24 -89
- package/src/generated/sid/v1/admin/enrollment.client.ts +24 -12
- package/src/generated/sid/v1/admin/enrollment.ts +65 -3
- package/src/generated/sid/v1/admin/events.client.ts +8 -8
- package/src/generated/sid/v1/admin/flow.ts +1 -1
- package/src/generated/sid/v1/admin/keys.ts +1 -1
- package/src/generated/sid/v1/admin/notifications.client.ts +12 -6
- package/src/generated/sid/v1/admin/notifications.ts +11 -70
- package/src/generated/sid/v1/admin/operations.ts +20 -35
- package/src/generated/sid/v1/admin/organizations.client.ts +2 -2
- package/src/generated/sid/v1/admin/organizations.ts +15 -32
- package/src/generated/sid/v1/admin/provisioning.client.ts +232 -0
- package/src/generated/sid/v1/admin/provisioning.ts +1383 -0
- package/src/generated/sid/v1/admin/scopes.ts +1 -1
- package/src/generated/sid/v1/admin/security.client.ts +22 -24
- package/src/generated/sid/v1/admin/security.ts +13 -22
- package/src/generated/sid/v1/admin/settings.client.ts +29 -18
- package/src/generated/sid/v1/admin/settings.ts +2 -82
- package/src/generated/sid/v1/attestation/attestation.client.ts +6 -6
- package/src/generated/sid/v1/attestation/attestation.ts +8 -8
- package/src/generated/sid/v1/authn/auth.client.ts +101 -58
- package/src/generated/sid/v1/authn/auth.ts +842 -414
- package/src/generated/sid/v1/authn/flows.ts +0 -12
- package/src/generated/sid/v1/authn/issuer.client.ts +489 -0
- package/src/generated/sid/v1/authn/issuer.ts +1600 -0
- package/src/generated/sid/v1/authn/password_history.client.ts +57 -0
- package/src/generated/sid/v1/authn/password_history.ts +578 -0
- package/src/generated/sid/v1/authn/system_integration.client.ts +73 -0
- package/src/generated/sid/v1/authn/system_integration.ts +218 -0
- package/src/generated/sid/v1/authz/authz.ts +1498 -317
- package/src/generated/sid/v1/authz/forward_auth.client.ts +97 -0
- package/src/generated/sid/v1/authz/forward_auth.ts +79 -0
- package/src/generated/sid/v1/authz/governance.client.ts +2 -2
- package/src/generated/sid/v1/authz/governance.ts +61 -4
- package/src/generated/sid/v1/common/errors.ts +532 -6
- package/src/generated/sid/v1/events/event_stream.client.ts +2 -2
- package/src/generated/sid/v1/events/event_stream.ts +2 -1
- package/src/generated/sid/v1/identity/identity.ts +98 -58
- package/src/generated/sid/v1/ids/ids.ts +395 -0
- package/src/generated/sid/v1/machine/machine_user.ts +0 -36
- package/src/generated/sid/v1/machine/pat.client.ts +2 -19
- package/src/generated/sid/v1/machine/pat.ts +1 -186
- package/src/generated/sid/v1/projects/projects.client.ts +137 -19
- package/src/generated/sid/v1/projects/projects.ts +2105 -405
- package/src/generated/sid/v1/scim/scim.client.ts +248 -2
- package/src/generated/sid/v1/scim/scim.ts +153 -14
- package/src/generated/sid/v1/test/test.ts +1 -1
- package/src/i18n/index.ts +0 -11
- package/src/index.ts +36 -2
- package/src/package-exports.test.ts +18 -0
- package/src/profile/components/SidLoginForm.vue +5 -2
- package/src/profile/components/SidRegistrationForm.test.ts +58 -1
- package/src/profile/components/SidRegistrationForm.vue +110 -12
- package/src/profile/components/index.ts +4 -0
- package/src/profile/composables/index.ts +2 -1
- package/src/profile/composables/useAccountApi.ts +20 -7
- package/src/profile/composables/useAuth.test.ts +453 -0
- package/src/profile/composables/useAuth.ts +414 -0
- package/src/profile/composables/useAuthApi.opaque.test.ts +18 -0
- package/src/profile/composables/useAuthApi.ts +188 -20
- package/src/profile/composables/useEnrollmentApi.test.ts +33 -0
- package/src/profile/composables/useEnrollmentApi.ts +26 -0
- package/src/profile/composables/useIdentityApi.ts +17 -7
- package/src/profile/composables/useLoginSuccess.test.ts +97 -3
- package/src/profile/composables/useLoginSuccess.ts +80 -22
- package/src/profile/composables/useSecurityLevel.test.ts +10 -3
- package/src/profile/composables/useSecurityLevel.ts +12 -3
- package/src/quasar/SidPrincipalInput.vue +4 -7
- package/src/utils/normalizePrincipal.test.ts +19 -5
- package/src/utils/normalizePrincipal.ts +7 -9
- package/src/utils/rpcRefusal.test.ts +151 -0
- package/src/utils/rpcRefusal.ts +252 -0
- package/src/utils/validators.test.ts +9 -0
- package/src/utils/validators.ts +14 -2
- package/src/composables/useAuth.ts +0 -40
- package/src/profile/composables/useOpaqueAuth.test.ts +0 -290
- 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.
|
|
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": "./
|
|
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.
|
|
97
|
+
"packageManager": "yarn@4.14.1",
|
|
98
98
|
"license": "AGPL-3.0-only"
|
|
99
99
|
}
|
package/src/browser-vp/crypto.ts
CHANGED
package/src/browser-vp/index.ts
CHANGED
|
@@ -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
|
package/src/browser-vp/prf.ts
CHANGED
|
@@ -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";
|
package/src/browser-vp/types.ts
CHANGED
package/src/browser-vp/vp.ts
CHANGED
|
@@ -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";
|
package/src/composables/index.ts
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
56
|
-
* The BFF
|
|
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
|
|
60
|
-
|
|
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 (
|
|
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.
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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 (
|
|
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?
|
|
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
|
|
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 (
|
|
124
|
-
const csrf =
|
|
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();
|