@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 { RpcError } from "@protobuf-ts/runtime-rpc";
3
- import type { MethodInfo, RpcOptions } from "@protobuf-ts/runtime-rpc";
4
- import {
5
- RequestHeader,
6
- ResponseHeader,
7
- } from "../generated/sid/v1/common/transport";
8
- import {
9
- WebTransportConnection,
10
- WebTransportRpcTransport,
11
- } from "./webtransport";
12
-
13
- // ---------------------------------------------------------------------------
14
- // Mock WebTransport browser API
15
- // ---------------------------------------------------------------------------
16
-
17
- interface MockBidiStream {
18
- readable: ReadableStream<Uint8Array>;
19
- writable: WritableStream<Uint8Array>;
20
- }
21
-
22
- /** Build a mock bidirectional stream where server sends `responseChunks`. */
23
- function makeMockBidiStream(responseChunks: Uint8Array[]): {
24
- stream: MockBidiStream;
25
- writtenChunks: Uint8Array[];
26
- } {
27
- const writtenChunks: Uint8Array[] = [];
28
-
29
- // Writable side: collects written bytes
30
- const writable = new WritableStream<Uint8Array>({
31
- write(chunk) {
32
- writtenChunks.push(chunk);
33
- },
34
- });
35
-
36
- // Readable side: yields responseChunks one by one
37
- let i = 0;
38
- const readable = new ReadableStream<Uint8Array>({
39
- pull(controller) {
40
- if (i < responseChunks.length) {
41
- controller.enqueue(responseChunks[i++]);
42
- } else {
43
- controller.close();
44
- }
45
- },
46
- });
47
-
48
- return { stream: { readable, writable }, writtenChunks };
49
- }
50
-
51
- /** Build a ResponseHeader frame (length-prefix + proto bytes). */
52
- function buildResponseFrame(header: ResponseHeader): Uint8Array {
53
- const payload = ResponseHeader.toBinary(header);
54
- const lenBuf = new Uint8Array(4);
55
- new DataView(lenBuf.buffer).setUint32(0, payload.length, false);
56
- const frame = new Uint8Array(4 + payload.length);
57
- frame.set(lenBuf);
58
- frame.set(payload, 4);
59
- return frame;
60
- }
61
-
62
- /** Build a body frame (length-prefix + raw bytes). */
63
- function buildBodyFrame(body: Uint8Array): Uint8Array {
64
- const lenBuf = new Uint8Array(4);
65
- new DataView(lenBuf.buffer).setUint32(0, body.length, false);
66
- const frame = new Uint8Array(4 + body.length);
67
- frame.set(lenBuf);
68
- frame.set(body, 4);
69
- return frame;
70
- }
71
-
72
- /** Parse a length-prefix frame from a flat byte array (for assertion helpers). */
73
- function parseFrame(
74
- bytes: Uint8Array,
75
- offset: number,
76
- ): { payload: Uint8Array; next: number } {
77
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
78
- const len = view.getUint32(offset, false);
79
- const payload = bytes.slice(offset + 4, offset + 4 + len);
80
- return { payload, next: offset + 4 + len };
81
- }
82
-
83
- /** Concatenate all chunks into a single Uint8Array. */
84
- function concat(chunks: Uint8Array[]): Uint8Array {
85
- const total = chunks.reduce((n, c) => n + c.length, 0);
86
- const out = new Uint8Array(total);
87
- let offset = 0;
88
- for (const c of chunks) {
89
- out.set(c, offset);
90
- offset += c.length;
91
- }
92
- return out;
93
- }
94
-
95
- // ---------------------------------------------------------------------------
96
- // Test fixture: minimal MethodInfo
97
- // ---------------------------------------------------------------------------
98
-
99
- interface Msg {
100
- value: string;
101
- }
102
-
103
- function makeMethod(): MethodInfo<Msg, Msg> {
104
- return {
105
- service: { typeName: "sid.v1.TestService", methods: [], options: {} },
106
- name: "TestRpc",
107
- localName: "testRpc",
108
- idempotency: undefined,
109
- serverStreaming: false,
110
- clientStreaming: false,
111
- I: {
112
- typeName: "TestInput",
113
- toBinary: (msg: Msg) => new TextEncoder().encode(JSON.stringify(msg)),
114
- fromBinary: (bytes: Uint8Array) =>
115
- JSON.parse(new TextDecoder().decode(bytes)) as Msg,
116
- },
117
- O: {
118
- typeName: "TestOutput",
119
- toBinary: (msg: Msg) => new TextEncoder().encode(JSON.stringify(msg)),
120
- fromBinary: (bytes: Uint8Array) =>
121
- JSON.parse(new TextDecoder().decode(bytes)) as Msg,
122
- },
123
- options: {},
124
- } as unknown as MethodInfo<Msg, Msg>;
125
- }
126
-
127
- // ---------------------------------------------------------------------------
128
- // WebTransportConnection tests
129
- // ---------------------------------------------------------------------------
130
-
131
- describe("WebTransportConnection", () => {
132
- let mockWt: {
133
- ready: Promise<void>;
134
- closed: Promise<void>;
135
- close: ReturnType<typeof vi.fn>;
136
- createBidirectionalStream: ReturnType<typeof vi.fn>;
137
- };
138
-
139
- beforeEach(() => {
140
- mockWt = {
141
- ready: Promise.resolve(),
142
- closed: new Promise(() => {}), // never resolves during test
143
- close: vi.fn(),
144
- createBidirectionalStream: vi.fn(),
145
- };
146
- (globalThis as Record<string, unknown>).WebTransport = vi
147
- .fn()
148
- .mockImplementation(() => mockWt);
149
- });
150
-
151
- afterEach(() => {
152
- delete (globalThis as Record<string, unknown>).WebTransport;
153
- });
154
-
155
- it("connects successfully and marks connected", async () => {
156
- const conn = new WebTransportConnection({
157
- url: "https://wt.sid.example.com",
158
- });
159
- expect(conn.connected).toBe(false);
160
-
161
- await conn.connect();
162
- expect(conn.connected).toBe(true);
163
- });
164
-
165
- it("connect() is idempotent — does not open second connection", async () => {
166
- const conn = new WebTransportConnection({
167
- url: "https://wt.sid.example.com",
168
- });
169
- await conn.connect();
170
- await conn.connect();
171
- await conn.connect();
172
-
173
- expect(
174
- (globalThis as Record<string, unknown>).WebTransport,
175
- ).toHaveBeenCalledTimes(1);
176
- });
177
-
178
- it("close() disconnects and allows reconnect", async () => {
179
- const conn = new WebTransportConnection({
180
- url: "https://wt.sid.example.com",
181
- });
182
- await conn.connect();
183
-
184
- conn.close();
185
- expect(conn.connected).toBe(false);
186
- expect(mockWt.close).toHaveBeenCalled();
187
-
188
- // Can reconnect after close
189
- await conn.connect();
190
- expect(conn.connected).toBe(true);
191
- expect(
192
- (globalThis as Record<string, unknown>).WebTransport,
193
- ).toHaveBeenCalledTimes(2);
194
- });
195
-
196
- it("detects remote disconnection via wt.closed", async () => {
197
- let resolveClosedPromise!: () => void;
198
- mockWt.closed = new Promise<void>((resolve) => {
199
- resolveClosedPromise = resolve;
200
- });
201
-
202
- const conn = new WebTransportConnection({
203
- url: "https://wt.sid.example.com",
204
- });
205
- await conn.connect();
206
- expect(conn.connected).toBe(true);
207
-
208
- // Simulate server closing the connection
209
- resolveClosedPromise();
210
- await Promise.resolve(); // let microtasks run
211
-
212
- expect(conn.connected).toBe(false);
213
- });
214
-
215
- it("passes cert hash as serverCertificateHashes when provided", async () => {
216
- const certHash = "deadbeef1234567890abcdef";
217
- const conn = new WebTransportConnection({
218
- url: "https://wt.sid.example.com",
219
- certHash,
220
- });
221
- await conn.connect();
222
-
223
- const WebTransportCtor = (globalThis as Record<string, unknown>)
224
- .WebTransport as ReturnType<typeof vi.fn>;
225
- const initOpts = WebTransportCtor.mock.calls[0][1] as {
226
- serverCertificateHashes?: unknown[];
227
- };
228
- expect(initOpts.serverCertificateHashes).toBeDefined();
229
- expect(
230
- (initOpts.serverCertificateHashes as Array<{ algorithm: string }>)[0]
231
- .algorithm,
232
- ).toBe("sha-256");
233
- });
234
-
235
- it("does not pass serverCertificateHashes when no cert hash", async () => {
236
- const conn = new WebTransportConnection({
237
- url: "https://wt.sid.example.com",
238
- });
239
- await conn.connect();
240
-
241
- const WebTransportCtor = (globalThis as Record<string, unknown>)
242
- .WebTransport as ReturnType<typeof vi.fn>;
243
- const initOpts = WebTransportCtor.mock.calls[0][1] as Record<
244
- string,
245
- unknown
246
- >;
247
- expect(initOpts.serverCertificateHashes).toBeUndefined();
248
- });
249
-
250
- it("connect() throws when wt.ready rejects", async () => {
251
- mockWt.ready = Promise.reject(new Error("TLS handshake failed"));
252
-
253
- const conn = new WebTransportConnection({
254
- url: "https://wt.sid.example.com",
255
- });
256
- await expect(conn.connect()).rejects.toThrow("TLS handshake failed");
257
- expect(conn.connected).toBe(false);
258
- });
259
-
260
- it("concurrent connect() calls share the same promise (no duplicate connections)", async () => {
261
- // Delay wt.ready so the connection is in-progress when second connect() is called
262
- let resolveReady!: () => void;
263
- mockWt.ready = new Promise<void>((resolve) => {
264
- resolveReady = resolve;
265
- });
266
-
267
- const conn = new WebTransportConnection({
268
- url: "https://wt.sid.example.com",
269
- });
270
-
271
- // Start two connects concurrently — neither has resolved yet
272
- const p1 = conn.connect();
273
- const p2 = conn.connect();
274
-
275
- // Both should be the same promise (or at least both resolve to same transport)
276
- resolveReady();
277
- const [wt1, wt2] = await Promise.all([p1, p2]);
278
-
279
- expect(wt1).toBe(wt2);
280
- expect(
281
- (globalThis as Record<string, unknown>).WebTransport,
282
- ).toHaveBeenCalledTimes(1);
283
- });
284
-
285
- it("close() is safe when not connected (no-op)", () => {
286
- const conn = new WebTransportConnection({
287
- url: "https://wt.sid.example.com",
288
- });
289
- // Never connected — close() should not throw
290
- expect(() => conn.close()).not.toThrow();
291
- expect(conn.connected).toBe(false);
292
- });
293
-
294
- it("wt.closed after close() does not set connected=false again (already null)", async () => {
295
- let resolveClosedPromise!: () => void;
296
- mockWt.closed = new Promise<void>((resolve) => {
297
- resolveClosedPromise = resolve;
298
- });
299
-
300
- const conn = new WebTransportConnection({
301
- url: "https://wt.sid.example.com",
302
- });
303
- await conn.connect();
304
-
305
- // Manually close before server closes the connection
306
- conn.close();
307
- expect(conn.connected).toBe(false);
308
-
309
- // Now the server closes — wt.closed resolves, but this.transport !== wt (it's null)
310
- // The handler should be a no-op (not throw, not change state)
311
- resolveClosedPromise();
312
- await Promise.resolve();
313
-
314
- // Still disconnected, no error
315
- expect(conn.connected).toBe(false);
316
- });
317
-
318
- it("connect() can be retried after failure (connecting state is reset)", async () => {
319
- // First attempt fails
320
- mockWt.ready = Promise.reject(new Error("connect failed"));
321
- const conn = new WebTransportConnection({
322
- url: "https://wt.sid.example.com",
323
- });
324
- await expect(conn.connect()).rejects.toThrow("connect failed");
325
-
326
- // Second attempt succeeds (new mockWt with resolved ready)
327
- const mockWt2 = {
328
- ready: Promise.resolve(),
329
- closed: new Promise<void>(() => {}),
330
- close: vi.fn(),
331
- createBidirectionalStream: vi.fn(),
332
- };
333
- (globalThis as Record<string, unknown>).WebTransport = vi
334
- .fn()
335
- .mockImplementation(() => mockWt2);
336
-
337
- await conn.connect();
338
- expect(conn.connected).toBe(true);
339
- });
340
- });
341
-
342
- // ---------------------------------------------------------------------------
343
- // WebTransportRpcTransport tests
344
- // ---------------------------------------------------------------------------
345
-
346
- describe("WebTransportRpcTransport", () => {
347
- const method = makeMethod();
348
- const input: Msg = { value: "hello" };
349
- const options: RpcOptions = { meta: {} };
350
-
351
- let mockWt: {
352
- ready: Promise<void>;
353
- closed: Promise<void>;
354
- close: ReturnType<typeof vi.fn>;
355
- createBidirectionalStream: ReturnType<typeof vi.fn>;
356
- };
357
- let conn: WebTransportConnection;
358
-
359
- beforeEach(() => {
360
- mockWt = {
361
- ready: Promise.resolve(),
362
- closed: new Promise(() => {}),
363
- close: vi.fn(),
364
- createBidirectionalStream: vi.fn(),
365
- };
366
- (globalThis as Record<string, unknown>).WebTransport = vi
367
- .fn()
368
- .mockImplementation(() => mockWt);
369
- conn = new WebTransportConnection({ url: "https://wt.sid.example.com" });
370
- });
371
-
372
- afterEach(() => {
373
- conn.close();
374
- delete (globalThis as Record<string, unknown>).WebTransport;
375
- });
376
-
377
- it("sends RequestHeader with correct method path and gets response", async () => {
378
- await conn.connect();
379
-
380
- const responseBody = new TextEncoder().encode(
381
- JSON.stringify({ value: "world" }),
382
- );
383
-
384
- const respHeader: ResponseHeader = {
385
- statusCode: 0,
386
- statusMessage: "",
387
- metadata: {},
388
- };
389
- const { stream, writtenChunks } = makeMockBidiStream([
390
- buildResponseFrame(respHeader),
391
- buildBodyFrame(responseBody),
392
- ]);
393
- mockWt.createBidirectionalStream.mockResolvedValue(stream);
394
-
395
- const transport = new WebTransportRpcTransport(conn);
396
- const call = transport.unary(method, input, options);
397
- const response = await call.response;
398
-
399
- expect(response).toEqual({ value: "world" });
400
-
401
- // Verify RequestHeader was sent correctly
402
- const sent = concat(writtenChunks);
403
- const { payload: reqHeaderBytes } = parseFrame(sent, 0);
404
- const sentHeader = RequestHeader.fromBinary(reqHeaderBytes);
405
- expect(sentHeader.method).toBe("sid.v1.TestService/TestRpc");
406
- });
407
-
408
- it("sends metadata from RpcOptions.meta in RequestHeader", async () => {
409
- await conn.connect();
410
-
411
- const responseBody = new TextEncoder().encode(
412
- JSON.stringify({ value: "ok" }),
413
- );
414
- const respHeader: ResponseHeader = {
415
- statusCode: 0,
416
- statusMessage: "",
417
- metadata: {},
418
- };
419
- const { stream, writtenChunks } = makeMockBidiStream([
420
- buildResponseFrame(respHeader),
421
- buildBodyFrame(responseBody),
422
- ]);
423
- mockWt.createBidirectionalStream.mockResolvedValue(stream);
424
-
425
- const transport = new WebTransportRpcTransport(conn);
426
- const callWithMeta = transport.unary(method, input, {
427
- meta: { authorization: "Bearer token123", "x-trace-id": "abc" },
428
- });
429
- await callWithMeta.response;
430
-
431
- const sent = concat(writtenChunks);
432
- const { payload: reqHeaderBytes } = parseFrame(sent, 0);
433
- const sentHeader = RequestHeader.fromBinary(reqHeaderBytes);
434
- expect(sentHeader.metadata["authorization"]).toBe("Bearer token123");
435
- expect(sentHeader.metadata["x-trace-id"]).toBe("abc");
436
- });
437
-
438
- it("resolves headers from ResponseHeader.metadata", async () => {
439
- await conn.connect();
440
-
441
- const responseBody = new TextEncoder().encode(
442
- JSON.stringify({ value: "ok" }),
443
- );
444
- const respHeader: ResponseHeader = {
445
- statusCode: 0,
446
- statusMessage: "",
447
- metadata: { "x-request-id": "req-456" },
448
- };
449
- const { stream } = makeMockBidiStream([
450
- buildResponseFrame(respHeader),
451
- buildBodyFrame(responseBody),
452
- ]);
453
- mockWt.createBidirectionalStream.mockResolvedValue(stream);
454
-
455
- const transport = new WebTransportRpcTransport(conn);
456
- const call = transport.unary(method, input, options);
457
- const headers = await call.headers;
458
-
459
- expect(headers["x-request-id"]).toBe("req-456");
460
- });
461
-
462
- it("joins array metadata values with comma in RequestHeader", async () => {
463
- await conn.connect();
464
-
465
- const responseBody = new TextEncoder().encode(
466
- JSON.stringify({ value: "ok" }),
467
- );
468
- const respHeader: ResponseHeader = {
469
- statusCode: 0,
470
- statusMessage: "",
471
- metadata: {},
472
- };
473
- const { stream, writtenChunks } = makeMockBidiStream([
474
- buildResponseFrame(respHeader),
475
- buildBodyFrame(responseBody),
476
- ]);
477
- mockWt.createBidirectionalStream.mockResolvedValue(stream);
478
-
479
- const transport = new WebTransportRpcTransport(conn);
480
- const call = transport.unary(method, input, {
481
- meta: { accept: ["application/json", "text/plain"] },
482
- });
483
- await call.response;
484
-
485
- const sent = concat(writtenChunks);
486
- const { payload: reqHeaderBytes } = parseFrame(sent, 0);
487
- const sentHeader = RequestHeader.fromBinary(reqHeaderBytes);
488
- expect(sentHeader.metadata["accept"]).toBe("application/json,text/plain");
489
- });
490
-
491
- it("includes Date timeout in RequestHeader.timeoutMs (future date)", async () => {
492
- await conn.connect();
493
-
494
- const responseBody = new TextEncoder().encode(
495
- JSON.stringify({ value: "ok" }),
496
- );
497
- const respHeader: ResponseHeader = {
498
- statusCode: 0,
499
- statusMessage: "",
500
- metadata: {},
501
- };
502
- const { stream, writtenChunks } = makeMockBidiStream([
503
- buildResponseFrame(respHeader),
504
- buildBodyFrame(responseBody),
505
- ]);
506
- mockWt.createBidirectionalStream.mockResolvedValue(stream);
507
-
508
- const transport = new WebTransportRpcTransport(conn);
509
- const deadlineMs = 5000;
510
- const deadline = new Date(Date.now() + deadlineMs);
511
- const call = transport.unary(method, input, {
512
- meta: {},
513
- timeout: deadline,
514
- });
515
- await call.response;
516
-
517
- const sent = concat(writtenChunks);
518
- const { payload: reqHeaderBytes } = parseFrame(sent, 0);
519
- const sentHeader = RequestHeader.fromBinary(reqHeaderBytes);
520
- // timeoutMs should be roughly 5000 (some drift allowed)
521
- expect(sentHeader.timeoutMs).toBeGreaterThan(0);
522
- expect(sentHeader.timeoutMs).toBeLessThanOrEqual(deadlineMs);
523
- });
524
-
525
- it("clamps past Date timeout to 0 in RequestHeader.timeoutMs", async () => {
526
- await conn.connect();
527
-
528
- const responseBody = new TextEncoder().encode(
529
- JSON.stringify({ value: "ok" }),
530
- );
531
- const respHeader: ResponseHeader = {
532
- statusCode: 0,
533
- statusMessage: "",
534
- metadata: {},
535
- };
536
- const { stream, writtenChunks } = makeMockBidiStream([
537
- buildResponseFrame(respHeader),
538
- buildBodyFrame(responseBody),
539
- ]);
540
- mockWt.createBidirectionalStream.mockResolvedValue(stream);
541
-
542
- const transport = new WebTransportRpcTransport(conn);
543
- const pastDate = new Date(Date.now() - 10000); // 10 seconds ago
544
- const call = transport.unary(method, input, {
545
- meta: {},
546
- timeout: pastDate,
547
- });
548
- await call.response;
549
-
550
- const sent = concat(writtenChunks);
551
- const { payload: reqHeaderBytes } = parseFrame(sent, 0);
552
- const sentHeader = RequestHeader.fromBinary(reqHeaderBytes);
553
- expect(sentHeader.timeoutMs).toBe(0);
554
- });
555
-
556
- it("includes numeric timeout in RequestHeader.timeoutMs", async () => {
557
- await conn.connect();
558
-
559
- const responseBody = new TextEncoder().encode(
560
- JSON.stringify({ value: "ok" }),
561
- );
562
- const respHeader: ResponseHeader = {
563
- statusCode: 0,
564
- statusMessage: "",
565
- metadata: {},
566
- };
567
- const { stream, writtenChunks } = makeMockBidiStream([
568
- buildResponseFrame(respHeader),
569
- buildBodyFrame(responseBody),
570
- ]);
571
- mockWt.createBidirectionalStream.mockResolvedValue(stream);
572
-
573
- const transport = new WebTransportRpcTransport(conn);
574
- const call = transport.unary(method, input, { meta: {}, timeout: 3000 });
575
- await call.response;
576
-
577
- const sent = concat(writtenChunks);
578
- const { payload: reqHeaderBytes } = parseFrame(sent, 0);
579
- const sentHeader = RequestHeader.fromBinary(reqHeaderBytes);
580
- expect(sentHeader.timeoutMs).toBe(3000);
581
- });
582
-
583
- it("uses 'RPC error' as message when statusMessage is empty", async () => {
584
- await conn.connect();
585
-
586
- const respHeader: ResponseHeader = {
587
- statusCode: 5, // NOT_FOUND
588
- statusMessage: "", // empty — code should fall back to "RPC error"
589
- metadata: {},
590
- };
591
- const { stream } = makeMockBidiStream([buildResponseFrame(respHeader)]);
592
- mockWt.createBidirectionalStream.mockResolvedValue(stream);
593
-
594
- const transport = new WebTransportRpcTransport(conn);
595
- const call = transport.unary(method, input, options);
596
-
597
- const err = await call.response.catch((e: unknown) => e);
598
- expect(err).toBeInstanceOf(RpcError);
599
- expect((err as RpcError).code).toBe("NOT_FOUND");
600
- expect((err as RpcError).message).toBe("RPC error");
601
- });
602
-
603
- it("throws RpcError on non-zero gRPC status code", async () => {
604
- await conn.connect();
605
-
606
- const respHeader: ResponseHeader = {
607
- statusCode: 5, // NOT_FOUND
608
- statusMessage: "record not found",
609
- metadata: {},
610
- };
611
- const { stream } = makeMockBidiStream([buildResponseFrame(respHeader)]);
612
- mockWt.createBidirectionalStream.mockResolvedValue(stream);
613
-
614
- const transport = new WebTransportRpcTransport(conn);
615
- const call = transport.unary(method, input, options);
616
-
617
- const err = await call.response.catch((e: unknown) => e);
618
- expect(err).toBeInstanceOf(RpcError);
619
- expect((err as RpcError).code).toBe("NOT_FOUND");
620
- expect((err as RpcError).message).toBe("record not found");
621
- });
622
-
623
- it("maps all gRPC status codes correctly", async () => {
624
- const cases: Array<{ code: number; name: string }> = [
625
- { code: 1, name: "CANCELLED" },
626
- { code: 2, name: "UNKNOWN" },
627
- { code: 3, name: "INVALID_ARGUMENT" },
628
- { code: 4, name: "DEADLINE_EXCEEDED" },
629
- { code: 7, name: "PERMISSION_DENIED" },
630
- { code: 14, name: "UNAVAILABLE" },
631
- { code: 16, name: "UNAUTHENTICATED" },
632
- ];
633
-
634
- for (const { code, name } of cases) {
635
- await conn.connect();
636
- const respHeader: ResponseHeader = {
637
- statusCode: code,
638
- statusMessage: name,
639
- metadata: {},
640
- };
641
- const { stream } = makeMockBidiStream([buildResponseFrame(respHeader)]);
642
- mockWt.createBidirectionalStream.mockResolvedValue(stream);
643
-
644
- const transport = new WebTransportRpcTransport(conn);
645
- const call = transport.unary(method, input, options);
646
- const err = await call.response.catch((e: unknown) => e);
647
-
648
- expect((err as RpcError).code).toBe(name);
649
- conn.close();
650
- conn = new WebTransportConnection({ url: "https://wt.sid.example.com" });
651
- }
652
- });
653
-
654
- it("falls back to UNKNOWN for unrecognized gRPC status code", async () => {
655
- await conn.connect();
656
-
657
- const respHeader: ResponseHeader = {
658
- statusCode: 999, // not in the grpcCodeName table
659
- statusMessage: "mystery error",
660
- metadata: {},
661
- };
662
- const { stream } = makeMockBidiStream([buildResponseFrame(respHeader)]);
663
- mockWt.createBidirectionalStream.mockResolvedValue(stream);
664
-
665
- const transport = new WebTransportRpcTransport(conn);
666
- const call = transport.unary(method, input, options);
667
-
668
- const err = await call.response.catch((e: unknown) => e);
669
- expect(err).toBeInstanceOf(RpcError);
670
- expect((err as RpcError).code).toBe("UNKNOWN");
671
- });
672
-
673
- it("throws UNAVAILABLE if connection not yet established", async () => {
674
- // conn not connected
675
- const transport = new WebTransportRpcTransport(conn);
676
- const call = transport.unary(method, input, options);
677
-
678
- const err = await call.response.catch((e: unknown) => e);
679
- expect(err).toBeInstanceOf(RpcError);
680
- expect((err as RpcError).code).toBe("UNAVAILABLE");
681
- });
682
-
683
- it("throws INTERNAL on stream error (non-RpcError)", async () => {
684
- await conn.connect();
685
- mockWt.createBidirectionalStream.mockRejectedValue(
686
- new Error("stream creation failed"),
687
- );
688
-
689
- const transport = new WebTransportRpcTransport(conn);
690
- const call = transport.unary(method, input, options);
691
-
692
- const err = await call.response.catch((e: unknown) => e);
693
- expect(err).toBeInstanceOf(RpcError);
694
- expect((err as RpcError).code).toBe("INTERNAL");
695
- expect((err as RpcError).message).toBe("stream creation failed");
696
- });
697
-
698
- it("uses String(err) as message when thrown value is not an Error", async () => {
699
- await conn.connect();
700
- // Simulate a non-Error non-RpcError thrown value (rare but possible)
701
- mockWt.createBidirectionalStream.mockRejectedValue("stream aborted");
702
-
703
- const transport = new WebTransportRpcTransport(conn);
704
- const call = transport.unary(method, input, options);
705
-
706
- const err = await call.response.catch((e: unknown) => e);
707
- expect(err).toBeInstanceOf(RpcError);
708
- expect((err as RpcError).code).toBe("INTERNAL");
709
- expect((err as RpcError).message).toBe("stream aborted");
710
- });
711
-
712
- it("serverStreaming throws RpcError", () => {
713
- const transport = new WebTransportRpcTransport(conn);
714
- expect(() => transport.serverStreaming(method, input, options)).toThrow(
715
- RpcError,
716
- );
717
- });
718
-
719
- it("clientStreaming throws RpcError", () => {
720
- const transport = new WebTransportRpcTransport(conn);
721
- expect(() => transport.clientStreaming(method, options)).toThrow(RpcError);
722
- });
723
-
724
- it("duplex throws RpcError", () => {
725
- const transport = new WebTransportRpcTransport(conn);
726
- expect(() => transport.duplex(method, options)).toThrow(RpcError);
727
- });
728
- });