@remit/backend 0.0.142 → 0.0.144

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 (269) 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 +7 -0
  4. package/dist/src/api.js +144 -0
  5. package/dist/src/auth.d.ts +30 -0
  6. package/dist/src/auth.js +73 -0
  7. package/dist/src/config/msoauth.d.ts +30 -0
  8. package/dist/src/config/msoauth.js +30 -0
  9. package/{src/data-backend.ts → dist/src/data-backend.d.ts} +2 -4
  10. package/dist/src/data-backend.js +12 -0
  11. package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
  12. package/dist/src/derive/autoMoved.js +49 -0
  13. package/dist/src/derive/contentSignature.d.ts +41 -0
  14. package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +27 -72
  15. package/dist/src/derive/contentUrl.d.ts +46 -0
  16. package/dist/src/derive/contentUrl.js +46 -0
  17. package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
  18. package/dist/src/derive/deriveMuted.js +7 -0
  19. package/dist/src/derive/enrichThreadRows.d.ts +118 -0
  20. package/dist/src/derive/enrichThreadRows.js +151 -0
  21. package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
  22. package/dist/src/derive/filterThreadCriteria.js +24 -0
  23. package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
  24. package/dist/src/derive/pendingMoveCounts.js +53 -0
  25. package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
  26. package/dist/src/derive/senderTrust.js +19 -0
  27. package/dist/src/error.d.ts +21 -0
  28. package/dist/src/error.js +85 -0
  29. package/dist/src/handlers/account-guards.d.ts +36 -0
  30. package/dist/src/handlers/account-guards.js +107 -0
  31. package/dist/src/handlers/account-oauth.d.ts +14 -0
  32. package/dist/src/handlers/account-oauth.js +387 -0
  33. package/dist/src/handlers/account-overrides.d.ts +53 -0
  34. package/dist/src/handlers/account-overrides.js +233 -0
  35. package/dist/src/handlers/account-signature.d.ts +28 -0
  36. package/dist/src/handlers/account-signature.js +85 -0
  37. package/dist/src/handlers/account.d.ts +6 -0
  38. package/dist/src/handlers/account.js +350 -0
  39. package/dist/src/handlers/address.d.ts +35 -0
  40. package/dist/src/handlers/address.js +141 -0
  41. package/dist/src/handlers/calendar-event.d.ts +70 -0
  42. package/dist/src/handlers/calendar-event.js +425 -0
  43. package/dist/src/handlers/calendar-feed.d.ts +47 -0
  44. package/dist/src/handlers/calendar-feed.js +192 -0
  45. package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
  46. package/dist/src/handlers/calendar-suggestion.js +212 -0
  47. package/dist/src/handlers/calendar.d.ts +116 -0
  48. package/dist/src/handlers/calendar.js +330 -0
  49. package/dist/src/handlers/config.d.ts +43 -0
  50. package/dist/src/handlers/config.js +176 -0
  51. package/dist/src/handlers/ensure-account-config.d.ts +9 -0
  52. package/dist/src/handlers/ensure-account-config.js +27 -0
  53. package/dist/src/handlers/filter.d.ts +3 -0
  54. package/dist/src/handlers/filter.js +103 -0
  55. package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
  56. package/dist/src/handlers/folder-role-appointments.js +168 -0
  57. package/dist/src/handlers/folder-role-labels.d.ts +26 -0
  58. package/dist/src/handlers/folder-role-labels.js +40 -0
  59. package/dist/src/handlers/folder-role.d.ts +9 -0
  60. package/dist/src/handlers/folder-role.js +40 -0
  61. package/dist/src/handlers/index.d.ts +2 -0
  62. package/dist/src/handlers/index.js +60 -0
  63. package/dist/src/handlers/label.d.ts +43 -0
  64. package/dist/src/handlers/label.js +114 -0
  65. package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
  66. package/dist/src/handlers/mailbox-bindings.js +61 -0
  67. package/dist/src/handlers/mailbox.d.ts +23 -0
  68. package/dist/src/handlers/mailbox.js +219 -0
  69. package/dist/src/handlers/me.d.ts +2 -0
  70. package/dist/src/handlers/me.js +138 -0
  71. package/dist/src/handlers/message.d.ts +84 -0
  72. package/dist/src/handlers/message.js +607 -0
  73. package/dist/src/handlers/organize.d.ts +10 -0
  74. package/dist/src/handlers/organize.js +112 -0
  75. package/dist/src/handlers/outbox-attachment.d.ts +23 -0
  76. package/dist/src/handlers/outbox-attachment.js +81 -0
  77. package/dist/src/handlers/outbox.d.ts +4 -0
  78. package/dist/src/handlers/outbox.js +144 -0
  79. package/dist/src/handlers/sync-progress.d.ts +46 -0
  80. package/dist/src/handlers/sync-progress.js +63 -0
  81. package/dist/src/handlers/sync.d.ts +28 -0
  82. package/dist/src/handlers/sync.js +82 -0
  83. package/dist/src/handlers/system-update.d.ts +2 -0
  84. package/dist/src/handlers/system-update.js +172 -0
  85. package/dist/src/handlers/thread.d.ts +148 -0
  86. package/dist/src/handlers/thread.js +233 -0
  87. package/dist/src/handlers/unified-threads.d.ts +247 -0
  88. package/dist/src/handlers/unified-threads.js +409 -0
  89. package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
  90. package/dist/src/handlers/vip-suggestions.js +16 -0
  91. package/dist/src/index.d.ts +2 -0
  92. package/dist/src/index.js +37 -0
  93. package/{src/json.ts → dist/src/json.d.ts} +1 -4
  94. package/dist/src/json.js +10 -0
  95. package/dist/src/jwt-auth.d.ts +18 -0
  96. package/dist/src/jwt-auth.js +97 -0
  97. package/dist/src/request-context.d.ts +9 -0
  98. package/dist/src/request-context.js +22 -0
  99. package/dist/src/request.d.ts +3 -0
  100. package/dist/src/request.js +7 -0
  101. package/dist/src/response.d.ts +27 -0
  102. package/dist/src/response.js +87 -0
  103. package/dist/src/service/account-deletion.d.ts +22 -0
  104. package/dist/src/service/account-deletion.js +39 -0
  105. package/dist/src/service/compose-sqlite.d.ts +2 -0
  106. package/dist/src/service/compose-sqlite.js +11 -0
  107. package/dist/src/service/create-remit-client.d.ts +95 -0
  108. package/dist/src/service/create-remit-client.js +207 -0
  109. package/dist/src/service/data-client.d.ts +12 -0
  110. package/dist/src/service/data-client.js +44 -0
  111. package/dist/src/service/export-identity.d.ts +3 -0
  112. package/dist/src/service/export-identity.js +35 -0
  113. package/dist/src/service/fire-and-forget.d.ts +54 -0
  114. package/dist/src/service/fire-and-forget.js +38 -0
  115. package/dist/src/service/organize.d.ts +184 -0
  116. package/dist/src/service/organize.js +384 -0
  117. package/dist/src/service/refusal.d.ts +4 -0
  118. package/dist/src/service/refusal.js +20 -0
  119. package/dist/src/service/sender-category.d.ts +51 -0
  120. package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
  121. package/dist/src/service/sqs.d.ts +1 -0
  122. package/{src/service/sqs.ts → dist/src/service/sqs.js} +0 -1
  123. package/dist/src/service/trigger-sync.d.ts +51 -0
  124. package/dist/src/service/trigger-sync.js +39 -0
  125. package/dist/src/types.d.ts +49 -0
  126. package/dist/src/types.js +1 -0
  127. package/package.json +113 -98
  128. package/.env.test +0 -4
  129. package/dev-server/content-auth.test.ts +0 -113
  130. package/dev-server/content-auth.ts +0 -54
  131. package/dev-server/content-handler.test.ts +0 -105
  132. package/dev-server/content-handler.ts +0 -79
  133. package/dev-server/content-path.test.ts +0 -37
  134. package/dev-server/content-path.ts +0 -27
  135. package/dev-server/cors.test.ts +0 -48
  136. package/dev-server/cors.ts +0 -25
  137. package/dev-server/keep-alive.test.ts +0 -24
  138. package/dev-server/lambda-helpers.ts +0 -69
  139. package/dev-server/log-output.test.ts +0 -173
  140. package/dev-server/oauth-callback-route.test.ts +0 -66
  141. package/dev-server/relational-health.test.ts +0 -83
  142. package/dev-server/relational-health.ts +0 -39
  143. package/dev-server/server.ts +0 -384
  144. package/dev-server/sync-age.test.ts +0 -91
  145. package/dev-server/sync-age.ts +0 -55
  146. package/dev-server/upload-handler.ts +0 -132
  147. package/dev-server/upload-route.test.ts +0 -196
  148. package/scripts/backfill-classification.ts +0 -91
  149. package/scripts/backfill-list-id.ts +0 -83
  150. package/scripts/config-save.ts +0 -128
  151. package/src/auth.test.ts +0 -12
  152. package/src/auth.ts +0 -92
  153. package/src/config/msoauth.test.ts +0 -48
  154. package/src/config/msoauth.ts +0 -48
  155. package/src/data-backend.test.ts +0 -23
  156. package/src/derive/autoMoved.test.ts +0 -104
  157. package/src/derive/contentSignature.test.ts +0 -161
  158. package/src/derive/contentUrl.test.ts +0 -172
  159. package/src/derive/contentUrl.ts +0 -86
  160. package/src/derive/deriveMuted.test.ts +0 -34
  161. package/src/derive/enrichThreadRows.test.ts +0 -360
  162. package/src/derive/enrichThreadRows.ts +0 -234
  163. package/src/derive/filterThreadCriteria.test.ts +0 -126
  164. package/src/derive/filterThreadCriteria.ts +0 -51
  165. package/src/derive/senderTrust.test.ts +0 -67
  166. package/src/error.test.ts +0 -201
  167. package/src/error.ts +0 -123
  168. package/src/handlers/account-guards.test.ts +0 -67
  169. package/src/handlers/account-guards.ts +0 -169
  170. package/src/handlers/account-oauth-callback.test.ts +0 -155
  171. package/src/handlers/account-oauth-consent.test.ts +0 -336
  172. package/src/handlers/account-oauth-start.test.ts +0 -191
  173. package/src/handlers/account-oauth.test.ts +0 -439
  174. package/src/handlers/account-oauth.ts +0 -635
  175. package/src/handlers/account-overrides.test.ts +0 -88
  176. package/src/handlers/account-overrides.ts +0 -323
  177. package/src/handlers/account-ownership.ts +0 -25
  178. package/src/handlers/account-signature.ts +0 -117
  179. package/src/handlers/account-synced-services.test.ts +0 -245
  180. package/src/handlers/account.ts +0 -552
  181. package/src/handlers/address.test.ts +0 -381
  182. package/src/handlers/address.ts +0 -200
  183. package/src/handlers/calendar-event.test.ts +0 -685
  184. package/src/handlers/calendar-event.ts +0 -738
  185. package/src/handlers/calendar-feed-validators.test.ts +0 -228
  186. package/src/handlers/calendar-feed.test.ts +0 -439
  187. package/src/handlers/calendar-feed.ts +0 -291
  188. package/src/handlers/calendar-sqlite-fixture.ts +0 -36
  189. package/src/handlers/calendar-suggestion.test.ts +0 -1269
  190. package/src/handlers/calendar-suggestion.ts +0 -425
  191. package/src/handlers/calendar.test.ts +0 -1080
  192. package/src/handlers/calendar.ts +0 -517
  193. package/src/handlers/config.test.ts +0 -245
  194. package/src/handlers/config.ts +0 -332
  195. package/src/handlers/ensure-account-config.ts +0 -30
  196. package/src/handlers/filter.test.ts +0 -644
  197. package/src/handlers/filter.ts +0 -518
  198. package/src/handlers/folder-role-appointments.test.ts +0 -438
  199. package/src/handlers/folder-role-appointments.ts +0 -249
  200. package/src/handlers/folder-role-labels.ts +0 -63
  201. package/src/handlers/folder-role.test.ts +0 -209
  202. package/src/handlers/folder-role.ts +0 -80
  203. package/src/handlers/index.ts +0 -83
  204. package/src/handlers/label.test.ts +0 -112
  205. package/src/handlers/label.ts +0 -210
  206. package/src/handlers/mailbox-bindings.ts +0 -111
  207. package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
  208. package/src/handlers/mailbox-settled-target.test.ts +0 -251
  209. package/src/handlers/mailbox.test.ts +0 -280
  210. package/src/handlers/mailbox.ts +0 -581
  211. package/src/handlers/me.test.ts +0 -81
  212. package/src/handlers/me.ts +0 -203
  213. package/src/handlers/message-read-backfill.test.ts +0 -103
  214. package/src/handlers/message.test.ts +0 -131
  215. package/src/handlers/message.ts +0 -1131
  216. package/src/handlers/organize.test.ts +0 -277
  217. package/src/handlers/organize.ts +0 -197
  218. package/src/handlers/outbox-attachment.test.ts +0 -765
  219. package/src/handlers/outbox-attachment.ts +0 -126
  220. package/src/handlers/outbox-list.test.ts +0 -179
  221. package/src/handlers/outbox.test.ts +0 -772
  222. package/src/handlers/outbox.ts +0 -258
  223. package/src/handlers/search.test.ts +0 -161
  224. package/src/handlers/search.ts +0 -105
  225. package/src/handlers/sync-progress.test.ts +0 -205
  226. package/src/handlers/sync-progress.ts +0 -99
  227. package/src/handlers/sync.test.ts +0 -168
  228. package/src/handlers/sync.ts +0 -118
  229. package/src/handlers/system-update.test.ts +0 -497
  230. package/src/handlers/system-update.ts +0 -230
  231. package/src/handlers/thread.test.ts +0 -396
  232. package/src/handlers/thread.ts +0 -427
  233. package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
  234. package/src/handlers/unified-threads.test.ts +0 -785
  235. package/src/handlers/unified-threads.ts +0 -669
  236. package/src/index.test.ts +0 -83
  237. package/src/index.ts +0 -228
  238. package/src/jwt-auth.test.ts +0 -152
  239. package/src/jwt-auth.ts +0 -132
  240. package/src/log-redaction.test.ts +0 -144
  241. package/src/openapi-spec.test.ts +0 -118
  242. package/src/request-context.test.ts +0 -61
  243. package/src/request-context.ts +0 -33
  244. package/src/request-pipeline.test.ts +0 -88
  245. package/src/request.ts +0 -10
  246. package/src/response.test.ts +0 -130
  247. package/src/response.ts +0 -144
  248. package/src/service/compose-sqlite.ts +0 -98
  249. package/src/service/config-import.ts +0 -39
  250. package/src/service/create-remit-client.ts +0 -521
  251. package/src/service/data-client.test.ts +0 -97
  252. package/src/service/data-client.ts +0 -58
  253. package/src/service/export-identity.ts +0 -39
  254. package/src/service/filter.ts +0 -40
  255. package/src/service/fire-and-forget.test.ts +0 -126
  256. package/src/service/fire-and-forget.ts +0 -79
  257. package/src/service/localhost-env-config.test.ts +0 -36
  258. package/src/service/organize-queue.ts +0 -98
  259. package/src/service/organize.test.ts +0 -1451
  260. package/src/service/organize.ts +0 -1025
  261. package/src/service/read-path-filter-config.test.ts +0 -243
  262. package/src/service/semantic-capability.test.ts +0 -78
  263. package/src/service/semantic-capability.ts +0 -95
  264. package/src/service/sender-category.test.ts +0 -235
  265. package/src/service/trigger-sync.test.ts +0 -205
  266. package/src/service/trigger-sync.ts +0 -100
  267. package/src/stdout-capture-fixture.ts +0 -40
  268. package/src/types.ts +0 -271
  269. 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
- });