@remit/backend 0.0.130 → 0.0.132

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/backend",
3
- "version": "0.0.130",
3
+ "version": "0.0.132",
4
4
  "description": "Remit Mail Inspector API backend",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -251,7 +251,7 @@ describe("the Microsoft consent for the services an account syncs", () => {
251
251
 
252
252
  assert.equal(
253
253
  location,
254
- `${WEB_ORIGIN}/settings/accounts?oauthError=scope_not_granted`,
254
+ `${WEB_ORIGIN}/settings/accounts?oauthError=scope_not_granted&oauthEmail=person%40example.com&missingServices=Calendar`,
255
255
  );
256
256
  assert.deepEqual(
257
257
  await client.account.listAllByAccountConfig(deriveAccountConfigId(sub)),
@@ -236,7 +236,24 @@ export function getWebOrigin(): string {
236
236
 
237
237
  type ScopeRedemption =
238
238
  | { kind: "granted"; refreshToken: string; grantedScopes: string[] }
239
- | { kind: "refused"; reason: "exchange_failed" | "scope_not_granted" };
239
+ | { kind: "refused"; reason: "exchange_failed" }
240
+ | {
241
+ kind: "refused";
242
+ reason: "scope_not_granted";
243
+ service: AccountServiceName;
244
+ };
245
+
246
+ const scopeRefusalUrl = (
247
+ webOrigin: string,
248
+ email: string,
249
+ missingServices: readonly AccountServiceName[],
250
+ ): string => {
251
+ const url = new URL(`${webOrigin}/settings/accounts`);
252
+ url.searchParams.set("oauthError", "scope_not_granted");
253
+ url.searchParams.set("oauthEmail", email);
254
+ url.searchParams.set("missingServices", missingServices.join(","));
255
+ return url.toString();
256
+ };
240
257
 
241
258
  const redeemEachService = async (
242
259
  serviceFor: (service: AccountServiceName) => MailOAuthService,
@@ -265,8 +282,10 @@ const redeemEachService = async (
265
282
  ? ("exchange_failed" as const)
266
283
  : ("scope_not_granted" as const);
267
284
  });
268
- if (typeof redeemed === "string")
285
+ if (redeemed === "exchange_failed")
269
286
  return { kind: "refused", reason: redeemed };
287
+ if (redeemed === "scope_not_granted")
288
+ return { kind: "refused", reason: redeemed, service };
270
289
  grantedScopes.push(...redeemed.grantedScopes);
271
290
  token = redeemed.refreshToken ?? token;
272
291
  }
@@ -512,6 +531,11 @@ export const MicrosoftOAuthOperations: Record<
512
531
  services.filter((service) => service !== firstService),
513
532
  accountConfigId,
514
533
  );
534
+ if (
535
+ redemption.kind === "refused" &&
536
+ redemption.reason === "scope_not_granted"
537
+ )
538
+ return redirect(scopeRefusalUrl(webOrigin, email, [redemption.service]));
515
539
  if (redemption.kind === "refused")
516
540
  return redirect(
517
541
  `${webOrigin}/settings/accounts?oauthError=${redemption.reason}`,
@@ -535,9 +559,7 @@ export const MicrosoftOAuthOperations: Record<
535
559
  },
536
560
  "MS OAuth callback: granted scopes lack a requested service",
537
561
  );
538
- return redirect(
539
- `${webOrigin}/settings/accounts?oauthError=scope_not_granted`,
540
- );
562
+ return redirect(scopeRefusalUrl(webOrigin, email, missingServices));
541
563
  }
542
564
 
543
565
  // Ensure the account config row exists for this user
@@ -46,6 +46,7 @@ import {
46
46
  type CalendarOutcome,
47
47
  calendarDepsOf,
48
48
  findCalendarFor,
49
+ findWritableCalendarFor,
49
50
  listCalendarsFor,
50
51
  notFound,
51
52
  preconditionFailed,
@@ -348,7 +349,7 @@ export const createCalendarEventFor = async (
348
349
  accountConfigId: string,
349
350
  input: CreateCalendarEventInput,
350
351
  ): Promise<CalendarOutcome<CalendarObjectItem>> => {
351
- const collection = await findCalendarFor(
352
+ const collection = await findWritableCalendarFor(
352
353
  deps,
353
354
  accountConfigId,
354
355
  input.calendarId,
@@ -388,12 +389,12 @@ const resolveResource = async (
388
389
  deps: CalendarEventDeps,
389
390
  accountConfigId: string,
390
391
  request: Pick<ScopedRequest, "calendarId" | "calendarObjectId">,
392
+ intent: "read" | "write",
391
393
  ): Promise<CalendarOutcome<ResolvedResource>> => {
392
- const collection = await findCalendarFor(
393
- deps,
394
- accountConfigId,
395
- request.calendarId,
396
- );
394
+ const collection =
395
+ intent === "write"
396
+ ? await findWritableCalendarFor(deps, accountConfigId, request.calendarId)
397
+ : await findCalendarFor(deps, accountConfigId, request.calendarId);
397
398
  if (!collection.ok) return collection;
398
399
 
399
400
  const object = await deps.calendarObject.find(
@@ -415,7 +416,12 @@ export const updateCalendarEventFor = async (
415
416
  request: ScopedRequest,
416
417
  patch: Partial<CalendarEventFields>,
417
418
  ): Promise<CalendarOutcome<CalendarObjectItem | null>> => {
418
- const resolved = await resolveResource(deps, accountConfigId, request);
419
+ const resolved = await resolveResource(
420
+ deps,
421
+ accountConfigId,
422
+ request,
423
+ "write",
424
+ );
419
425
  if (!resolved.ok) return resolved;
420
426
  const { collection, object } = resolved.value;
421
427
 
@@ -458,7 +464,12 @@ export const deleteCalendarEventFor = async (
458
464
  accountConfigId: string,
459
465
  request: ScopedRequest,
460
466
  ): Promise<CalendarOutcome<CalendarObjectItem | null>> => {
461
- const resolved = await resolveResource(deps, accountConfigId, request);
467
+ const resolved = await resolveResource(
468
+ deps,
469
+ accountConfigId,
470
+ request,
471
+ "write",
472
+ );
462
473
  if (!resolved.ok) return resolved;
463
474
  const { collection, object } = resolved.value;
464
475
 
@@ -646,6 +657,7 @@ export const CalendarEventDetailOperations: Record<
646
657
  deps,
647
658
  accountConfigId,
648
659
  request.value,
660
+ "read",
649
661
  );
650
662
  if (!resolved.ok) return answerRefusal(resolved.error);
651
663
  return toEventResponse(resolved.value.object);
@@ -9,6 +9,7 @@ import {
9
9
  } from "@remit/data-ports";
10
10
  import { BadRequestError } from "@remit/data-ports/errors";
11
11
  import {
12
+ CalendarSource,
12
13
  CalendarSuggestionState,
13
14
  FilterClauseField,
14
15
  FilterMatchOperator,
@@ -224,7 +225,15 @@ export const CalendarSuggestionActionOperations: Record<
224
225
  // Reads the collection through the caller's own account config, so a
225
226
  // calendarId naming somebody else's collection is a 404 before anything
226
227
  // is written into it.
227
- await client.calendarCollection.get(accountConfigId, calendarId);
228
+ const collection = await client.calendarCollection.get(
229
+ accountConfigId,
230
+ calendarId,
231
+ );
232
+ if (collection.source === CalendarSource.Subscribed) {
233
+ throw new BadRequestError(
234
+ `"${collection.displayName}" is subscribed to a feed and is read-only — accept the invitation into another calendar`,
235
+ );
236
+ }
228
237
 
229
238
  const accepted = await acceptCalendarSuggestion(client.calendarUnitOfWork, {
230
239
  accountConfigId,
@@ -1,5 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { after, before, describe, it } from "node:test";
3
+ import type { CalendarFeedFetcher } from "@remit/calendar-service";
3
4
  import type {
4
5
  CalendarCollectionItem,
5
6
  CalendarEventIndexItem,
@@ -862,3 +863,218 @@ describe("the calendar collection wrappers", () => {
862
863
  );
863
864
  });
864
865
  });
866
+
867
+ const FEED_URL =
868
+ "https://calendar.example/calendar/ical/rota%40example.com/private-0ddba11/basic.ics";
869
+
870
+ const feedOf = (...events: string[][]): string =>
871
+ `${[
872
+ "BEGIN:VCALENDAR",
873
+ "VERSION:2.0",
874
+ "PRODID:-//Example//Published Calendar//EN",
875
+ "METHOD:PUBLISH",
876
+ ...events.flat(),
877
+ "END:VCALENDAR",
878
+ ].join("\r\n")}\r\n`;
879
+
880
+ const FERRY = [
881
+ "BEGIN:VEVENT",
882
+ "UID:ferry@calendar.example",
883
+ "DTSTAMP:20260901T000000Z",
884
+ "DTSTART:20260908T070000Z",
885
+ "DTEND:20260908T080000Z",
886
+ "RRULE:FREQ=DAILY;COUNT=3",
887
+ "SUMMARY:Ferry crossing",
888
+ "END:VEVENT",
889
+ ];
890
+
891
+ const LOCKS = [
892
+ "BEGIN:VEVENT",
893
+ "UID:locks@calendar.example",
894
+ "DTSTAMP:20260901T000000Z",
895
+ "DTSTART;VALUE=DATE:20260910",
896
+ "DTEND;VALUE=DATE:20260911",
897
+ "SUMMARY:Locks closed",
898
+ "END:VEVENT",
899
+ ];
900
+
901
+ const serving =
902
+ (body: string, status = 200): CalendarFeedFetcher =>
903
+ async () =>
904
+ new Response(body, { status });
905
+
906
+ const subscribe = async (
907
+ account: CalendarAccount,
908
+ fetcher: CalendarFeedFetcher = serving(feedOf(FERRY, LOCKS)),
909
+ ) =>
910
+ createCalendarFor(
911
+ account.eventDeps(),
912
+ account.accountConfigId,
913
+ {
914
+ urlSegment: "harbour",
915
+ displayName: "Harbour",
916
+ subscriptionUrl: FEED_URL.replace("https:", "webcal:"),
917
+ },
918
+ fetcher,
919
+ 1_000,
920
+ );
921
+
922
+ describe("a subscribed calendar", () => {
923
+ it("is created with the feed's events already in it", async () => {
924
+ const account = anAccount();
925
+
926
+ const created = await subscribe(account);
927
+
928
+ assert.ok(created.ok, JSON.stringify(created));
929
+ assert.equal(created.value.source, CalendarSource.Subscribed);
930
+ assert.equal(created.value.subscriptionUrl, FEED_URL);
931
+ assert.equal(created.value.subscriptionEnabled, true);
932
+ assert.equal(created.value.subscriptionFetchedAt, 1_000);
933
+ assert.equal(created.value.subscriptionError, "");
934
+ const objects = await account.objects();
935
+ assert.deepEqual(objects.map((object) => object.summary).sort(), [
936
+ "Ferry crossing",
937
+ "Locks closed",
938
+ ]);
939
+ const ferry = objects.find((object) => object.summary === "Ferry crossing");
940
+ assert.ok(ferry);
941
+ assert.equal((await account.occurrences(ferry)).length, 3);
942
+ });
943
+
944
+ it("is not created when the feed cannot be read, and says why without the address", async () => {
945
+ const account = anAccount();
946
+
947
+ const created = await subscribe(account, serving("nope", 401));
948
+
949
+ assert.ok(!created.ok);
950
+ assert.equal(created.error.code, "SubscriptionUnreachable");
951
+ assert.equal(
952
+ created.error.message,
953
+ "the calendar was not created: the feed answered HTTP 401",
954
+ );
955
+ assert.equal(created.error.message.includes("private-0ddba11"), false);
956
+ assert.deepEqual(await account.collections(), []);
957
+ });
958
+
959
+ it("refuses an address that is not a web address", async () => {
960
+ const account = anAccount();
961
+
962
+ const created = await createCalendarFor(
963
+ account.deps(),
964
+ account.accountConfigId,
965
+ {
966
+ urlSegment: "harbour",
967
+ displayName: "Harbour",
968
+ subscriptionUrl: "file:///etc/passwd",
969
+ },
970
+ serving(feedOf(FERRY)),
971
+ );
972
+
973
+ assert.ok(!created.ok);
974
+ assert.equal(created.error.code, "InvalidSubscriptionUrl");
975
+ });
976
+
977
+ it("refuses a new event, an edit and a delete", async () => {
978
+ const account = anAccount();
979
+ const created = await subscribe(account);
980
+ assert.ok(created.ok);
981
+ const deps = account.eventDeps();
982
+ const [ferry] = (await account.objects()).filter(
983
+ (object) => object.summary === "Ferry crossing",
984
+ );
985
+ assert.ok(ferry);
986
+ const request = {
987
+ calendarId: created.value.calendarId,
988
+ calendarObjectId: ferry.calendarObjectId,
989
+ scope: RecurrenceScope.All,
990
+ recurrenceId: "",
991
+ ifMatch: undefined,
992
+ };
993
+
994
+ const added = await createCalendarEventFor(deps, account.accountConfigId, {
995
+ calendarId: created.value.calendarId,
996
+ summary: "Smuggled in",
997
+ start: "2026-09-09T09:00:00Z",
998
+ end: "2026-09-09T10:00:00Z",
999
+ });
1000
+ const edited = await updateCalendarEventFor(
1001
+ deps,
1002
+ account.accountConfigId,
1003
+ request,
1004
+ { summary: "Renamed" },
1005
+ );
1006
+ const removed = await deleteCalendarEventFor(
1007
+ deps,
1008
+ account.accountConfigId,
1009
+ request,
1010
+ );
1011
+
1012
+ for (const outcome of [added, edited, removed]) {
1013
+ assert.ok(!outcome.ok);
1014
+ assert.equal(outcome.error.code, "ReadOnlyCalendar");
1015
+ }
1016
+ const stored = await account.object(
1017
+ created.value.calendarId,
1018
+ ferry.calendarObjectId,
1019
+ );
1020
+ assert.equal(stored?.etag, ferry.etag);
1021
+ assert.equal((await account.objects()).length, 2);
1022
+ });
1023
+
1024
+ it("turns its refresh off without removing a collection or an event", async () => {
1025
+ const account = anAccount();
1026
+ const created = await subscribe(account);
1027
+ assert.ok(created.ok);
1028
+ const held = await account.objects();
1029
+
1030
+ const paused = await updateCalendarFor(
1031
+ account.deps(),
1032
+ account.accountConfigId,
1033
+ created.value.calendarId,
1034
+ { subscriptionEnabled: false },
1035
+ );
1036
+
1037
+ assert.ok(paused.ok);
1038
+ assert.equal(paused.value.subscriptionEnabled, false);
1039
+ assert.equal(paused.value.source, CalendarSource.Subscribed);
1040
+ assert.deepEqual(
1041
+ (await account.objects()).map((object) => object.etag).sort(),
1042
+ held.map((object) => object.etag).sort(),
1043
+ );
1044
+ const enabled = await client.calendarCollection.listEnabledSubscriptions();
1045
+ assert.equal(
1046
+ enabled.some(
1047
+ (collection) => collection.calendarId === created.value.calendarId,
1048
+ ),
1049
+ false,
1050
+ );
1051
+
1052
+ const resumed = await updateCalendarFor(
1053
+ account.deps(),
1054
+ account.accountConfigId,
1055
+ created.value.calendarId,
1056
+ { subscriptionEnabled: true },
1057
+ );
1058
+ assert.ok(resumed.ok);
1059
+ assert.equal(resumed.value.subscriptionEnabled, true);
1060
+ });
1061
+
1062
+ it("refuses to turn a refresh on for a calendar that has no feed", async () => {
1063
+ const account = anAccount();
1064
+ const [own] = await listCalendarsFor(
1065
+ account.deps(),
1066
+ account.accountConfigId,
1067
+ );
1068
+ assert.ok(own);
1069
+
1070
+ const updated = await updateCalendarFor(
1071
+ account.deps(),
1072
+ account.accountConfigId,
1073
+ own.calendarId,
1074
+ { subscriptionEnabled: true },
1075
+ );
1076
+
1077
+ assert.ok(!updated.ok);
1078
+ assert.equal(updated.error.code, "NotSubscribed");
1079
+ });
1080
+ });
@@ -4,10 +4,14 @@ import type {
4
4
  UpdateCalendarInput,
5
5
  } from "@remit/api-openapi-types";
6
6
  import {
7
+ type CalendarFeedFetcher,
7
8
  DEFAULT_CALENDAR_URL_SEGMENT,
8
9
  isResolvableZone,
9
10
  provisionDefaultCalendar,
10
11
  putCalendarObject,
12
+ readCalendarFeed,
13
+ readSubscriptionUrl,
14
+ writeCalendarFeed,
11
15
  } from "@remit/calendar-service";
12
16
  import type {
13
17
  CalendarCollectionItem,
@@ -114,6 +118,11 @@ const toCalendarResponse = (
114
118
  source: item.source,
115
119
  timezone: item.timezone,
116
120
  syncSequence: item.syncSequence,
121
+ subscriptionUrl: item.subscriptionUrl,
122
+ subscriptionEnabled: item.subscriptionEnabled,
123
+ subscriptionCheckedAt: item.subscriptionCheckedAt,
124
+ subscriptionFetchedAt: item.subscriptionFetchedAt,
125
+ subscriptionError: item.subscriptionError,
117
126
  createdAt: item.createdAt,
118
127
  updatedAt: item.updatedAt,
119
128
  });
@@ -159,6 +168,28 @@ export const findCalendarFor = async (
159
168
  return { ok: true, value: found };
160
169
  };
161
170
 
171
+ /**
172
+ * One of the caller's collections that a person may write events into. A
173
+ * subscribed collection holds what its feed holds: an edit made here would be
174
+ * overwritten by the next refresh, so it is refused rather than accepted and
175
+ * silently lost.
176
+ */
177
+ export const findWritableCalendarFor = async (
178
+ deps: CalendarDeps,
179
+ accountConfigId: string,
180
+ calendarId: string,
181
+ ): Promise<CalendarOutcome<CalendarCollectionItem>> => {
182
+ const found = await findCalendarFor(deps, accountConfigId, calendarId);
183
+ if (!found.ok) return found;
184
+ if (found.value.source === CalendarSource.Subscribed) {
185
+ return refuseCalendar(
186
+ "ReadOnlyCalendar",
187
+ `"${found.value.displayName}" is subscribed to a feed and is read-only — change the event where the feed comes from`,
188
+ );
189
+ }
190
+ return found;
191
+ };
192
+
162
193
  /**
163
194
  * A collection's timezone is what every floating time in it is read in, so a
164
195
  * name this server cannot resolve is not a cosmetic setting — it silently moves
@@ -179,10 +210,66 @@ export const readCollectionTimezone = (
179
210
  return { ok: true, value: timezone };
180
211
  };
181
212
 
213
+ /**
214
+ * Creates a collection subscribed to a feed. The feed is read before anything
215
+ * is written, so an address that cannot be fetched leaves no empty calendar
216
+ * behind; its events then land through the one write path, in the same unit as
217
+ * the collection itself.
218
+ */
219
+ const createSubscribedCalendarFor = async (
220
+ deps: CalendarDeps,
221
+ accountConfigId: string,
222
+ input: CreateCalendarInput & { subscriptionUrl: string },
223
+ collection: { urlSegment: string; timezone: string },
224
+ fetcher: CalendarFeedFetcher,
225
+ now: number,
226
+ ): Promise<CalendarOutcome<CalendarCollectionItem>> => {
227
+ const url = readSubscriptionUrl(input.subscriptionUrl);
228
+ if (!url.ok) return url;
229
+
230
+ const feed = await readCalendarFeed(url.value, fetcher);
231
+ if (!feed.ok) {
232
+ return refuseCalendar(
233
+ "SubscriptionUnreachable",
234
+ `the calendar was not created: ${feed.reason}`,
235
+ );
236
+ }
237
+
238
+ const created = await deps.calendarUnitOfWork.transaction(async (repos) => {
239
+ const made = await repos.calendarCollection.createExclusive({
240
+ accountConfigId,
241
+ urlSegment: collection.urlSegment,
242
+ displayName: input.displayName,
243
+ color: input.color,
244
+ timezone: collection.timezone,
245
+ source: CalendarSource.Subscribed,
246
+ subscriptionUrl: url.value,
247
+ subscriptionEnabled: true,
248
+ });
249
+ if (!made) return null;
250
+ await writeCalendarFeed(
251
+ deps.calendarUnitOfWork,
252
+ { accountConfigId, calendarId: made.calendarId },
253
+ feed.value,
254
+ now,
255
+ );
256
+ return repos.calendarCollection.get(accountConfigId, made.calendarId);
257
+ });
258
+ if (!created) {
259
+ return refuseCalendar(
260
+ "UrlSegmentTaken",
261
+ `"${collection.urlSegment}" already addresses a calendar on this account — pick another`,
262
+ );
263
+ }
264
+ return { ok: true, value: created };
265
+ };
266
+
182
267
  export const createCalendarFor = async (
183
268
  deps: CalendarDeps,
184
269
  accountConfigId: string,
185
270
  input: CreateCalendarInput,
271
+ fetcher: CalendarFeedFetcher = fetch,
272
+ now: number = Date.now(),
186
273
  ): Promise<CalendarOutcome<CalendarCollectionItem>> => {
187
274
  const urlSegment = normalizeCalendarUrlSegment(input.urlSegment);
188
275
  if (urlSegment === "") {
@@ -195,6 +282,18 @@ export const createCalendarFor = async (
195
282
  const timezone = readCollectionTimezone(input.timezone);
196
283
  if (!timezone.ok) return timezone;
197
284
 
285
+ const { subscriptionUrl } = input;
286
+ if (subscriptionUrl !== undefined) {
287
+ return createSubscribedCalendarFor(
288
+ deps,
289
+ accountConfigId,
290
+ { ...input, subscriptionUrl },
291
+ { urlSegment, timezone: timezone.value },
292
+ fetcher,
293
+ now,
294
+ );
295
+ }
296
+
198
297
  // The write decides, not a prior read: two creates of one segment arriving
199
298
  // together would both find it free, and the loser would silently be handed
200
299
  // the winner's calendar to write into.
@@ -264,6 +363,9 @@ export const pickCalendarUpdate = (
264
363
  if (Object.hasOwn(body, "displayName")) patch.displayName = body.displayName;
265
364
  if (Object.hasOwn(body, "color")) patch.color = body.color;
266
365
  if (Object.hasOwn(body, "timezone")) patch.timezone = body.timezone;
366
+ if (Object.hasOwn(body, "subscriptionEnabled")) {
367
+ patch.subscriptionEnabled = body.subscriptionEnabled;
368
+ }
267
369
  return patch;
268
370
  };
269
371
 
@@ -288,6 +390,15 @@ export const updateCalendarFor = async (
288
390
  if (!current.ok) return current;
289
391
 
290
392
  const patch = pickCalendarUpdate(body);
393
+ if (
394
+ patch.subscriptionEnabled !== undefined &&
395
+ current.value.source !== CalendarSource.Subscribed
396
+ ) {
397
+ return refuseCalendar(
398
+ "NotSubscribed",
399
+ `"${current.value.displayName}" is not subscribed to a feed, so there is no refresh to turn on or off`,
400
+ );
401
+ }
291
402
  if (patch.timezone !== undefined) {
292
403
  const timezone = readCollectionTimezone(patch.timezone);
293
404
  if (!timezone.ok) return timezone;
@@ -83,6 +83,35 @@ describe("what a request leaves in the log", () => {
83
83
  );
84
84
  });
85
85
 
86
+ it("never writes the address a calendar subscribes to", async () => {
87
+ const secret = "private-7c0ffee5eed";
88
+ const { result: response, logged } = await captureStdout(() =>
89
+ send({
90
+ httpMethod: "POST",
91
+ path: "/calendars",
92
+ headers: { "Content-Type": "application/json" },
93
+ body: JSON.stringify({
94
+ urlSegment: "harbour",
95
+ displayName: "Harbour",
96
+ subscriptionUrl: `http://127.0.0.1:9/calendar/ical/${secret}/basic.ics`,
97
+ }),
98
+ requestContext: {
99
+ authorizer: { claims: { sub: "log-redaction-subscriber" } },
100
+ } as unknown as APIGatewayProxyEvent["requestContext"],
101
+ }),
102
+ );
103
+
104
+ assert.equal(response.statusCode, 400, response.body);
105
+ assert.match(response.body, /subscription_unreachable/);
106
+ assert.equal(response.body.includes(secret), false);
107
+ assert.ok(logged.length > 0, "the request has to have logged something");
108
+ assert.equal(
109
+ logged.includes(secret),
110
+ false,
111
+ "for a Google secret address the URL is the whole credential",
112
+ );
113
+ });
114
+
86
115
  it("never writes the credentials a failing request arrived with", async () => {
87
116
  // A session is already established, so the bearer is never verified and the
88
117
  // request reaches validation — where the whole parsed request, headers and