@structured-id/ui-core 2.0.0 → 3.0.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 (152) hide show
  1. package/README.md +22 -10
  2. package/package.json +25 -43
  3. package/src/components/SidStatusBadge.vue +1 -5
  4. package/src/composables/useAccountTransport.test.ts +2 -1
  5. package/src/composables/useGrpc.test.ts +17 -14
  6. package/src/composables/useGrpc.ts +4 -2
  7. package/src/index.ts +2 -120
  8. package/src/profile/components/ConsentManager.vue +5 -1
  9. package/src/profile/components/ProfileEditor.vue +1 -1
  10. package/src/profile/components/SecuritySettings.vue +5 -1
  11. package/src/profile/components/SessionList.vue +2 -1
  12. package/src/profile/components/SidRegistrationForm.test.ts +173 -1
  13. package/src/profile/components/SidRegistrationForm.vue +88 -32
  14. package/src/profile/components/index.ts +1 -4
  15. package/src/profile/composables/index.ts +1 -0
  16. package/src/profile/composables/operationPlan.test.ts +155 -0
  17. package/src/profile/composables/operationPlan.ts +218 -0
  18. package/src/profile/composables/useAccountApi.ts +4 -4
  19. package/src/profile/composables/useAuth.test.ts +312 -31
  20. package/src/profile/composables/useAuth.ts +176 -30
  21. package/src/profile/composables/useAuthApi.opaque.test.ts +13 -6
  22. package/src/profile/composables/useAuthApi.ts +40 -18
  23. package/src/profile/composables/useCurrentPasswordRequirement.test.ts +196 -0
  24. package/src/profile/composables/useCurrentPasswordRequirement.ts +95 -0
  25. package/src/profile/composables/useEnrollmentApi.test.ts +7 -3
  26. package/src/profile/composables/useEnrollmentApi.ts +3 -5
  27. package/src/profile/composables/useIdentityApi.ts +4 -4
  28. package/src/profile/composables/useSecurityLevel.test.ts +1 -1
  29. package/src/profile/composables/useSecurityLevel.ts +1 -1
  30. package/src/quasar/SidPasswordCeremony.test.ts +253 -0
  31. package/src/quasar/SidPasswordCeremony.vue +456 -0
  32. package/src/quasar/index.ts +2 -0
  33. package/src/utils/rpcRefusal.test.ts +4 -4
  34. package/src/utils/rpcRefusal.ts +8 -4
  35. package/dist/browser-vp/index.cjs +0 -349
  36. package/dist/browser-vp/index.d.cts +0 -276
  37. package/dist/browser-vp/index.d.ts +0 -276
  38. package/dist/browser-vp/index.js +0 -349
  39. package/dist/chunk-C3Q766CY.cjs +0 -125
  40. package/dist/chunk-J5YXRXB5.js +0 -125
  41. package/dist/index.cjs +0 -23581
  42. package/dist/index.d.cts +0 -11539
  43. package/dist/index.d.ts +0 -11539
  44. package/dist/index.js +0 -23581
  45. package/dist/storage-4CB2JTLW.cjs +0 -22
  46. package/dist/storage-THRWREZP.js +0 -22
  47. package/src/browser-vp/crypto.ts +0 -253
  48. package/src/browser-vp/index.test.ts +0 -10
  49. package/src/browser-vp/index.ts +0 -116
  50. package/src/browser-vp/prf.ts +0 -179
  51. package/src/browser-vp/storage.test.ts +0 -9
  52. package/src/browser-vp/storage.ts +0 -163
  53. package/src/browser-vp/types.ts +0 -95
  54. package/src/browser-vp/vp.test.ts +0 -43
  55. package/src/browser-vp/vp.ts +0 -111
  56. package/src/generated/google/api/http.ts +0 -687
  57. package/src/generated/google/api/httpbody.ts +0 -159
  58. package/src/generated/google/protobuf/any.ts +0 -326
  59. package/src/generated/google/protobuf/descriptor.ts +0 -4865
  60. package/src/generated/google/protobuf/duration.ts +0 -230
  61. package/src/generated/google/protobuf/empty.ts +0 -94
  62. package/src/generated/google/protobuf/struct.ts +0 -484
  63. package/src/generated/google/protobuf/timestamp.ts +0 -287
  64. package/src/generated/google/rpc/error_details.ts +0 -1386
  65. package/src/generated/google/rpc/status.ts +0 -127
  66. package/src/generated/sid/v1/account/account.client.ts +0 -527
  67. package/src/generated/sid/v1/account/account.ts +0 -3261
  68. package/src/generated/sid/v1/admin/admin.client.ts +0 -139
  69. package/src/generated/sid/v1/admin/admin.ts +0 -781
  70. package/src/generated/sid/v1/admin/admin_roles.client.ts +0 -181
  71. package/src/generated/sid/v1/admin/admin_roles.ts +0 -1606
  72. package/src/generated/sid/v1/admin/analytics.client.ts +0 -106
  73. package/src/generated/sid/v1/admin/analytics.ts +0 -1017
  74. package/src/generated/sid/v1/admin/branding.client.ts +0 -166
  75. package/src/generated/sid/v1/admin/branding.ts +0 -1906
  76. package/src/generated/sid/v1/admin/enrollment.client.ts +0 -177
  77. package/src/generated/sid/v1/admin/enrollment.ts +0 -1766
  78. package/src/generated/sid/v1/admin/events.client.ts +0 -195
  79. package/src/generated/sid/v1/admin/events.ts +0 -1900
  80. package/src/generated/sid/v1/admin/flow.client.ts +0 -183
  81. package/src/generated/sid/v1/admin/flow.ts +0 -1552
  82. package/src/generated/sid/v1/admin/keys.client.ts +0 -107
  83. package/src/generated/sid/v1/admin/keys.ts +0 -931
  84. package/src/generated/sid/v1/admin/login_experience.client.ts +0 -160
  85. package/src/generated/sid/v1/admin/login_experience.ts +0 -1061
  86. package/src/generated/sid/v1/admin/migration.client.ts +0 -127
  87. package/src/generated/sid/v1/admin/migration.ts +0 -1948
  88. package/src/generated/sid/v1/admin/notifications.client.ts +0 -134
  89. package/src/generated/sid/v1/admin/notifications.ts +0 -1273
  90. package/src/generated/sid/v1/admin/operations.client.ts +0 -241
  91. package/src/generated/sid/v1/admin/operations.ts +0 -2633
  92. package/src/generated/sid/v1/admin/organizations.client.ts +0 -57
  93. package/src/generated/sid/v1/admin/organizations.ts +0 -332
  94. package/src/generated/sid/v1/admin/provisioning.client.ts +0 -232
  95. package/src/generated/sid/v1/admin/provisioning.ts +0 -1383
  96. package/src/generated/sid/v1/admin/scopes.client.ts +0 -171
  97. package/src/generated/sid/v1/admin/scopes.ts +0 -1324
  98. package/src/generated/sid/v1/admin/security.client.ts +0 -410
  99. package/src/generated/sid/v1/admin/security.ts +0 -4077
  100. package/src/generated/sid/v1/admin/sessions.client.ts +0 -114
  101. package/src/generated/sid/v1/admin/sessions.ts +0 -923
  102. package/src/generated/sid/v1/admin/settings.client.ts +0 -351
  103. package/src/generated/sid/v1/admin/settings.ts +0 -2458
  104. package/src/generated/sid/v1/admin/user_profile.client.ts +0 -57
  105. package/src/generated/sid/v1/admin/user_profile.ts +0 -730
  106. package/src/generated/sid/v1/admin/webhooks.client.ts +0 -269
  107. package/src/generated/sid/v1/admin/webhooks.ts +0 -2804
  108. package/src/generated/sid/v1/attestation/attestation.client.ts +0 -115
  109. package/src/generated/sid/v1/attestation/attestation.ts +0 -813
  110. package/src/generated/sid/v1/authn/auth.client.ts +0 -943
  111. package/src/generated/sid/v1/authn/auth.ts +0 -9719
  112. package/src/generated/sid/v1/authn/flows.client.ts +0 -413
  113. package/src/generated/sid/v1/authn/flows.ts +0 -3205
  114. package/src/generated/sid/v1/authn/issuer.client.ts +0 -489
  115. package/src/generated/sid/v1/authn/issuer.ts +0 -1600
  116. package/src/generated/sid/v1/authn/password_history.client.ts +0 -57
  117. package/src/generated/sid/v1/authn/password_history.ts +0 -578
  118. package/src/generated/sid/v1/authn/system_integration.client.ts +0 -73
  119. package/src/generated/sid/v1/authn/system_integration.ts +0 -218
  120. package/src/generated/sid/v1/authz/authz.client.ts +0 -459
  121. package/src/generated/sid/v1/authz/authz.ts +0 -6524
  122. package/src/generated/sid/v1/authz/forward_auth.client.ts +0 -97
  123. package/src/generated/sid/v1/authz/forward_auth.ts +0 -79
  124. package/src/generated/sid/v1/authz/governance.client.ts +0 -102
  125. package/src/generated/sid/v1/authz/governance.ts +0 -875
  126. package/src/generated/sid/v1/common/errors.ts +0 -710
  127. package/src/generated/sid/v1/common/pagination.ts +0 -152
  128. package/src/generated/sid/v1/common/transport.ts +0 -227
  129. package/src/generated/sid/v1/common/types.ts +0 -151
  130. package/src/generated/sid/v1/events/event_stream.client.ts +0 -67
  131. package/src/generated/sid/v1/events/event_stream.ts +0 -375
  132. package/src/generated/sid/v1/federation/upstream.client.ts +0 -142
  133. package/src/generated/sid/v1/federation/upstream.ts +0 -1736
  134. package/src/generated/sid/v1/identity/identity.client.ts +0 -467
  135. package/src/generated/sid/v1/identity/identity.ts +0 -5676
  136. package/src/generated/sid/v1/ids/ids.ts +0 -395
  137. package/src/generated/sid/v1/machine/machine_user.client.ts +0 -219
  138. package/src/generated/sid/v1/machine/machine_user.ts +0 -2364
  139. package/src/generated/sid/v1/machine/pat.client.ts +0 -130
  140. package/src/generated/sid/v1/machine/pat.ts +0 -1083
  141. package/src/generated/sid/v1/projects/projects.client.ts +0 -404
  142. package/src/generated/sid/v1/projects/projects.ts +0 -4446
  143. package/src/generated/sid/v1/scim/scim.client.ts +0 -520
  144. package/src/generated/sid/v1/scim/scim.ts +0 -3182
  145. package/src/generated/sid/v1/test/test.client.ts +0 -232
  146. package/src/generated/sid/v1/test/test.ts +0 -1136
  147. package/src/transport/adaptive.test.ts +0 -728
  148. package/src/transport/adaptive.ts +0 -308
  149. package/src/transport/framing.test.ts +0 -227
  150. package/src/transport/framing.ts +0 -98
  151. package/src/transport/webtransport.test.ts +0 -728
  152. package/src/transport/webtransport.ts +0 -323
@@ -1,728 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { Deferred, RpcError, UnaryCall } from "@protobuf-ts/runtime-rpc";
3
- import type {
4
- RpcTransport,
5
- MethodInfo,
6
- RpcOptions,
7
- RpcMetadata,
8
- RpcStatus,
9
- } from "@protobuf-ts/runtime-rpc";
10
-
11
- // ---------------------------------------------------------------------------
12
- // Mock WebTransport global (does not exist in Node.js)
13
- // ---------------------------------------------------------------------------
14
-
15
- class MockWebTransport {
16
- ready = Promise.resolve();
17
- closed = new Promise<void>(() => {});
18
- close = vi.fn();
19
- createBidirectionalStream = vi.fn();
20
- }
21
-
22
- // ---------------------------------------------------------------------------
23
- // Mock modules BEFORE importing the module under test
24
- // ---------------------------------------------------------------------------
25
-
26
- // Mock GrpcWebFetchTransport
27
- vi.mock("@protobuf-ts/grpcweb-transport", () => ({
28
- GrpcWebFetchTransport: vi
29
- .fn()
30
- .mockImplementation(() => createMockTransport()),
31
- }));
32
-
33
- // Mock WebTransportConnection and WebTransportRpcTransport
34
- const mockWtConnect = vi.fn<[], Promise<void>>();
35
- const mockWtClose = vi.fn();
36
- let mockWtTransportInstance: RpcTransport;
37
-
38
- vi.mock("./webtransport", () => ({
39
- WebTransportConnection: vi.fn().mockImplementation(() => ({
40
- connect: mockWtConnect,
41
- close: mockWtClose,
42
- })),
43
- WebTransportRpcTransport: vi.fn().mockImplementation(() => {
44
- return mockWtTransportInstance;
45
- }),
46
- }));
47
-
48
- // Now import the module under test (AFTER vi.mock calls)
49
- import { AdaptiveRpcTransport } from "./adaptive";
50
- import type { AdaptiveTransportOptions } from "./adaptive";
51
-
52
- // ---------------------------------------------------------------------------
53
- // Helpers
54
- // ---------------------------------------------------------------------------
55
-
56
- interface MockMsg {
57
- value: string;
58
- }
59
-
60
- function createMethodInfo(): MethodInfo<MockMsg, MockMsg> {
61
- return {
62
- service: { typeName: "test.TestService", methods: [], options: {} },
63
- name: "TestMethod",
64
- localName: "testMethod",
65
- idempotency: undefined,
66
- serverStreaming: false,
67
- clientStreaming: false,
68
- I: {
69
- typeName: "TestInput",
70
- toBinary: () => new Uint8Array(),
71
- fromBinary: () => ({ value: "input" }),
72
- },
73
- O: {
74
- typeName: "TestOutput",
75
- toBinary: () => new Uint8Array(),
76
- fromBinary: () => ({ value: "output" }),
77
- },
78
- options: {},
79
- } as unknown as MethodInfo<MockMsg, MockMsg>;
80
- }
81
-
82
- function createMockTransport(): RpcTransport {
83
- return {
84
- mergeOptions: (opts?: Partial<RpcOptions>) => opts ?? {},
85
- unary: vi.fn(),
86
- serverStreaming: vi.fn(),
87
- clientStreaming: vi.fn(),
88
- duplex: vi.fn(),
89
- } as unknown as RpcTransport;
90
- }
91
-
92
- /** Create a resolved UnaryCall with the given response. */
93
- function resolvedUnaryCall<I extends object, O extends object>(
94
- method: MethodInfo<I, O>,
95
- input: I,
96
- response: O,
97
- ): UnaryCall<I, O> {
98
- const dH = new Deferred<RpcMetadata>();
99
- const dM = new Deferred<O>();
100
- const dS = new Deferred<RpcStatus>();
101
- const dT = new Deferred<RpcMetadata>();
102
-
103
- dH.resolve({});
104
- dM.resolve(response);
105
- dS.resolve({ code: "OK", detail: "" });
106
- dT.resolve({});
107
-
108
- return new UnaryCall(
109
- method,
110
- {},
111
- input,
112
- dH.promise,
113
- dM.promise,
114
- dS.promise,
115
- dT.promise,
116
- );
117
- }
118
-
119
- /** Create a rejected UnaryCall that fails with the given RpcError. */
120
- function rejectedUnaryCall<I extends object, O extends object>(
121
- method: MethodInfo<I, O>,
122
- input: I,
123
- error: RpcError,
124
- ): UnaryCall<I, O> {
125
- const dH = new Deferred<RpcMetadata>();
126
- const dM = new Deferred<O>();
127
- const dS = new Deferred<RpcStatus>();
128
- const dT = new Deferred<RpcMetadata>();
129
-
130
- dH.reject(error);
131
- dM.reject(error);
132
- dS.reject(error);
133
- dT.reject(error);
134
-
135
- return new UnaryCall(
136
- method,
137
- {},
138
- input,
139
- dH.promise,
140
- dM.promise,
141
- dS.promise,
142
- dT.promise,
143
- );
144
- }
145
-
146
- function getGrpcWebTransport(adaptive: AdaptiveRpcTransport): RpcTransport {
147
- // Access the private fallback field
148
- return (adaptive as unknown as { fallback: RpcTransport }).fallback;
149
- }
150
-
151
- // ---------------------------------------------------------------------------
152
- // Tests
153
- // ---------------------------------------------------------------------------
154
-
155
- describe("AdaptiveRpcTransport", () => {
156
- const method = createMethodInfo();
157
- const input: MockMsg = { value: "hello" };
158
- const options: RpcOptions = { meta: {} };
159
-
160
- beforeEach(() => {
161
- vi.useFakeTimers();
162
- vi.clearAllMocks();
163
- mockWtConnect.mockResolvedValue(undefined);
164
- mockWtTransportInstance = createMockTransport();
165
- });
166
-
167
- afterEach(() => {
168
- vi.useRealTimers();
169
- });
170
-
171
- describe("when WebTransport is unavailable (no URL)", () => {
172
- it("always uses gRPC-web", async () => {
173
- const opts: AdaptiveTransportOptions = {
174
- grpcWebUrl: "https://grpc.sid.example.com",
175
- // no webTransportUrl
176
- };
177
-
178
- const adaptive = new AdaptiveRpcTransport(opts);
179
- await adaptive.init();
180
-
181
- expect(adaptive.activeTransport).toBe("grpc-web");
182
-
183
- const fallback = getGrpcWebTransport(adaptive);
184
- const mockCall = resolvedUnaryCall(method, input, { value: "ok" });
185
- (fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(mockCall);
186
-
187
- adaptive.unary(method, input, options);
188
-
189
- expect(fallback.unary).toHaveBeenCalledWith(method, input, options);
190
- });
191
- });
192
-
193
- describe("when WebTransport is available", () => {
194
- let adaptive: AdaptiveRpcTransport;
195
-
196
- beforeEach(async () => {
197
- // Make WebTransport global exist so the constructor enables WT
198
- (globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
199
-
200
- const opts: AdaptiveTransportOptions = {
201
- grpcWebUrl: "https://grpc.sid.example.com",
202
- webTransportUrl: "https://wt.sid.example.com",
203
- reconnectDelay: 1000,
204
- };
205
-
206
- adaptive = new AdaptiveRpcTransport(opts);
207
- await adaptive.init();
208
- });
209
-
210
- afterEach(() => {
211
- adaptive.close();
212
- delete (globalThis as Record<string, unknown>).WebTransport;
213
- });
214
-
215
- it("uses WebTransport as primary transport", () => {
216
- expect(adaptive.activeTransport).toBe("webtransport");
217
- });
218
-
219
- it("sends unary calls through WebTransport primary", async () => {
220
- const mockCall = resolvedUnaryCall(method, input, { value: "wt-ok" });
221
- (
222
- mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
223
- ).mockReturnValue(mockCall);
224
-
225
- const call = adaptive.unary(method, input, options);
226
- const response = await call.response;
227
-
228
- expect(response).toEqual({ value: "wt-ok" });
229
- expect(mockWtTransportInstance.unary).toHaveBeenCalledWith(
230
- method,
231
- input,
232
- options,
233
- );
234
- });
235
-
236
- it("fails over to gRPC-web on transport error (UNAVAILABLE)", async () => {
237
- // Primary rejects with UNAVAILABLE (transport error)
238
- const transportErr = new RpcError("connection lost", "UNAVAILABLE");
239
- const failedCall = rejectedUnaryCall(method, input, transportErr);
240
- (
241
- mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
242
- ).mockReturnValue(failedCall);
243
-
244
- // Fallback succeeds
245
- const fallback = getGrpcWebTransport(adaptive);
246
- const successCall = resolvedUnaryCall(method, input, {
247
- value: "fallback-ok",
248
- });
249
- (fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(successCall);
250
-
251
- const call = adaptive.unary(method, input, options);
252
- const response = await call.response;
253
-
254
- expect(response).toEqual({ value: "fallback-ok" });
255
- expect(fallback.unary).toHaveBeenCalledWith(method, input, options);
256
- });
257
-
258
- it("does NOT failover on application error (NOT_FOUND)", async () => {
259
- const appErr = new RpcError("not found", "NOT_FOUND");
260
- const failedCall = rejectedUnaryCall(method, input, appErr);
261
- (
262
- mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
263
- ).mockReturnValue(failedCall);
264
-
265
- const fallback = getGrpcWebTransport(adaptive);
266
-
267
- const call = adaptive.unary(method, input, options);
268
-
269
- await expect(call.response).rejects.toThrow("not found");
270
- expect(fallback.unary).not.toHaveBeenCalled();
271
- });
272
-
273
- it("does NOT failover on PERMISSION_DENIED", async () => {
274
- const appErr = new RpcError("forbidden", "PERMISSION_DENIED");
275
- const failedCall = rejectedUnaryCall(method, input, appErr);
276
- (
277
- mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
278
- ).mockReturnValue(failedCall);
279
-
280
- const fallback = getGrpcWebTransport(adaptive);
281
-
282
- const call = adaptive.unary(method, input, options);
283
-
284
- await expect(call.response).rejects.toThrow("forbidden");
285
- expect(fallback.unary).not.toHaveBeenCalled();
286
- });
287
-
288
- it("schedules reconnect after failover", async () => {
289
- const transportErr = new RpcError("connection lost", "UNAVAILABLE");
290
- const failedCall = rejectedUnaryCall(method, input, transportErr);
291
- (
292
- mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
293
- ).mockReturnValue(failedCall);
294
-
295
- const fallback = getGrpcWebTransport(adaptive);
296
- const successCall = resolvedUnaryCall(method, input, { value: "ok" });
297
- (fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(successCall);
298
-
299
- const call = adaptive.unary(method, input, options);
300
- await call.response;
301
-
302
- // After failover, transport should be grpc-web
303
- expect(adaptive.activeTransport).toBe("grpc-web");
304
-
305
- // Reconnect is scheduled — advance timer
306
- mockWtConnect.mockResolvedValue(undefined);
307
- await vi.advanceTimersByTimeAsync(1000);
308
-
309
- // After successful reconnect, primary should be available again
310
- expect(adaptive.activeTransport).toBe("webtransport");
311
- });
312
-
313
- it("retries reconnect if reconnect attempt fails", async () => {
314
- const transportErr = new RpcError("connection lost", "UNAVAILABLE");
315
- const failedCall = rejectedUnaryCall(method, input, transportErr);
316
- (
317
- mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
318
- ).mockReturnValue(failedCall);
319
-
320
- const fallback = getGrpcWebTransport(adaptive);
321
- const successCall = resolvedUnaryCall(method, input, { value: "ok" });
322
- (fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(successCall);
323
-
324
- const call = adaptive.unary(method, input, options);
325
- await call.response;
326
-
327
- // First reconnect attempt fails
328
- mockWtConnect.mockRejectedValueOnce(new Error("still down"));
329
- await vi.advanceTimersByTimeAsync(1000);
330
-
331
- // Still on gRPC-web
332
- expect(adaptive.activeTransport).toBe("grpc-web");
333
-
334
- // Second reconnect succeeds
335
- mockWtConnect.mockResolvedValue(undefined);
336
- await vi.advanceTimersByTimeAsync(1000);
337
-
338
- expect(adaptive.activeTransport).toBe("webtransport");
339
- });
340
- });
341
-
342
- describe("activeTransport getter", () => {
343
- it("returns grpc-web when WebTransport URL not provided", async () => {
344
- const adaptive = new AdaptiveRpcTransport({
345
- grpcWebUrl: "https://grpc.sid.example.com",
346
- });
347
- await adaptive.init();
348
-
349
- expect(adaptive.activeTransport).toBe("grpc-web");
350
- });
351
-
352
- it("returns webtransport after successful init", async () => {
353
- (globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
354
-
355
- const adaptive = new AdaptiveRpcTransport({
356
- grpcWebUrl: "https://grpc.sid.example.com",
357
- webTransportUrl: "https://wt.sid.example.com",
358
- });
359
- await adaptive.init();
360
-
361
- expect(adaptive.activeTransport).toBe("webtransport");
362
-
363
- adaptive.close();
364
- delete (globalThis as Record<string, unknown>).WebTransport;
365
- });
366
-
367
- it("returns grpc-web after init failure", async () => {
368
- (globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
369
- mockWtConnect.mockRejectedValueOnce(new Error("connect failed"));
370
-
371
- const adaptive = new AdaptiveRpcTransport({
372
- grpcWebUrl: "https://grpc.sid.example.com",
373
- webTransportUrl: "https://wt.sid.example.com",
374
- });
375
- await adaptive.init();
376
-
377
- expect(adaptive.activeTransport).toBe("grpc-web");
378
-
379
- adaptive.close();
380
- delete (globalThis as Record<string, unknown>).WebTransport;
381
- });
382
-
383
- it("schedules reconnect after init() failure", async () => {
384
- (globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
385
- mockWtConnect.mockRejectedValueOnce(new Error("connect failed"));
386
-
387
- const adaptive = new AdaptiveRpcTransport({
388
- grpcWebUrl: "https://grpc.sid.example.com",
389
- webTransportUrl: "https://wt.sid.example.com",
390
- reconnectDelay: 2000,
391
- });
392
- await adaptive.init();
393
-
394
- expect(adaptive.activeTransport).toBe("grpc-web");
395
- mockWtConnect.mockResolvedValue(undefined);
396
-
397
- // Reconnect fires after delay
398
- await vi.advanceTimersByTimeAsync(2000);
399
- expect(mockWtConnect).toHaveBeenCalledTimes(2); // init fail + reconnect
400
- expect(adaptive.activeTransport).toBe("webtransport");
401
-
402
- adaptive.close();
403
- delete (globalThis as Record<string, unknown>).WebTransport;
404
- });
405
- });
406
-
407
- describe("onTransportChange callback", () => {
408
- it("fires when transport changes during failover", async () => {
409
- (globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
410
-
411
- const onChange = vi.fn();
412
- const adaptive = new AdaptiveRpcTransport({
413
- grpcWebUrl: "https://grpc.sid.example.com",
414
- webTransportUrl: "https://wt.sid.example.com",
415
- reconnectDelay: 1000,
416
- onTransportChange: onChange,
417
- });
418
- await adaptive.init();
419
-
420
- // init() calls onTransportChange with "webtransport"
421
- expect(onChange).toHaveBeenCalledWith("webtransport");
422
-
423
- // Trigger failover
424
- const transportErr = new RpcError("down", "UNAVAILABLE");
425
- const failedCall = rejectedUnaryCall(method, input, transportErr);
426
- (
427
- mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
428
- ).mockReturnValue(failedCall);
429
-
430
- const fallback = getGrpcWebTransport(adaptive);
431
- const successCall = resolvedUnaryCall(method, input, { value: "ok" });
432
- (fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(successCall);
433
-
434
- const call = adaptive.unary(method, input, options);
435
- await call.response;
436
-
437
- expect(onChange).toHaveBeenCalledWith("grpc-web");
438
-
439
- adaptive.close();
440
- delete (globalThis as Record<string, unknown>).WebTransport;
441
- });
442
- });
443
-
444
- describe("close()", () => {
445
- it("cancels pending reconnect timer", async () => {
446
- (globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
447
-
448
- const adaptive = new AdaptiveRpcTransport({
449
- grpcWebUrl: "https://grpc.sid.example.com",
450
- webTransportUrl: "https://wt.sid.example.com",
451
- reconnectDelay: 5000,
452
- });
453
- await adaptive.init();
454
-
455
- // Trigger failover to schedule reconnect
456
- const transportErr = new RpcError("down", "UNAVAILABLE");
457
- const failedCall = rejectedUnaryCall(method, input, transportErr);
458
- (
459
- mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
460
- ).mockReturnValue(failedCall);
461
-
462
- const fallback = getGrpcWebTransport(adaptive);
463
- const successCall = resolvedUnaryCall(method, input, { value: "ok" });
464
- (fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(successCall);
465
-
466
- const call = adaptive.unary(method, input, options);
467
- await call.response;
468
-
469
- // Close before reconnect fires
470
- adaptive.close();
471
-
472
- // Advance past reconnect delay — should NOT attempt reconnect
473
- mockWtConnect.mockClear();
474
- await vi.advanceTimersByTimeAsync(10000);
475
-
476
- // connect should not be called again (only the init() call)
477
- expect(mockWtConnect).not.toHaveBeenCalled();
478
-
479
- delete (globalThis as Record<string, unknown>).WebTransport;
480
- });
481
- });
482
-
483
- describe("clientStreaming / duplex", () => {
484
- it("clientStreaming throws RpcError", () => {
485
- const adaptive = new AdaptiveRpcTransport({
486
- grpcWebUrl: "https://grpc.sid.example.com",
487
- });
488
- expect(() => adaptive.clientStreaming(method, options)).toThrow(RpcError);
489
- });
490
-
491
- it("duplex throws RpcError", () => {
492
- const adaptive = new AdaptiveRpcTransport({
493
- grpcWebUrl: "https://grpc.sid.example.com",
494
- });
495
- expect(() => adaptive.duplex(method, options)).toThrow(RpcError);
496
- });
497
- });
498
-
499
- describe("failover triggers (all transport error codes)", () => {
500
- it("fails over on CANCELLED", async () => {
501
- (globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
502
- const adaptive = new AdaptiveRpcTransport({
503
- grpcWebUrl: "https://grpc.sid.example.com",
504
- webTransportUrl: "https://wt.sid.example.com",
505
- });
506
- await adaptive.init();
507
-
508
- const err = new RpcError("cancelled", "CANCELLED");
509
- (
510
- mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
511
- ).mockReturnValue(rejectedUnaryCall(method, input, err));
512
-
513
- const fallback = getGrpcWebTransport(adaptive);
514
- (fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(
515
- resolvedUnaryCall(method, input, { value: "ok" }),
516
- );
517
-
518
- await adaptive.unary(method, input, options).response;
519
- expect(adaptive.activeTransport).toBe("grpc-web");
520
-
521
- adaptive.close();
522
- delete (globalThis as Record<string, unknown>).WebTransport;
523
- });
524
-
525
- it("fails over on UNKNOWN", async () => {
526
- (globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
527
- const adaptive = new AdaptiveRpcTransport({
528
- grpcWebUrl: "https://grpc.sid.example.com",
529
- webTransportUrl: "https://wt.sid.example.com",
530
- });
531
- await adaptive.init();
532
-
533
- const err = new RpcError("unknown", "UNKNOWN");
534
- (
535
- mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
536
- ).mockReturnValue(rejectedUnaryCall(method, input, err));
537
-
538
- const fallback = getGrpcWebTransport(adaptive);
539
- (fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(
540
- resolvedUnaryCall(method, input, { value: "ok" }),
541
- );
542
-
543
- await adaptive.unary(method, input, options).response;
544
- expect(adaptive.activeTransport).toBe("grpc-web");
545
-
546
- adaptive.close();
547
- delete (globalThis as Record<string, unknown>).WebTransport;
548
- });
549
-
550
- it("fails over on non-RpcError (plain Error from stream failure)", async () => {
551
- (globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
552
- const adaptive = new AdaptiveRpcTransport({
553
- grpcWebUrl: "https://grpc.sid.example.com",
554
- webTransportUrl: "https://wt.sid.example.com",
555
- });
556
- await adaptive.init();
557
-
558
- // Simulate a plain Error (not RpcError) from the primary transport
559
- // This happens when WebTransport stream throws a non-gRPC error
560
- const defH = new Deferred<RpcMetadata>();
561
- const defM = new Deferred<MockMsg>();
562
- const defS = new Deferred<RpcStatus>();
563
- const defT = new Deferred<RpcMetadata>();
564
- const plainErr = new Error("stream aborted");
565
- defH.reject(plainErr);
566
- defM.reject(plainErr);
567
- defS.reject(plainErr);
568
- defT.reject(plainErr);
569
- const crashedCall = new UnaryCall(
570
- method,
571
- {},
572
- input,
573
- defH.promise,
574
- defM.promise,
575
- defS.promise,
576
- defT.promise,
577
- );
578
- (
579
- mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
580
- ).mockReturnValue(crashedCall);
581
-
582
- const fallback = getGrpcWebTransport(adaptive);
583
- (fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(
584
- resolvedUnaryCall(method, input, { value: "fallback-ok" }),
585
- );
586
-
587
- const response = await adaptive.unary(method, input, options).response;
588
- expect(response).toEqual({ value: "fallback-ok" });
589
- expect(adaptive.activeTransport).toBe("grpc-web");
590
-
591
- adaptive.close();
592
- delete (globalThis as Record<string, unknown>).WebTransport;
593
- });
594
-
595
- it("propagates error when fallback also fails", async () => {
596
- (globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
597
- const adaptive = new AdaptiveRpcTransport({
598
- grpcWebUrl: "https://grpc.sid.example.com",
599
- webTransportUrl: "https://wt.sid.example.com",
600
- });
601
- await adaptive.init();
602
-
603
- // Primary fails with transport error
604
- const transportErr = new RpcError("primary down", "UNAVAILABLE");
605
- (
606
- mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
607
- ).mockReturnValue(rejectedUnaryCall(method, input, transportErr));
608
-
609
- // Fallback also fails
610
- const fallbackErr = new RpcError("fallback down", "UNAVAILABLE");
611
- const fallback = getGrpcWebTransport(adaptive);
612
- (fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(
613
- rejectedUnaryCall(method, input, fallbackErr),
614
- );
615
-
616
- const err = await adaptive
617
- .unary(method, input, options)
618
- .response.catch((e: unknown) => e);
619
- expect((err as RpcError).message).toBe("fallback down");
620
-
621
- adaptive.close();
622
- delete (globalThis as Record<string, unknown>).WebTransport;
623
- });
624
- });
625
-
626
- describe("scheduleReconnect idempotency", () => {
627
- it("does not schedule multiple reconnect timers on concurrent failovers", async () => {
628
- (globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
629
- const adaptive = new AdaptiveRpcTransport({
630
- grpcWebUrl: "https://grpc.sid.example.com",
631
- webTransportUrl: "https://wt.sid.example.com",
632
- reconnectDelay: 5000,
633
- });
634
- await adaptive.init();
635
-
636
- const transportErr = new RpcError("down", "UNAVAILABLE");
637
- const fallback = getGrpcWebTransport(adaptive);
638
-
639
- // Two concurrent unary calls both fail with UNAVAILABLE
640
- (
641
- mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
642
- ).mockReturnValue(rejectedUnaryCall(method, input, transportErr));
643
- (fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(
644
- resolvedUnaryCall(method, input, { value: "ok" }),
645
- );
646
-
647
- await Promise.all([
648
- adaptive.unary(method, input, options).response,
649
- adaptive.unary(method, input, options).response,
650
- ]);
651
-
652
- // Advance past the reconnect delay — connect should fire exactly once
653
- mockWtConnect.mockResolvedValue(undefined);
654
- await vi.advanceTimersByTimeAsync(5000);
655
-
656
- // Only one reconnect attempt, not two (init=1, reconnect=1 — second failover is no-op)
657
- expect(mockWtConnect).toHaveBeenCalledTimes(2);
658
-
659
- adaptive.close();
660
- delete (globalThis as Record<string, unknown>).WebTransport;
661
- });
662
- });
663
-
664
- describe("onTransportChange on reconnect", () => {
665
- it("fires 'webtransport' when reconnect restores primary", async () => {
666
- (globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
667
-
668
- const onChange = vi.fn();
669
- const adaptive = new AdaptiveRpcTransport({
670
- grpcWebUrl: "https://grpc.sid.example.com",
671
- webTransportUrl: "https://wt.sid.example.com",
672
- reconnectDelay: 1000,
673
- onTransportChange: onChange,
674
- });
675
- await adaptive.init();
676
-
677
- // Trigger failover
678
- const transportErr = new RpcError("down", "UNAVAILABLE");
679
- (
680
- mockWtTransportInstance.unary as ReturnType<typeof vi.fn>
681
- ).mockReturnValue(rejectedUnaryCall(method, input, transportErr));
682
- const fallback = getGrpcWebTransport(adaptive);
683
- (fallback.unary as ReturnType<typeof vi.fn>).mockReturnValue(
684
- resolvedUnaryCall(method, input, { value: "ok" }),
685
- );
686
-
687
- await adaptive.unary(method, input, options).response;
688
- expect(onChange).toHaveBeenLastCalledWith("grpc-web");
689
-
690
- // Reconnect
691
- mockWtConnect.mockResolvedValue(undefined);
692
- await vi.advanceTimersByTimeAsync(1000);
693
- expect(onChange).toHaveBeenLastCalledWith("webtransport");
694
-
695
- adaptive.close();
696
- delete (globalThis as Record<string, unknown>).WebTransport;
697
- });
698
- });
699
-
700
- describe("serverStreaming", () => {
701
- it("always routes to gRPC-web fallback (WebTransport does not support streaming)", async () => {
702
- (globalThis as Record<string, unknown>).WebTransport = MockWebTransport;
703
- const adaptive = new AdaptiveRpcTransport({
704
- grpcWebUrl: "https://grpc.sid.example.com",
705
- webTransportUrl: "https://wt.sid.example.com",
706
- });
707
- await adaptive.init();
708
-
709
- const fallback = getGrpcWebTransport(adaptive);
710
- const mockStream = {} as ReturnType<RpcTransport["serverStreaming"]>;
711
- (fallback.serverStreaming as ReturnType<typeof vi.fn>).mockReturnValue(
712
- mockStream,
713
- );
714
-
715
- const result = adaptive.serverStreaming(method, input, options);
716
-
717
- expect(fallback.serverStreaming).toHaveBeenCalledWith(
718
- method,
719
- input,
720
- options,
721
- );
722
- expect(result).toBe(mockStream);
723
-
724
- adaptive.close();
725
- delete (globalThis as Record<string, unknown>).WebTransport;
726
- });
727
- });
728
- });