Skip to content

Commit 00a46d5

Browse files
Merge main into release
2 parents 1623fb0 + 34c63bf commit 00a46d5

4 files changed

Lines changed: 98 additions & 38 deletions

File tree

.changeset/empty-tools-greet.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'firebase': minor
3+
'@firebase/data-connect': minor
4+
---
5+
6+
Fix header names for auth and app check tokens over streaming

packages/data-connect/src/network/stream/streamTransport.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ export abstract class AbstractDataConnectStreamTransport extends AbstractDataCon
171171
private closeTimeoutFinished = false;
172172
/** current auth uid. used to detect if a different user logs in */
173173
private authUid: string | null | undefined;
174+
/** Flag to ensure we wait for the initial auth state once per connection attempt. */
175+
private hasWaitedForInitialAuth = false;
174176

175177
/**
176178
* Tracks a query execution request, storing the request body and creating and storing a promise that
@@ -329,6 +331,7 @@ export abstract class AbstractDataConnectStreamTransport extends AbstractDataCon
329331
protected onConnectionReady(): void {
330332
this.isFirstStreamMessage = true;
331333
this.lastSentAuthToken = null;
334+
this.hasWaitedForInitialAuth = false;
332335
}
333336

334337
/**
@@ -426,12 +429,12 @@ export abstract class AbstractDataConnectStreamTransport extends AbstractDataCon
426429
this._callerSdkType
427430
);
428431
if (this.shouldIncludeAuth && this._authToken) {
429-
headers.authToken = this._authToken;
432+
headers['X-Firebase-Auth-Token'] = this._authToken;
430433
this.lastSentAuthToken = this._authToken;
431434
}
432435
if (this.isFirstStreamMessage) {
433436
if (this._appCheckToken) {
434-
headers.appCheckToken = this._appCheckToken;
437+
headers['X-Firebase-App-Check'] = this._appCheckToken;
435438
}
436439
preparedRequestBody.name = this._connectorResourcePath;
437440
}
@@ -440,14 +443,19 @@ export abstract class AbstractDataConnectStreamTransport extends AbstractDataCon
440443
return preparedRequestBody;
441444
}
442445

446+
// TODO(stephenarosaj): just make this async
443447
/**
444448
* Sends a request message to the server via the concrete implementation.
445449
* Ensures the connection is ready and prepares the message before sending.
446450
* @returns A promise that resolves when the request message has been sent.
447451
*/
448-
private sendRequestMessage<Variables>(
452+
private async sendRequestMessage<Variables>(
449453
requestBody: DataConnectStreamRequest<Variables>
450454
): Promise<void> {
455+
if (!this.hasWaitedForInitialAuth && this.authProvider) {
456+
await this.getWithAuth();
457+
this.hasWaitedForInitialAuth = true;
458+
}
451459
if (this.streamIsReady) {
452460
const prepared = this.prepareMessage(requestBody);
453461
return this.sendMessage(prepared);

packages/data-connect/src/network/stream/wire.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ export interface StreamRequest {
5050
* @internal
5151
*/
5252
export interface StreamRequestHeaders {
53-
/** used to initially authenticate or re-authenticate */
54-
authToken?: string;
55-
/** used to initially authenticate or re-authenticate */
56-
appCheckToken?: string;
53+
/** used to initially authenticate or re-authenticate user */
54+
'X-Firebase-Auth-Token'?: string;
55+
/** used to initially attest app */
56+
'X-Firebase-App-Check'?: string;
5757
/** SDK telemetry header */
5858
'X-Goog-Api-Client'?: string;
5959
/** firebase appid */

packages/data-connect/test/unit/streamTransport.test.ts

Lines changed: 77 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,10 @@ class TestStreamTransport extends AbstractDataConnectStreamTransport {
8484
}
8585

8686
authProvider = {
87-
getAuth: () => ({ getUid: () => this._authToken })
88-
} as AuthTokenProvider;
87+
getAuth: () => ({ getUid: () => this._authToken }),
88+
getToken: (forceToken?: boolean) =>
89+
Promise.resolve({ accessToken: this._authToken || 'token' })
90+
} as unknown as AuthTokenProvider;
8991

9092
/** Manually set app check token for testing purposes. */
9193
setAppCheckToken(token: string | null): void {
@@ -116,6 +118,11 @@ interface TransportWithInternals {
116118
triggerOnConnectionReady(): void;
117119
closeConnection(): Promise<void>;
118120
cancelClose(): void;
121+
sendRequestMessage<Variables>(
122+
requestBody: DataConnectStreamRequest<Variables>
123+
): Promise<void>;
124+
getWithAuth(forceToken?: boolean): Promise<string | null>;
125+
hasWaitedForInitialAuth: boolean;
119126
prepareMessage<
120127
Variables,
121128
StreamBody extends DataConnectStreamRequest<Variables>
@@ -209,12 +216,40 @@ describe('AbstractDataConnectStreamTransport', () => {
209216
transport.setAuthToken(initialAuthToken);
210217
transport.setAppCheckToken(initialAppCheckToken);
211218
transport.appId = initialAppId;
219+
transport.hasWaitedForInitialAuth = true;
212220
});
213221

214222
afterEach(() => {
215223
sinon.restore();
216224
});
217225

226+
describe('sendRequestMessage', () => {
227+
it('should wait until auth token and app check token have been initialized before sending the message', async () => {
228+
transport.hasWaitedForInitialAuth = false;
229+
230+
let resolveAuth!: () => void;
231+
const authPromise = new Promise<string | null>(resolve => {
232+
resolveAuth = () => resolve('token');
233+
});
234+
235+
const getWithAuthStub = sinon
236+
.stub(transport, 'getWithAuth')
237+
.returns(authPromise);
238+
const sendMessageSpy = sinon.spy(transport, 'sendMessage');
239+
240+
const promise = transport.sendRequestMessage(unpreparedMessage);
241+
242+
expect(getWithAuthStub).to.have.been.calledOnce;
243+
expect(sendMessageSpy).to.not.have.been.called;
244+
245+
resolveAuth();
246+
await promise;
247+
248+
expect(sendMessageSpy).to.have.been.calledOnce;
249+
expect(sendMessageSpy).to.have.been.calledAfter(getWithAuthStub);
250+
});
251+
});
252+
218253
describe('prepareMessage', () => {
219254
it('should not change data fields', () => {
220255
const preparedMessage = transport.prepareMessage(
@@ -235,30 +270,34 @@ describe('AbstractDataConnectStreamTransport', () => {
235270
unpreparedMessage
236271
) as DataConnectStreamRequest<unknown>;
237272
expect(preparedMessage.headers).to.exist;
238-
expect(preparedMessage.headers?.authToken).to.equal(initialAuthToken);
273+
expect(preparedMessage.headers?.['X-Firebase-Auth-Token']).to.equal(
274+
initialAuthToken
275+
);
239276
});
240277

241278
it('should NOT add the same auth token to subsequent messages', () => {
242279
transport.prepareMessage(unpreparedMessage);
243280
const secondPreparedMessage = transport.prepareMessage(
244281
unpreparedMessage
245282
) as DataConnectStreamRequest<unknown>;
246-
expect(secondPreparedMessage.headers?.authToken).to.be.undefined;
283+
expect(secondPreparedMessage.headers?.['X-Firebase-Auth-Token']).to.be
284+
.undefined;
247285
});
248286

249287
it('should include auth token when it changes', () => {
250288
transport.prepareMessage(unpreparedMessage);
251289
const secondPreparedMessage = transport.prepareMessage(
252290
unpreparedMessage
253291
) as DataConnectStreamRequest<unknown>;
254-
expect(secondPreparedMessage.headers?.authToken).to.be.undefined;
292+
expect(secondPreparedMessage.headers?.['X-Firebase-Auth-Token']).to.be
293+
.undefined;
255294
transport.setAuthToken(newAuthToken);
256295
const thirdPreparedMessage = transport.prepareMessage(
257296
unpreparedMessage
258297
) as DataConnectStreamRequest<unknown>;
259-
expect(thirdPreparedMessage.headers?.authToken).to.equal(
260-
newAuthToken
261-
);
298+
expect(
299+
thirdPreparedMessage.headers?.['X-Firebase-Auth-Token']
300+
).to.equal(newAuthToken);
262301
});
263302
});
264303

@@ -268,30 +307,33 @@ describe('AbstractDataConnectStreamTransport', () => {
268307
unpreparedMessage
269308
) as DataConnectStreamRequest<unknown>;
270309
expect(firstPreparedMessage.headers).to.exist;
271-
expect(firstPreparedMessage.headers?.appCheckToken).to.equal(
272-
initialAppCheckToken
273-
);
310+
expect(
311+
firstPreparedMessage.headers?.['X-Firebase-App-Check']
312+
).to.equal(initialAppCheckToken);
274313
});
275314

276315
it('should NOT add the same app check token to subsequent messages', () => {
277316
transport.prepareMessage(unpreparedMessage);
278317
const secondPreparedMessage = transport.prepareMessage(
279318
unpreparedMessage
280319
) as DataConnectStreamRequest<unknown>;
281-
expect(secondPreparedMessage.headers?.appCheckToken).to.be.undefined;
320+
expect(secondPreparedMessage.headers?.['X-Firebase-App-Check']).to.be
321+
.undefined;
282322
});
283323

284324
it('should NOT include app check token when it changes', () => {
285325
transport.prepareMessage(unpreparedMessage);
286326
const secondPreparedMessage = transport.prepareMessage(
287327
unpreparedMessage
288328
) as DataConnectStreamRequest<unknown>;
289-
expect(secondPreparedMessage.headers?.appCheckToken).to.be.undefined;
329+
expect(secondPreparedMessage.headers?.['X-Firebase-App-Check']).to.be
330+
.undefined;
290331
transport.setAppCheckToken(newAppCheckToken);
291332
const thirdPreparedMessage = transport.prepareMessage(
292333
unpreparedMessage
293334
) as DataConnectStreamRequest<unknown>;
294-
expect(thirdPreparedMessage.headers?.appCheckToken).to.be.undefined;
335+
expect(thirdPreparedMessage.headers?.['X-Firebase-App-Check']).to.be
336+
.undefined;
295337
});
296338
});
297339

@@ -374,8 +416,8 @@ describe('AbstractDataConnectStreamTransport', () => {
374416
unpreparedMessage
375417
) as DataConnectStreamRequest<unknown>;
376418
expect(secondMessage.name).to.be.undefined;
377-
expect(secondMessage.headers?.appCheckToken).to.be.undefined;
378-
expect(secondMessage.headers?.authToken).to.be.undefined;
419+
expect(secondMessage.headers?.['X-Firebase-App-Check']).to.be.undefined;
420+
expect(secondMessage.headers?.['X-Firebase-Auth-Token']).to.be.undefined;
379421

380422
// Trigger the physical connection reset
381423
transport.triggerOnConnectionReady();
@@ -385,10 +427,12 @@ describe('AbstractDataConnectStreamTransport', () => {
385427
unpreparedMessage
386428
) as DataConnectStreamRequest<unknown>;
387429
expect(thirdMessage.name).to.equal(expectedName);
388-
expect(thirdMessage.headers?.appCheckToken).to.equal(
430+
expect(thirdMessage.headers?.['X-Firebase-App-Check']).to.equal(
389431
initialAppCheckToken
390432
);
391-
expect(thirdMessage.headers?.authToken).to.equal(initialAuthToken);
433+
expect(thirdMessage.headers?.['X-Firebase-Auth-Token']).to.equal(
434+
initialAuthToken
435+
);
392436
});
393437
});
394438

@@ -592,21 +636,23 @@ describe('AbstractDataConnectStreamTransport', () => {
592636
onDisconnect: sinon.spy(),
593637
onError: sinon.spy()
594638
};
639+
640+
const sendMessageStub = sinon.stub(transport, 'sendMessage');
641+
sendMessageStub.onFirstCall().resolves();
642+
sendMessageStub.onSecondCall().rejects(expectedError);
643+
595644
transport.invokeSubscribe(observer, queryName1, variables1);
596645

597646
const expectedKey = transport.getMapKey(queryName1, variables1);
598647
const subscribeRequest =
599648
transport.activeSubscribeRequests.get(expectedKey);
600649
const subscribeRequestId = subscribeRequest?.requestId!;
601650

602-
const sendMessageStub = sinon
603-
.stub(transport, 'sendMessage')
604-
.rejects(expectedError);
605651
transport.invokeUnsubscribe(queryName1, variables1);
606652
// invokeUnsubscribe's sendMessage is fire and forget
607653
await sleep(500);
608654

609-
expect(sendMessageStub).to.have.been.calledOnce;
655+
expect(sendMessageStub).to.have.been.calledTwice;
610656
expect(logErrorStub).to.have.been.calledOnce;
611657
expect(logErrorStub).to.have.been.calledWithMatch(
612658
'Stream Transport failed to send unsubscribe message'
@@ -1025,10 +1071,10 @@ describe('AbstractDataConnectStreamTransport', () => {
10251071
await transport.invokeSubscribe(observer, queryName1, variables1);
10261072
await transport.invokeUnsubscribe(queryName1, variables1);
10271073

1028-
clock.tick(1000 * 59);
1074+
await clock.tickAsync(1000 * 59);
10291075
expect(closeSpy).to.not.have.been.called;
10301076

1031-
clock.tick(1000 * 2);
1077+
await clock.tickAsync(1000 * 2);
10321078
expect(closeSpy).to.have.been.calledOnce;
10331079
});
10341080

@@ -1044,12 +1090,12 @@ describe('AbstractDataConnectStreamTransport', () => {
10441090
await transport.invokeSubscribe(observer, queryName1, variables1);
10451091
await transport.invokeUnsubscribe(queryName1, variables1);
10461092

1047-
clock.tick(1000 * 30);
1093+
await clock.tickAsync(1000 * 30);
10481094
expect(closeSpy).to.not.have.been.called;
10491095

10501096
await transport.invokeSubscribe(observer, queryName2, variables2);
10511097

1052-
clock.tick(1000 * 65);
1098+
await clock.tickAsync(1000 * 65);
10531099
expect(closeSpy).to.not.have.been.called;
10541100
});
10551101

@@ -1066,16 +1112,16 @@ describe('AbstractDataConnectStreamTransport', () => {
10661112
await transport.invokeSubscribe(observer, queryName1, variables1);
10671113
await transport.invokeUnsubscribe(queryName1, variables1);
10681114

1069-
clock.tick(1000 * 30);
1115+
await clock.tickAsync(1000 * 30);
10701116
expect(closeSpy).to.not.have.been.called;
10711117

10721118
sendMessageStub.rejects();
10731119
await transport.invokeSubscribe(observer, queryName2, variables2);
10741120

1075-
clock.tick(1000 * 30);
1121+
await clock.tickAsync(1000 * 30);
10761122
expect(closeSpy).to.not.have.been.called;
10771123

1078-
clock.tick(1000 * 35);
1124+
await clock.tickAsync(1000 * 35);
10791125
expect(closeSpy).to.have.been.calledOnce;
10801126
});
10811127

@@ -1093,7 +1139,7 @@ describe('AbstractDataConnectStreamTransport', () => {
10931139

10941140
void transport.invokeQuery(queryName2, variables2);
10951141

1096-
clock.tick(1000 * 65);
1142+
await clock.tickAsync(1000 * 65);
10971143
expect(closeSpy).to.not.have.been.called;
10981144
});
10991145

@@ -1111,7 +1157,7 @@ describe('AbstractDataConnectStreamTransport', () => {
11111157

11121158
const queryPromise = transport.invokeQuery(queryName2, variables2);
11131159

1114-
clock.tick(1000 * 65);
1160+
await clock.tickAsync(1000 * 65);
11151161
expect(closeSpy).to.not.have.been.called;
11161162

11171163
const expectedKey = transport.getMapKey(queryName2, variables2);

0 commit comments

Comments
 (0)