@remit/backend 0.0.143 → 0.0.145

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 (279) hide show
  1. package/dist/dev-server/content-handler.d.ts +38 -0
  2. package/dist/dev-server/content-handler.js +46 -0
  3. package/dist/src/api.d.ts +5 -0
  4. package/dist/src/api.js +73 -0
  5. package/dist/src/auth.d.ts +24 -0
  6. package/dist/src/auth.js +81 -0
  7. package/dist/src/config/api-config.d.ts +20 -0
  8. package/dist/src/config/api-config.js +1 -0
  9. package/dist/src/config/from-env.d.ts +2 -0
  10. package/dist/src/config/from-env.js +65 -0
  11. package/dist/src/config/msoauth-secrets-manager.d.ts +2 -0
  12. package/dist/src/config/msoauth-secrets-manager.js +28 -0
  13. package/dist/src/config/msoauth.d.ts +19 -0
  14. package/dist/src/config/msoauth.js +6 -0
  15. package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
  16. package/dist/src/derive/autoMoved.js +49 -0
  17. package/dist/src/derive/contentSignature.d.ts +40 -0
  18. package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +23 -77
  19. package/dist/src/derive/contentUrl.d.ts +33 -0
  20. package/dist/src/derive/contentUrl.js +27 -0
  21. package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
  22. package/dist/src/derive/deriveMuted.js +7 -0
  23. package/dist/src/derive/enrichThreadRows.d.ts +118 -0
  24. package/dist/src/derive/enrichThreadRows.js +151 -0
  25. package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
  26. package/dist/src/derive/filterThreadCriteria.js +24 -0
  27. package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
  28. package/dist/src/derive/pendingMoveCounts.js +53 -0
  29. package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
  30. package/dist/src/derive/senderTrust.js +19 -0
  31. package/dist/src/error.d.ts +21 -0
  32. package/dist/src/error.js +85 -0
  33. package/dist/src/handlers/account-guards.d.ts +36 -0
  34. package/dist/src/handlers/account-guards.js +107 -0
  35. package/dist/src/handlers/account-oauth.d.ts +15 -0
  36. package/dist/src/handlers/account-oauth.js +334 -0
  37. package/dist/src/handlers/account-overrides.d.ts +53 -0
  38. package/dist/src/handlers/account-overrides.js +233 -0
  39. package/dist/src/handlers/account-signature.d.ts +28 -0
  40. package/dist/src/handlers/account-signature.js +85 -0
  41. package/dist/src/handlers/account.d.ts +7 -0
  42. package/dist/src/handlers/account.js +337 -0
  43. package/dist/src/handlers/address.d.ts +35 -0
  44. package/dist/src/handlers/address.js +133 -0
  45. package/dist/src/handlers/calendar-event.d.ts +70 -0
  46. package/dist/src/handlers/calendar-event.js +417 -0
  47. package/dist/src/handlers/calendar-feed.d.ts +47 -0
  48. package/dist/src/handlers/calendar-feed.js +184 -0
  49. package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
  50. package/dist/src/handlers/calendar-suggestion.js +204 -0
  51. package/dist/src/handlers/calendar.d.ts +116 -0
  52. package/dist/src/handlers/calendar.js +323 -0
  53. package/dist/src/handlers/config.d.ts +43 -0
  54. package/dist/src/handlers/config.js +164 -0
  55. package/dist/src/handlers/ensure-account-config.d.ts +9 -0
  56. package/dist/src/handlers/ensure-account-config.js +27 -0
  57. package/dist/src/handlers/filter.d.ts +3 -0
  58. package/dist/src/handlers/filter.js +88 -0
  59. package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
  60. package/dist/src/handlers/folder-role-appointments.js +168 -0
  61. package/dist/src/handlers/folder-role-labels.d.ts +26 -0
  62. package/dist/src/handlers/folder-role-labels.js +40 -0
  63. package/dist/src/handlers/folder-role.d.ts +9 -0
  64. package/dist/src/handlers/folder-role.js +37 -0
  65. package/dist/src/handlers/index.d.ts +88 -0
  66. package/dist/src/handlers/index.js +62 -0
  67. package/dist/src/handlers/label.d.ts +43 -0
  68. package/dist/src/handlers/label.js +107 -0
  69. package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
  70. package/dist/src/handlers/mailbox-bindings.js +61 -0
  71. package/dist/src/handlers/mailbox.d.ts +23 -0
  72. package/dist/src/handlers/mailbox.js +211 -0
  73. package/dist/src/handlers/me.d.ts +2 -0
  74. package/dist/src/handlers/me.js +117 -0
  75. package/dist/src/handlers/message.d.ts +82 -0
  76. package/dist/src/handlers/message.js +595 -0
  77. package/dist/src/handlers/organize.d.ts +10 -0
  78. package/dist/src/handlers/organize.js +103 -0
  79. package/dist/src/handlers/outbox-attachment.d.ts +25 -0
  80. package/dist/src/handlers/outbox-attachment.js +77 -0
  81. package/dist/src/handlers/outbox.d.ts +4 -0
  82. package/dist/src/handlers/outbox.js +136 -0
  83. package/dist/src/handlers/sync-progress.d.ts +46 -0
  84. package/dist/src/handlers/sync-progress.js +63 -0
  85. package/dist/src/handlers/sync.d.ts +28 -0
  86. package/dist/src/handlers/sync.js +73 -0
  87. package/dist/src/handlers/system-update.d.ts +2 -0
  88. package/dist/src/handlers/system-update.js +159 -0
  89. package/dist/src/handlers/thread.d.ts +148 -0
  90. package/dist/src/handlers/thread.js +228 -0
  91. package/dist/src/handlers/unified-threads.d.ts +247 -0
  92. package/dist/src/handlers/unified-threads.js +406 -0
  93. package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
  94. package/dist/src/handlers/vip-suggestions.js +16 -0
  95. package/dist/src/index.d.ts +5 -0
  96. package/dist/src/index.js +53 -0
  97. package/{src/json.ts → dist/src/json.d.ts} +1 -4
  98. package/dist/src/json.js +10 -0
  99. package/dist/src/jwt-auth.d.ts +15 -0
  100. package/dist/src/jwt-auth.js +45 -0
  101. package/dist/src/lambda.d.ts +13 -0
  102. package/dist/src/lambda.js +33 -0
  103. package/dist/src/request.d.ts +9 -0
  104. package/dist/src/request.js +17 -0
  105. package/dist/src/response.d.ts +44 -0
  106. package/dist/src/response.js +80 -0
  107. package/dist/src/serve.d.ts +31 -0
  108. package/dist/src/serve.js +117 -0
  109. package/dist/src/service/account-deletion.d.ts +20 -0
  110. package/dist/src/service/account-deletion.js +33 -0
  111. package/dist/src/service/compose-sqlite.d.ts +3 -0
  112. package/dist/src/service/compose-sqlite.js +13 -0
  113. package/dist/src/service/create-remit-client.d.ts +109 -0
  114. package/dist/src/service/create-remit-client.js +188 -0
  115. package/dist/src/service/data-client.d.ts +12 -0
  116. package/dist/src/service/data-client.js +42 -0
  117. package/dist/src/service/export-identity.d.ts +4 -0
  118. package/dist/src/service/export-identity.js +33 -0
  119. package/dist/src/service/fire-and-forget.d.ts +54 -0
  120. package/dist/src/service/fire-and-forget.js +37 -0
  121. package/dist/src/service/organize.d.ts +181 -0
  122. package/dist/src/service/organize.js +367 -0
  123. package/dist/src/service/queues.d.ts +12 -0
  124. package/dist/src/service/queues.js +1 -0
  125. package/dist/src/service/refusal.d.ts +4 -0
  126. package/dist/src/service/refusal.js +20 -0
  127. package/dist/src/service/sender-category.d.ts +51 -0
  128. package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
  129. package/dist/src/service/shared-deps.d.ts +6 -0
  130. package/dist/src/service/shared-deps.js +24 -0
  131. package/dist/src/service/trigger-sync.d.ts +57 -0
  132. package/dist/src/service/trigger-sync.js +31 -0
  133. package/dist/src/types.d.ts +72 -0
  134. package/dist/src/types.js +1 -0
  135. package/package.json +125 -98
  136. package/.env.test +0 -4
  137. package/dev-server/content-auth.test.ts +0 -113
  138. package/dev-server/content-auth.ts +0 -54
  139. package/dev-server/content-handler.test.ts +0 -105
  140. package/dev-server/content-handler.ts +0 -79
  141. package/dev-server/content-path.test.ts +0 -37
  142. package/dev-server/content-path.ts +0 -27
  143. package/dev-server/cors.test.ts +0 -48
  144. package/dev-server/cors.ts +0 -25
  145. package/dev-server/keep-alive.test.ts +0 -24
  146. package/dev-server/lambda-helpers.ts +0 -69
  147. package/dev-server/log-output.test.ts +0 -173
  148. package/dev-server/oauth-callback-route.test.ts +0 -66
  149. package/dev-server/relational-health.test.ts +0 -83
  150. package/dev-server/relational-health.ts +0 -39
  151. package/dev-server/server.ts +0 -384
  152. package/dev-server/sync-age.test.ts +0 -91
  153. package/dev-server/sync-age.ts +0 -55
  154. package/dev-server/upload-handler.ts +0 -132
  155. package/dev-server/upload-route.test.ts +0 -196
  156. package/scripts/backfill-classification.ts +0 -91
  157. package/scripts/backfill-list-id.ts +0 -83
  158. package/scripts/config-save.ts +0 -128
  159. package/src/auth.test.ts +0 -12
  160. package/src/auth.ts +0 -92
  161. package/src/config/msoauth.test.ts +0 -48
  162. package/src/config/msoauth.ts +0 -48
  163. package/src/data-backend.test.ts +0 -23
  164. package/src/data-backend.ts +0 -14
  165. package/src/derive/autoMoved.test.ts +0 -104
  166. package/src/derive/contentSignature.test.ts +0 -161
  167. package/src/derive/contentUrl.test.ts +0 -172
  168. package/src/derive/contentUrl.ts +0 -86
  169. package/src/derive/deriveMuted.test.ts +0 -34
  170. package/src/derive/enrichThreadRows.test.ts +0 -360
  171. package/src/derive/enrichThreadRows.ts +0 -234
  172. package/src/derive/filterThreadCriteria.test.ts +0 -126
  173. package/src/derive/filterThreadCriteria.ts +0 -51
  174. package/src/derive/senderTrust.test.ts +0 -67
  175. package/src/error.test.ts +0 -201
  176. package/src/error.ts +0 -123
  177. package/src/handlers/account-guards.test.ts +0 -67
  178. package/src/handlers/account-guards.ts +0 -169
  179. package/src/handlers/account-oauth-callback.test.ts +0 -155
  180. package/src/handlers/account-oauth-consent.test.ts +0 -336
  181. package/src/handlers/account-oauth-start.test.ts +0 -191
  182. package/src/handlers/account-oauth.test.ts +0 -439
  183. package/src/handlers/account-oauth.ts +0 -635
  184. package/src/handlers/account-overrides.test.ts +0 -88
  185. package/src/handlers/account-overrides.ts +0 -323
  186. package/src/handlers/account-ownership.ts +0 -25
  187. package/src/handlers/account-signature.ts +0 -117
  188. package/src/handlers/account-synced-services.test.ts +0 -245
  189. package/src/handlers/account.ts +0 -552
  190. package/src/handlers/address.test.ts +0 -381
  191. package/src/handlers/address.ts +0 -200
  192. package/src/handlers/calendar-event.test.ts +0 -685
  193. package/src/handlers/calendar-event.ts +0 -738
  194. package/src/handlers/calendar-feed-validators.test.ts +0 -228
  195. package/src/handlers/calendar-feed.test.ts +0 -439
  196. package/src/handlers/calendar-feed.ts +0 -291
  197. package/src/handlers/calendar-sqlite-fixture.ts +0 -36
  198. package/src/handlers/calendar-suggestion.test.ts +0 -1269
  199. package/src/handlers/calendar-suggestion.ts +0 -425
  200. package/src/handlers/calendar.test.ts +0 -1080
  201. package/src/handlers/calendar.ts +0 -517
  202. package/src/handlers/config.test.ts +0 -245
  203. package/src/handlers/config.ts +0 -332
  204. package/src/handlers/ensure-account-config.ts +0 -30
  205. package/src/handlers/filter.test.ts +0 -644
  206. package/src/handlers/filter.ts +0 -518
  207. package/src/handlers/folder-role-appointments.test.ts +0 -438
  208. package/src/handlers/folder-role-appointments.ts +0 -249
  209. package/src/handlers/folder-role-labels.ts +0 -63
  210. package/src/handlers/folder-role.test.ts +0 -209
  211. package/src/handlers/folder-role.ts +0 -80
  212. package/src/handlers/index.ts +0 -83
  213. package/src/handlers/label.test.ts +0 -112
  214. package/src/handlers/label.ts +0 -210
  215. package/src/handlers/mailbox-bindings.ts +0 -111
  216. package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
  217. package/src/handlers/mailbox-settled-target.test.ts +0 -251
  218. package/src/handlers/mailbox.test.ts +0 -280
  219. package/src/handlers/mailbox.ts +0 -581
  220. package/src/handlers/me.test.ts +0 -81
  221. package/src/handlers/me.ts +0 -203
  222. package/src/handlers/message-read-backfill.test.ts +0 -103
  223. package/src/handlers/message.test.ts +0 -131
  224. package/src/handlers/message.ts +0 -1131
  225. package/src/handlers/organize.test.ts +0 -277
  226. package/src/handlers/organize.ts +0 -197
  227. package/src/handlers/outbox-attachment.test.ts +0 -765
  228. package/src/handlers/outbox-attachment.ts +0 -126
  229. package/src/handlers/outbox-list.test.ts +0 -179
  230. package/src/handlers/outbox.test.ts +0 -772
  231. package/src/handlers/outbox.ts +0 -258
  232. package/src/handlers/search.test.ts +0 -161
  233. package/src/handlers/search.ts +0 -105
  234. package/src/handlers/sync-progress.test.ts +0 -205
  235. package/src/handlers/sync-progress.ts +0 -99
  236. package/src/handlers/sync.test.ts +0 -168
  237. package/src/handlers/sync.ts +0 -118
  238. package/src/handlers/system-update.test.ts +0 -497
  239. package/src/handlers/system-update.ts +0 -230
  240. package/src/handlers/thread.test.ts +0 -396
  241. package/src/handlers/thread.ts +0 -427
  242. package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
  243. package/src/handlers/unified-threads.test.ts +0 -785
  244. package/src/handlers/unified-threads.ts +0 -669
  245. package/src/index.test.ts +0 -83
  246. package/src/index.ts +0 -228
  247. package/src/jwt-auth.test.ts +0 -152
  248. package/src/jwt-auth.ts +0 -132
  249. package/src/log-redaction.test.ts +0 -144
  250. package/src/openapi-spec.test.ts +0 -118
  251. package/src/request-context.test.ts +0 -61
  252. package/src/request-context.ts +0 -33
  253. package/src/request-pipeline.test.ts +0 -88
  254. package/src/request.ts +0 -10
  255. package/src/response.test.ts +0 -130
  256. package/src/response.ts +0 -144
  257. package/src/service/compose-sqlite.ts +0 -98
  258. package/src/service/config-import.ts +0 -39
  259. package/src/service/create-remit-client.ts +0 -522
  260. package/src/service/data-client.test.ts +0 -97
  261. package/src/service/data-client.ts +0 -58
  262. package/src/service/export-identity.ts +0 -39
  263. package/src/service/filter.ts +0 -40
  264. package/src/service/fire-and-forget.test.ts +0 -126
  265. package/src/service/fire-and-forget.ts +0 -79
  266. package/src/service/localhost-env-config.test.ts +0 -36
  267. package/src/service/organize-queue.ts +0 -98
  268. package/src/service/organize.test.ts +0 -1451
  269. package/src/service/organize.ts +0 -1025
  270. package/src/service/read-path-filter-config.test.ts +0 -243
  271. package/src/service/semantic-capability.test.ts +0 -78
  272. package/src/service/semantic-capability.ts +0 -95
  273. package/src/service/sender-category.test.ts +0 -235
  274. package/src/service/sqs.ts +0 -4
  275. package/src/service/trigger-sync.test.ts +0 -205
  276. package/src/service/trigger-sync.ts +0 -100
  277. package/src/stdout-capture-fixture.ts +0 -40
  278. package/src/types.ts +0 -271
  279. package/tsconfig.json +0 -7
@@ -1,228 +0,0 @@
1
- /**
2
- * Issue #1067: which timestamp a feed's validators are taken from.
3
- *
4
- * Driven against a store rather than through the API, because the decision only
5
- * shows when the collection and the events in it carry times a test running
6
- * inside one second cannot produce: here the calendar is stamped an hour after
7
- * its newest surviving event, which is the shape a delete leaves behind.
8
- */
9
-
10
- import assert from "node:assert/strict";
11
- import { describe, it } from "node:test";
12
- import {
13
- hashCalendarFeedToken,
14
- mintCalendarFeedToken,
15
- putCalendarObject,
16
- } from "@remit/calendar-service";
17
- import { MemoryCalendarStore } from "@remit/calendar-service/memory-store";
18
- import type { CalendarDeps } from "./calendar.js";
19
- import { serveCalendarFeed } from "./calendar-feed.js";
20
-
21
- const ACCOUNT_CONFIG_ID = "feed-validators-account";
22
- const EVENT_WRITTEN_AT = Date.UTC(2026, 8, 7, 9, 0, 0);
23
- const CALENDAR_TOUCHED_AT = Date.UTC(2026, 8, 7, 10, 0, 0);
24
-
25
- const ICAL = [
26
- "BEGIN:VCALENDAR",
27
- "VERSION:2.0",
28
- "PRODID:-//test//test//EN",
29
- "BEGIN:VEVENT",
30
- "UID:kept@test",
31
- "DTSTAMP:20260907T090000Z",
32
- "DTSTART:20260907T090000Z",
33
- "DTEND:20260907T100000Z",
34
- "SUMMARY:Kept",
35
- "END:VEVENT",
36
- "END:VCALENDAR",
37
- ].join("\r\n");
38
-
39
- const depsOf = (store: MemoryCalendarStore): Promise<CalendarDeps> =>
40
- store.transaction(async (repos) => ({
41
- ...repos,
42
- calendarUnitOfWork: store,
43
- }));
44
-
45
- const tokenFor = async (
46
- store: MemoryCalendarStore,
47
- calendarId: string,
48
- ): Promise<string> => {
49
- const minted = mintCalendarFeedToken();
50
- await store.transaction(async (repos) =>
51
- repos.calendarFeedToken.put({
52
- accountConfigId: ACCOUNT_CONFIG_ID,
53
- calendarId,
54
- tokenHash: minted.tokenHash,
55
- }),
56
- );
57
- return minted.token;
58
- };
59
-
60
- interface Fixture {
61
- deps: CalendarDeps;
62
- token: string;
63
- readonly listedCollections: number;
64
- }
65
-
66
- const fixture = async (): Promise<Fixture> => {
67
- const store = new MemoryCalendarStore();
68
- const deps = await depsOf(store);
69
-
70
- const collection = await store.transaction(async (repos) =>
71
- repos.calendarCollection.create({
72
- accountConfigId: ACCOUNT_CONFIG_ID,
73
- urlSegment: "work",
74
- displayName: "Work",
75
- }),
76
- );
77
-
78
- const written = await putCalendarObject(store, {
79
- accountConfigId: ACCOUNT_CONFIG_ID,
80
- calendarId: collection.calendarId,
81
- resourceName: "kept.ics",
82
- icalData: ICAL,
83
- });
84
- assert.ok(written.ok, "the fixture's own event has to be storable");
85
-
86
- // The shape a delete leaves: the calendar was stamped by the delete, and every
87
- // event still in it is older than that.
88
- store.objects.set(written.value.calendarObjectId, {
89
- ...written.value,
90
- updatedAt: EVENT_WRITTEN_AT,
91
- });
92
- const bumped = store.collections.get(collection.calendarId);
93
- assert.ok(bumped, "the write should have stamped the collection");
94
- store.collections.set(collection.calendarId, {
95
- ...bumped,
96
- updatedAt: CALENDAR_TOUCHED_AT,
97
- });
98
-
99
- const token = await tokenFor(store, collection.calendarId);
100
-
101
- let listedCollections = 0;
102
- const counted: CalendarDeps = {
103
- ...deps,
104
- calendarCollection: {
105
- ...deps.calendarCollection,
106
- listByAccountConfig: async (accountConfigId: string) => {
107
- listedCollections += 1;
108
- return deps.calendarCollection.listByAccountConfig(accountConfigId);
109
- },
110
- },
111
- };
112
-
113
- return {
114
- deps: counted,
115
- token,
116
- get listedCollections() {
117
- return listedCollections;
118
- },
119
- };
120
- };
121
-
122
- const NO_CONDITIONS = { ifNoneMatch: undefined, ifModifiedSince: undefined };
123
-
124
- describe("the validators a feed serves", () => {
125
- it("dates the calendar by its own last change, not by its newest event", async () => {
126
- const { deps, token } = await fixture();
127
-
128
- const served = await serveCalendarFeed(deps, token, NO_CONDITIONS);
129
-
130
- assert.equal(served.statusCode, 200);
131
- assert.match(served.body, /SUMMARY:Kept/);
132
- assert.equal(
133
- served.headers["Last-Modified"],
134
- new Date(CALENDAR_TOUCHED_AT).toUTCString(),
135
- "a delete takes the newest event, so every survivor is older than the change",
136
- );
137
- });
138
-
139
- it("is unchanged for the date it served and changed for the one before it", async () => {
140
- const { deps, token } = await fixture();
141
- const served = await serveCalendarFeed(deps, token, NO_CONDITIONS);
142
- const lastModified = served.headers["Last-Modified"];
143
-
144
- const unchanged = await serveCalendarFeed(deps, token, {
145
- ifNoneMatch: undefined,
146
- ifModifiedSince: lastModified,
147
- });
148
- const stale = await serveCalendarFeed(deps, token, {
149
- ifNoneMatch: undefined,
150
- ifModifiedSince: new Date(CALENDAR_TOUCHED_AT - 1000).toUTCString(),
151
- });
152
-
153
- assert.equal(unchanged.statusCode, 304);
154
- assert.equal(unchanged.body, "");
155
- assert.equal(unchanged.headers["Last-Modified"], lastModified);
156
- assert.equal(unchanged.headers.ETag, served.headers.ETag);
157
- assert.equal(stale.statusCode, 200);
158
- });
159
-
160
- it("lets a tag that arrives settle the answer on its own", async () => {
161
- // RFC 9110 13.2.2: the date is not consulted when a tag is present, so a
162
- // tag nobody served re-sends the calendar however current the date is.
163
- const { deps, token } = await fixture();
164
- const served = await serveCalendarFeed(deps, token, NO_CONDITIONS);
165
-
166
- const overridden = await serveCalendarFeed(deps, token, {
167
- ifNoneMatch: '"not-the-tag"',
168
- ifModifiedSince: served.headers["Last-Modified"],
169
- });
170
-
171
- assert.equal(overridden.statusCode, 200);
172
- assert.match(overridden.body, /SUMMARY:Kept/);
173
- });
174
-
175
- it("reads the one collection it needs rather than every collection there is", async () => {
176
- const found = await fixture();
177
-
178
- await serveCalendarFeed(found.deps, found.token, NO_CONDITIONS);
179
-
180
- assert.equal(
181
- found.listedCollections,
182
- 0,
183
- "a feed poll is the hot path and the token already names its collection",
184
- );
185
- });
186
-
187
- it("is absent when the token outlives the calendar it named", async () => {
188
- const store = new MemoryCalendarStore();
189
- const deps = await depsOf(store);
190
- const token = await tokenFor(store, "a-calendar-that-is-gone");
191
-
192
- const served = await serveCalendarFeed(deps, token, NO_CONDITIONS);
193
-
194
- assert.equal(served.statusCode, 404);
195
- assert.equal(
196
- served.body.includes("BEGIN:VCALENDAR"),
197
- false,
198
- "a blank calendar would be worse than saying nothing",
199
- );
200
- });
201
-
202
- it("is absent for an unknown, a malformed and an oversize token alike", async () => {
203
- const store = new MemoryCalendarStore();
204
- const deps = await depsOf(store);
205
-
206
- const unknown = await serveCalendarFeed(
207
- deps,
208
- mintCalendarFeedToken().token,
209
- NO_CONDITIONS,
210
- );
211
- const malformed = await serveCalendarFeed(deps, "nope", NO_CONDITIONS);
212
- const oversize = await serveCalendarFeed(
213
- deps,
214
- "z".repeat(600),
215
- NO_CONDITIONS,
216
- );
217
-
218
- for (const served of [unknown, malformed, oversize]) {
219
- assert.equal(served.statusCode, 404);
220
- assert.equal(served.body, unknown.body);
221
- }
222
- assert.equal(
223
- hashCalendarFeedToken("nope").length,
224
- 64,
225
- "the digest is what the store is asked about, never the token",
226
- );
227
- });
228
- });
@@ -1,439 +0,0 @@
1
- /**
2
- * Issue #1067: subscribing to a calendar by its secret .ics address.
3
- *
4
- * Driven through the real OpenAPI document, so what is asserted is the routing,
5
- * the status code, the headers and the bytes a subscribed Apple Calendar or
6
- * Thunderbird receives — including that the feed answers with no session while
7
- * the management sub-resource refuses without one. The store is the SQLite
8
- * composition a self-host deployment boots, over the committed migrations.
9
- */
10
-
11
- import assert from "node:assert/strict";
12
- import { after, before, describe, it } from "node:test";
13
- import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
14
- import {
15
- _resetForTest,
16
- type RemitClient,
17
- setClient,
18
- } from "../service/data-client.js";
19
- import { createCalendarSqliteClient } from "./calendar-sqlite-fixture.js";
20
-
21
- let handler: (
22
- event: APIGatewayProxyEvent,
23
- context: unknown,
24
- ) => Promise<APIGatewayProxyResult>;
25
- let client: RemitClient;
26
- let cleanup: () => void;
27
- let mintedAccounts = 0;
28
-
29
- interface Requested {
30
- method: string;
31
- path: string;
32
- sub?: string;
33
- body?: Record<string, unknown>;
34
- headers?: Record<string, string>;
35
- query?: Record<string, string>;
36
- }
37
-
38
- const request = async ({
39
- method,
40
- path,
41
- sub,
42
- body,
43
- headers,
44
- query,
45
- }: Requested): Promise<APIGatewayProxyResult> => {
46
- const event = {
47
- httpMethod: method,
48
- path,
49
- headers: headers ?? {},
50
- queryStringParameters: query ?? null,
51
- body: body === undefined ? null : JSON.stringify(body),
52
- requestContext: sub ? { authorizer: { claims: { sub } } } : {},
53
- } as unknown as APIGatewayProxyEvent;
54
-
55
- return handler(event, { awsRequestId: `req-${path}`, functionName: "test" });
56
- };
57
-
58
- const json = (response: APIGatewayProxyResult): Record<string, unknown> =>
59
- JSON.parse(response.body) as Record<string, unknown>;
60
-
61
- const headerOf = (
62
- response: APIGatewayProxyResult,
63
- name: string,
64
- ): string | undefined => {
65
- for (const [key, value] of Object.entries(response.headers ?? {})) {
66
- if (key.toLowerCase() === name) return String(value);
67
- }
68
- return undefined;
69
- };
70
-
71
- /** One caller, their calendar, and an event in it. */
72
- class Subscriber {
73
- readonly sub: string;
74
-
75
- constructor() {
76
- mintedAccounts += 1;
77
- this.sub = `calendar-feed-sub-${mintedAccounts}`;
78
- }
79
-
80
- async calendar(displayName = "Work"): Promise<string> {
81
- const created = await request({
82
- method: "POST",
83
- path: "/calendars",
84
- sub: this.sub,
85
- body: { urlSegment: `work-${mintedAccounts}`, displayName },
86
- });
87
- assert.equal(created.statusCode, 200, created.body);
88
- return json(created).calendarId as string;
89
- }
90
-
91
- async event(calendarId: string, summary: string, recurrenceRule = "") {
92
- const created = await request({
93
- method: "POST",
94
- path: "/calendar-events",
95
- sub: this.sub,
96
- body: {
97
- calendarId,
98
- summary,
99
- start: "2026-09-07T09:00:00+00:00",
100
- end: "2026-09-07T10:00:00+00:00",
101
- recurrenceRule,
102
- },
103
- });
104
- assert.equal(created.statusCode, 200, created.body);
105
- return json(created);
106
- }
107
-
108
- async deleteEvent(calendarId: string, calendarObjectId: string) {
109
- const removed = await request({
110
- method: "DELETE",
111
- path: `/calendar-events/${calendarObjectId}`,
112
- sub: this.sub,
113
- query: { calendarId },
114
- });
115
- assert.equal(removed.statusCode, 204, removed.body);
116
- }
117
-
118
- async mintFeed(calendarId: string): Promise<string> {
119
- const minted = await request({
120
- method: "PUT",
121
- path: `/calendars/${calendarId}/feed`,
122
- sub: this.sub,
123
- });
124
- assert.equal(minted.statusCode, 200, minted.body);
125
- return json(minted).feedToken as string;
126
- }
127
- }
128
-
129
- const fetchFeed = (
130
- feedToken: string,
131
- headers?: Record<string, string>,
132
- ): Promise<APIGatewayProxyResult> =>
133
- request({
134
- method: "GET",
135
- path: `/feeds/calendar/${feedToken}.ics`,
136
- headers,
137
- });
138
-
139
- before(async () => {
140
- _resetForTest();
141
- // The self-host gate, so the feed's exemption from it is exercised rather
142
- // than assumed. Set before the module loads: it reads the flag per request,
143
- // but the boot-time bypass assertion reads NODE_ENV at import.
144
- process.env.DATA_BACKEND = "sqlite";
145
- ({ client, cleanup } = await createCalendarSqliteClient());
146
- setClient(client);
147
- ({ handler } = (await import("../index.js")) as unknown as {
148
- handler: typeof handler;
149
- });
150
- });
151
-
152
- after(() => {
153
- _resetForTest();
154
- cleanup();
155
- });
156
-
157
- describe("the feed a subscriber polls", () => {
158
- it("serves the calendar as iCalendar text with no session at all", async () => {
159
- const owner = new Subscriber();
160
- const calendarId = await owner.calendar("Team");
161
- await owner.event(calendarId, "Stand-up");
162
- const feedToken = await owner.mintFeed(calendarId);
163
-
164
- const feed = await fetchFeed(feedToken);
165
-
166
- assert.equal(feed.statusCode, 200, feed.body);
167
- assert.equal(
168
- headerOf(feed, "content-type"),
169
- "text/calendar; charset=utf-8",
170
- );
171
- assert.match(feed.body, /^BEGIN:VCALENDAR/);
172
- assert.match(feed.body, /END:VCALENDAR\r\n$/);
173
- assert.match(feed.body, /X-WR-CALNAME:Team/);
174
- assert.match(feed.body, /SUMMARY:Stand-up/);
175
- assert.ok(headerOf(feed, "etag"), "a poller needs a tag to send back");
176
- assert.ok(headerOf(feed, "last-modified"));
177
- });
178
-
179
- it("carries a recurring event as its master and rule, not as occurrences", async () => {
180
- const owner = new Subscriber();
181
- const calendarId = await owner.calendar();
182
- await owner.event(calendarId, "Weekly", "FREQ=WEEKLY;COUNT=5");
183
- const feedToken = await owner.mintFeed(calendarId);
184
-
185
- const feed = await fetchFeed(feedToken);
186
-
187
- assert.equal(feed.body.match(/BEGIN:VEVENT/g)?.length, 1);
188
- assert.match(feed.body, /RRULE:FREQ=WEEKLY;COUNT=5/);
189
- assert.equal(
190
- feed.body.includes("RECURRENCE-ID"),
191
- false,
192
- "expanding the series would end it at whatever horizon this server chose",
193
- );
194
- });
195
-
196
- it("answers 304 to the tag it just served, and re-serves a changed calendar", async () => {
197
- const owner = new Subscriber();
198
- const calendarId = await owner.calendar();
199
- await owner.event(calendarId, "Stand-up");
200
- const feedToken = await owner.mintFeed(calendarId);
201
- const first = await fetchFeed(feedToken);
202
- const etag = headerOf(first, "etag") as string;
203
-
204
- const unchanged = await fetchFeed(feedToken, { "If-None-Match": etag });
205
-
206
- assert.equal(unchanged.statusCode, 304);
207
- assert.equal(unchanged.body, "");
208
- assert.equal(headerOf(unchanged, "etag"), etag);
209
- assert.equal(
210
- headerOf(unchanged, "last-modified"),
211
- headerOf(first, "last-modified"),
212
- "a 304 carries the validators a 200 would have carried",
213
- );
214
-
215
- await owner.event(calendarId, "Retro");
216
- const changed = await fetchFeed(feedToken, { "If-None-Match": etag });
217
-
218
- assert.equal(changed.statusCode, 200);
219
- assert.match(changed.body, /SUMMARY:Retro/);
220
- assert.match(changed.body, /SUMMARY:Stand-up/);
221
- assert.equal(changed.body.match(/BEGIN:VEVENT/g)?.length, 2);
222
- assert.notEqual(headerOf(changed, "etag"), etag);
223
- });
224
-
225
- it("does not serve the feed at an address the token only starts", async () => {
226
- // The route is built by substituting the token into the literal path, which
227
- // leaves the dot of ".ics" matching any character, so /feeds/calendar/
228
- // <token>Xics reaches the feed operation with the token intact. Asserted at
229
- // both layers that refuse it: the public-route gate does not recognise the
230
- // address, and the handler re-derives the token from the raw path rather
231
- // than trusting what the router captured — so a caller who does hold a
232
- // session gets the same 404 rather than somebody's calendar.
233
- const owner = new Subscriber();
234
- const calendarId = await owner.calendar();
235
- await owner.event(calendarId, "Stand-up");
236
- const feedToken = await owner.mintFeed(calendarId);
237
- const nearlyPath = `/feeds/calendar/${feedToken}Xics`;
238
-
239
- const served = await fetchFeed(feedToken);
240
- const unauthenticated = await request({ method: "GET", path: nearlyPath });
241
- const authenticated = await request({
242
- method: "GET",
243
- path: nearlyPath,
244
- sub: owner.sub,
245
- });
246
-
247
- assert.equal(served.statusCode, 200, served.body);
248
- assert.equal(unauthenticated.statusCode, 401);
249
- assert.equal(authenticated.statusCode, 404);
250
- for (const response of [unauthenticated, authenticated]) {
251
- assert.equal(
252
- response.body.includes("BEGIN:VCALENDAR"),
253
- false,
254
- "an address that is not the one handed out must not serve the calendar",
255
- );
256
- }
257
- });
258
-
259
- it("answers an oversize token the same 404 as an unknown one", async () => {
260
- // Not a 400: a length the spec enforces makes an oversize address
261
- // distinguishable from a wrong one, and error-logs the path it refused.
262
- const unknown = await fetchFeed("z".repeat(43));
263
- const oversize = await fetchFeed("z".repeat(600));
264
-
265
- assert.equal(oversize.statusCode, 404);
266
- assert.equal(oversize.body, unknown.body);
267
- assert.equal(
268
- headerOf(oversize, "content-type"),
269
- headerOf(unknown, "content-type"),
270
- );
271
- });
272
-
273
- it("keeps Last-Modified moving forward when an event is deleted", async () => {
274
- // The newest surviving event is not when the calendar last changed: deleting
275
- // the newest one leaves every survivor older than the delete, and a
276
- // validator that moved backwards would tell a subscriber its stale copy is
277
- // still current.
278
- const owner = new Subscriber();
279
- const calendarId = await owner.calendar();
280
- await owner.event(calendarId, "Stand-up");
281
- const retro = await owner.event(calendarId, "Retro");
282
- const feedToken = await owner.mintFeed(calendarId);
283
- const full = await fetchFeed(feedToken);
284
- const before = Date.parse(headerOf(full, "last-modified") as string);
285
-
286
- await owner.deleteEvent(calendarId, retro.calendarObjectId as string);
287
- const shrunk = await fetchFeed(feedToken);
288
-
289
- assert.equal(shrunk.statusCode, 200, shrunk.body);
290
- assert.equal(shrunk.body.includes("SUMMARY:Retro"), false);
291
- assert.ok(
292
- Date.parse(headerOf(shrunk, "last-modified") as string) >= before,
293
- "the calendar changed, so its modification time cannot have gone back",
294
- );
295
- });
296
-
297
- it("answers 304 to the date it just served, and lets the tag override it", async () => {
298
- const owner = new Subscriber();
299
- const calendarId = await owner.calendar();
300
- await owner.event(calendarId, "Stand-up");
301
- const feedToken = await owner.mintFeed(calendarId);
302
- const first = await fetchFeed(feedToken);
303
- const lastModified = headerOf(first, "last-modified") as string;
304
-
305
- const unchanged = await fetchFeed(feedToken, {
306
- "If-Modified-Since": lastModified,
307
- });
308
-
309
- assert.equal(unchanged.statusCode, 304);
310
- assert.equal(headerOf(unchanged, "etag"), headerOf(first, "etag"));
311
- assert.equal(headerOf(unchanged, "last-modified"), lastModified);
312
-
313
- // RFC 9110 13.2.2: a tag that arrives is the whole condition, and the date
314
- // is not consulted. Sending a tag nobody served alongside a date that would
315
- // have earned a 304 has to re-serve the calendar.
316
- const overridden = await fetchFeed(feedToken, {
317
- "If-None-Match": '"not-the-tag"',
318
- "If-Modified-Since": lastModified,
319
- });
320
-
321
- assert.equal(overridden.statusCode, 200);
322
- assert.match(overridden.body, /SUMMARY:Stand-up/);
323
- });
324
- it("answers the same plain 404 to an unknown and to a malformed token", async () => {
325
- const owner = new Subscriber();
326
- const calendarId = await owner.calendar();
327
- await owner.mintFeed(calendarId);
328
-
329
- const unknown = await fetchFeed("z".repeat(43));
330
- const malformed = await fetchFeed("not-a-token");
331
-
332
- assert.equal(unknown.statusCode, 404);
333
- assert.equal(malformed.statusCode, 404);
334
- assert.equal(
335
- headerOf(unknown, "content-type"),
336
- "text/plain; charset=utf-8",
337
- );
338
- assert.equal(unknown.body, malformed.body);
339
- assert.equal(
340
- unknown.body.includes("calendar"),
341
- true,
342
- "the body says nothing about which of the two it was",
343
- );
344
- });
345
- });
346
-
347
- describe("the feed a person manages", () => {
348
- it("hands out the token once and rotates the address on a second write", async () => {
349
- const owner = new Subscriber();
350
- const calendarId = await owner.calendar();
351
- await owner.event(calendarId, "Stand-up");
352
- const first = await owner.mintFeed(calendarId);
353
-
354
- const second = await owner.mintFeed(calendarId);
355
-
356
- assert.notEqual(second, first);
357
- assert.equal((await fetchFeed(first)).statusCode, 404);
358
- assert.equal((await fetchFeed(second)).statusCode, 200);
359
-
360
- const state = await request({
361
- method: "GET",
362
- path: `/calendars/${calendarId}/feed`,
363
- sub: owner.sub,
364
- });
365
- assert.equal(state.statusCode, 200);
366
- assert.equal(json(state).feedToken, undefined);
367
- assert.ok((json(state).rotatedAt as number) > 0);
368
- });
369
-
370
- it("stops the address resolving when it is revoked", async () => {
371
- const owner = new Subscriber();
372
- const calendarId = await owner.calendar();
373
- const feedToken = await owner.mintFeed(calendarId);
374
-
375
- const revoked = await request({
376
- method: "DELETE",
377
- path: `/calendars/${calendarId}/feed`,
378
- sub: owner.sub,
379
- });
380
-
381
- assert.equal(revoked.statusCode, 204);
382
- assert.equal((await fetchFeed(feedToken)).statusCode, 404);
383
- assert.equal(
384
- (
385
- await request({
386
- method: "DELETE",
387
- path: `/calendars/${calendarId}/feed`,
388
- sub: owner.sub,
389
- })
390
- ).statusCode,
391
- 404,
392
- "revoking a feed that is not there is not something to confirm",
393
- );
394
- });
395
-
396
- it("takes the address with the calendar it belonged to", async () => {
397
- const owner = new Subscriber();
398
- const calendarId = await owner.calendar();
399
- const feedToken = await owner.mintFeed(calendarId);
400
-
401
- const removed = await request({
402
- method: "DELETE",
403
- path: `/calendars/${calendarId}`,
404
- sub: owner.sub,
405
- });
406
-
407
- assert.equal(removed.statusCode, 204);
408
- assert.equal((await fetchFeed(feedToken)).statusCode, 404);
409
- });
410
-
411
- it("is absent for a calendar the caller does not own", async () => {
412
- const owner = new Subscriber();
413
- const stranger = new Subscriber();
414
- const calendarId = await owner.calendar();
415
- await owner.mintFeed(calendarId);
416
-
417
- for (const method of ["GET", "PUT", "DELETE"]) {
418
- const response = await request({
419
- method,
420
- path: `/calendars/${calendarId}/feed`,
421
- sub: stranger.sub,
422
- });
423
- assert.equal(response.statusCode, 404, `${method} ${response.body}`);
424
- }
425
- });
426
-
427
- it("refuses to mint or revoke without a session", async () => {
428
- const owner = new Subscriber();
429
- const calendarId = await owner.calendar();
430
-
431
- for (const method of ["GET", "PUT", "DELETE"]) {
432
- const response = await request({
433
- method,
434
- path: `/calendars/${calendarId}/feed`,
435
- });
436
- assert.equal(response.statusCode, 401, `${method} ${response.body}`);
437
- }
438
- });
439
- });