@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,497 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import {
3
- existsSync,
4
- mkdirSync,
5
- mkdtempSync,
6
- readFileSync,
7
- rmSync,
8
- writeFileSync,
9
- } from "node:fs";
10
- import { join } from "node:path";
11
- import { after, afterEach, before, beforeEach, describe, it } from "node:test";
12
- import { fileURLToPath } from "node:url";
13
- import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
14
- import type { Context } from "openapi-backend";
15
- import { normalizeRequest } from "../request.js";
16
- import { formatResponse } from "../response.js";
17
- import { SystemOperations } from "./system-update.js";
18
-
19
- const tmpRoot = join(
20
- fileURLToPath(new URL(".", import.meta.url)),
21
- "..",
22
- "..",
23
- ".tmp",
24
- "system-update",
25
- );
26
-
27
- const USER = "any-signed-in-user";
28
- const MANIFEST_URL = "https://updates.example.com/stable.json";
29
-
30
- let controlDir: string;
31
-
32
- const okState = {
33
- currentVersion: "v1.4.1",
34
- check: {
35
- status: "ok",
36
- lastCheckedAt: "2026-07-20T08:00:00Z",
37
- latestVersion: "v1.5.0",
38
- publishedAt: "2026-07-18T09:00:00Z",
39
- summary: "Faster search.",
40
- releaseNotesUrl: "https://example.com/notes",
41
- updateAvailable: true,
42
- },
43
- run: null,
44
- };
45
-
46
- const writeState = (state: unknown): void => {
47
- writeFileSync(join(controlDir, "state.json"), JSON.stringify(state));
48
- };
49
-
50
- before(() => {
51
- mkdirSync(tmpRoot, { recursive: true });
52
- });
53
-
54
- after(() => {
55
- rmSync(tmpRoot, { recursive: true, force: true });
56
- });
57
-
58
- beforeEach(() => {
59
- controlDir = mkdtempSync(join(tmpRoot, "control-"));
60
- process.env.REMIT_UPDATE_CONTROL_DIR = controlDir;
61
- process.env.REMIT_UPDATE_MANIFEST_URL = MANIFEST_URL;
62
- process.env.REMIT_TAG = "v1.4.1";
63
- });
64
-
65
- afterEach(() => {
66
- delete process.env.REMIT_UPDATE_MANIFEST_URL;
67
- delete process.env.REMIT_UPDATE_CONTROL_DIR;
68
- delete process.env.REMIT_TAG;
69
- });
70
-
71
- const buildEvent = (sub?: string): APIGatewayProxyEvent =>
72
- ({
73
- headers: {},
74
- requestContext: sub ? { authorizer: { claims: { sub } } } : {},
75
- }) as unknown as APIGatewayProxyEvent;
76
-
77
- // The query as openapi-backend hands it over: validated and coerced against the
78
- // spec, so `refresh` reaches the handler as a boolean or not at all.
79
- const getContext = (query: Record<string, unknown> = {}): Context =>
80
- ({ request: { query } }) as unknown as Context;
81
-
82
- const postContext = (targetVersion: string): Context =>
83
- ({ request: { requestBody: { targetVersion } } }) as unknown as Context;
84
-
85
- const getSystemUpdate =
86
- SystemOperations.SystemOperations_getSystemUpdate as unknown as (
87
- context: Context,
88
- event: APIGatewayProxyEvent,
89
- ) => Promise<unknown>;
90
-
91
- const applySystemUpdate =
92
- SystemOperations.SystemOperations_applySystemUpdate as unknown as (
93
- context: Context,
94
- event: APIGatewayProxyEvent,
95
- ) => Promise<unknown>;
96
-
97
- const getUpdate = (
98
- event: APIGatewayProxyEvent,
99
- query: Record<string, unknown> = {},
100
- ) => getSystemUpdate(getContext(query), event);
101
-
102
- const applyUpdate = (targetVersion: string, event: APIGatewayProxyEvent) =>
103
- applySystemUpdate(postContext(targetVersion), event);
104
-
105
- const hasStatus = (value: unknown, code: number): boolean =>
106
- typeof value === "object" &&
107
- value !== null &&
108
- (value as { statusCode?: number }).statusCode === code;
109
-
110
- describe("GET /system/update", () => {
111
- it("returns 404 when no manifest URL is configured", async () => {
112
- delete process.env.REMIT_UPDATE_MANIFEST_URL;
113
-
114
- const result = await getUpdate(buildEvent(USER));
115
-
116
- assert.ok(hasStatus(result, 404));
117
- });
118
-
119
- it("returns 401 when the caller is not authenticated", async () => {
120
- writeState(okState);
121
-
122
- const result = await getUpdate(buildEvent());
123
-
124
- assert.ok(hasStatus(result, 401));
125
- });
126
-
127
- it("returns 200 for any authenticated caller", async () => {
128
- writeState(okState);
129
-
130
- const result = await getUpdate(buildEvent("another-signed-in-user"));
131
-
132
- assert.deepEqual(result, okState);
133
- });
134
-
135
- it("returns 200 with the state file verbatim", async () => {
136
- writeState(okState);
137
-
138
- const result = await getUpdate(buildEvent(USER));
139
-
140
- assert.deepEqual(result, okState);
141
- });
142
-
143
- it("records a check request on the control volume when refresh is set (#599)", async () => {
144
- writeState(okState);
145
-
146
- const result = await getUpdate(buildEvent(USER), { refresh: true });
147
-
148
- // The press reaches the updater through the seam, and the answer is the
149
- // stored state — lastCheckedAt included, so the panel can say how old the
150
- // verdict it is still showing is.
151
- const file = readFileSync(join(controlDir, "check-request.json"), "utf8");
152
- assert.deepEqual(JSON.parse(file), {});
153
- assert.deepEqual(result, okState);
154
- });
155
-
156
- it("records a check request over an unknown version when no state exists yet", async () => {
157
- const result = await getUpdate(buildEvent(USER), { refresh: true });
158
-
159
- assert.equal(existsSync(join(controlDir, "check-request.json")), true);
160
- assert.deepEqual(result, {
161
- currentVersion: "unknown",
162
- check: { status: "disabled" },
163
- run: null,
164
- });
165
- });
166
-
167
- it("records nothing when the query carries no refresh", async () => {
168
- // `?refresh=1` is rejected by the spec before it reaches here, so the only
169
- // value that records a request is the boolean the validator produced.
170
- writeState(okState);
171
-
172
- await getUpdate(buildEvent(USER));
173
-
174
- assert.equal(existsSync(join(controlDir, "check-request.json")), false);
175
- });
176
-
177
- it("returns 401 and records nothing when a refresh is not authenticated", async () => {
178
- writeState(okState);
179
-
180
- const result = await getUpdate(buildEvent(), { refresh: true });
181
-
182
- assert.ok(hasStatus(result, 401));
183
- assert.equal(existsSync(join(controlDir, "check-request.json")), false);
184
- });
185
-
186
- it("reports an unknown version, not its own process env, when no state file exists", async () => {
187
- // The updater owns the running version and writes it into state.json; with
188
- // no state file the backend has nothing authoritative, so it says unknown
189
- // rather than surfacing REMIT_TAG, which drifts from the real running tag.
190
- const result = await getUpdate(buildEvent(USER));
191
-
192
- assert.deepEqual(result, {
193
- currentVersion: "unknown",
194
- check: { status: "disabled" },
195
- run: null,
196
- });
197
- });
198
-
199
- it("returns 200 with the run block untouched when the check has failed", async () => {
200
- const run = {
201
- runId: "run-1",
202
- fromVersion: "v1.3.0",
203
- targetVersion: "v1.4.1",
204
- phase: "committing",
205
- outcome: "succeeded",
206
- startedAt: "2026-07-19T08:00:00Z",
207
- updatedAt: "2026-07-19T08:05:00Z",
208
- message: "Update installed.",
209
- logCommand: "remit logs backend",
210
- };
211
- writeState({
212
- currentVersion: "v1.4.1",
213
- check: {
214
- status: "failed",
215
- lastCheckedAt: "2026-07-20T08:00:00Z",
216
- error: "The update manifest could not be fetched.",
217
- },
218
- run,
219
- });
220
-
221
- const result = (await getUpdate(buildEvent(USER))) as {
222
- check: { status: string };
223
- run: unknown;
224
- };
225
-
226
- assert.equal(result.check.status, "failed");
227
- assert.deepEqual(result.run, run);
228
- });
229
-
230
- it("passes both schema-version fields through when the state file carries them", async () => {
231
- writeState({
232
- currentVersion: "v1.4.1",
233
- currentSchemaVersion: 5,
234
- check: { ...okState.check, schemaVersion: 6 },
235
- run: null,
236
- });
237
-
238
- const result = (await getUpdate(buildEvent(USER))) as {
239
- currentSchemaVersion?: number;
240
- check: { schemaVersion?: number };
241
- };
242
-
243
- assert.equal(result.currentSchemaVersion, 5);
244
- assert.equal(result.check.schemaVersion, 6);
245
- });
246
-
247
- it("omits both schema-version fields when the state file predates them", async () => {
248
- writeState(okState);
249
-
250
- const result = (await getUpdate(buildEvent(USER))) as {
251
- currentSchemaVersion?: number;
252
- check: { schemaVersion?: number };
253
- };
254
-
255
- assert.ok(!("currentSchemaVersion" in result));
256
- assert.ok(!("schemaVersion" in result.check));
257
- });
258
-
259
- it("carries currentSchemaVersion alone when the check has no schema version", async () => {
260
- writeState({
261
- currentVersion: "v1.4.1",
262
- currentSchemaVersion: 5,
263
- check: okState.check,
264
- run: null,
265
- });
266
-
267
- const result = (await getUpdate(buildEvent(USER))) as {
268
- currentSchemaVersion?: number;
269
- check: { schemaVersion?: number };
270
- };
271
-
272
- assert.equal(result.currentSchemaVersion, 5);
273
- assert.ok(!("schemaVersion" in result.check));
274
- });
275
-
276
- it("carries check.schemaVersion alone when currentSchemaVersion is absent", async () => {
277
- writeState({
278
- currentVersion: "v1.4.1",
279
- check: { ...okState.check, schemaVersion: 6 },
280
- run: null,
281
- });
282
-
283
- const result = (await getUpdate(buildEvent(USER))) as {
284
- currentSchemaVersion?: number;
285
- check: { schemaVersion?: number };
286
- };
287
-
288
- assert.ok(!("currentSchemaVersion" in result));
289
- assert.equal(result.check.schemaVersion, 6);
290
- });
291
- });
292
-
293
- describe("POST /system/update", () => {
294
- it("returns 404 when no manifest URL is configured", async () => {
295
- delete process.env.REMIT_UPDATE_MANIFEST_URL;
296
-
297
- const result = await applyUpdate("v1.5.0", buildEvent(USER));
298
-
299
- assert.ok(hasStatus(result, 404));
300
- });
301
-
302
- it("returns 401 without writing request.json when the caller is not authenticated", async () => {
303
- writeState(okState);
304
-
305
- const result = await applyUpdate("v1.5.0", buildEvent());
306
-
307
- assert.ok(hasStatus(result, 401));
308
- assert.throws(() => readFileSync(join(controlDir, "request.json"), "utf8"));
309
- });
310
-
311
- it("returns 202 for any authenticated caller and records that identity", async () => {
312
- writeState(okState);
313
-
314
- const result = (await applyUpdate(
315
- "v1.5.0",
316
- buildEvent("another-signed-in-user"),
317
- )) as { statusCode: number };
318
-
319
- assert.equal(result.statusCode, 202);
320
-
321
- const request = JSON.parse(
322
- readFileSync(join(controlDir, "request.json"), "utf8"),
323
- );
324
- assert.equal(request.requestedBy, "another-signed-in-user");
325
- });
326
-
327
- it("returns 409 when a run is already in flight", async () => {
328
- writeState({
329
- ...okState,
330
- run: {
331
- runId: "run-9",
332
- fromVersion: "v1.4.1",
333
- targetVersion: "v1.5.0",
334
- phase: "verifying",
335
- outcome: null,
336
- startedAt: "2026-07-20T08:01:00Z",
337
- updatedAt: "2026-07-20T08:04:00Z",
338
- message: "Waiting for the new version to answer.",
339
- logCommand: "remit logs backend",
340
- },
341
- });
342
-
343
- await assert.rejects(applyUpdate("v1.5.0", buildEvent(USER)), {
344
- statusCode: 409,
345
- });
346
- });
347
-
348
- it("returns 422 when the target is not the checked version", async () => {
349
- writeState(okState);
350
-
351
- await assert.rejects(applyUpdate("v9.9.9", buildEvent(USER)), {
352
- statusCode: 422,
353
- });
354
- });
355
-
356
- it("returns 422 when no check has ever succeeded", async () => {
357
- await assert.rejects(applyUpdate("v1.5.0", buildEvent(USER)), {
358
- statusCode: 422,
359
- });
360
- });
361
-
362
- it("returns 202 with a run block carrying a fresh runId", async () => {
363
- writeState(okState);
364
-
365
- const result = (await applyUpdate("v1.5.0", buildEvent(USER))) as {
366
- statusCode: number;
367
- body: { run: { runId: string; targetVersion: string; outcome: null } };
368
- };
369
-
370
- assert.equal(result.statusCode, 202);
371
- assert.equal(result.body.run.targetVersion, "v1.5.0");
372
- assert.equal(result.body.run.outcome, null);
373
- assert.ok(result.body.run.runId.length > 0);
374
- });
375
-
376
- it("writes request.json with exactly the four seam fields and nothing else", async () => {
377
- writeState(okState);
378
-
379
- await applyUpdate("v1.5.0", buildEvent(USER));
380
-
381
- const request = JSON.parse(
382
- readFileSync(join(controlDir, "request.json"), "utf8"),
383
- );
384
-
385
- assert.deepEqual(Object.keys(request).sort(), [
386
- "requestedAt",
387
- "requestedBy",
388
- "runId",
389
- "targetVersion",
390
- ]);
391
- assert.equal(request.targetVersion, "v1.5.0");
392
- assert.equal(request.requestedBy, USER);
393
- for (const forbidden of ["registry", "image", "digest"]) {
394
- assert.ok(!(forbidden in request));
395
- }
396
- });
397
-
398
- it("stamps requestedAt with the moment of the request, in UTC", async () => {
399
- // The updater installs a request only while it is still current (#587), so
400
- // requestedAt is the field that decides it. A missing, fixed or non-UTC
401
- // stamp would make every request expire on arrival.
402
- writeState(okState);
403
- const before = Date.now();
404
-
405
- await applyUpdate("v1.5.0", buildEvent(USER));
406
-
407
- const after = Date.now();
408
- const request = JSON.parse(
409
- readFileSync(join(controlDir, "request.json"), "utf8"),
410
- ) as { requestedAt: string };
411
-
412
- assert.match(
413
- request.requestedAt,
414
- /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/,
415
- );
416
- const stamped = Date.parse(request.requestedAt);
417
- assert.ok(stamped >= before - 1000 && stamped <= after + 1000);
418
- });
419
- });
420
-
421
- describe("GET /system/update through the whole request pipeline", () => {
422
- it("records the check for ?refresh=true off the wire", async () => {
423
- // The rest of this file hands the handler a query the validator has already
424
- // coerced. Here the query arrives as API Gateway delivers it — every value a
425
- // string — and goes through the real built spec, which is where the press
426
- // was being answered with 400 instead of reaching the handler at all.
427
- writeState(okState);
428
- const { api } = await import("../index.js");
429
-
430
- const event = {
431
- httpMethod: "GET",
432
- path: "/system/update",
433
- queryStringParameters: { refresh: "true" },
434
- headers: {},
435
- requestContext: { authorizer: { claims: { sub: USER } } },
436
- } as unknown as APIGatewayProxyEvent;
437
-
438
- const result = (await api.handleRequest(
439
- normalizeRequest(event),
440
- event,
441
- {} as never,
442
- )) as APIGatewayProxyResult;
443
-
444
- assert.equal(result.statusCode, 200);
445
- assert.deepEqual(
446
- JSON.parse(readFileSync(join(controlDir, "check-request.json"), "utf8")),
447
- {},
448
- );
449
- assert.deepEqual(JSON.parse(result.body), okState);
450
- });
451
- });
452
-
453
- // A refusal here is the object `formatResponse` unwraps, never one that has
454
- // already been serialized: handing it a `body` string publishes the envelope
455
- // itself — `{"statusCode":…,"headers":…,"body":"{…}"}` — and what the caller
456
- // then parses is not an ApiError at all. Only the status was ever asserted, so
457
- // the shape went unguarded (issue #371).
458
- describe("the wire body of a self-update refusal", () => {
459
- it("answers an unconfigured seam with a flat not_found", async () => {
460
- delete process.env.REMIT_UPDATE_MANIFEST_URL;
461
-
462
- const result = await getUpdate(buildEvent(USER));
463
- const wire = formatResponse(result as Record<string, unknown>);
464
-
465
- assert.equal(wire.statusCode, 404);
466
- assert.deepEqual(JSON.parse(wire.body), {
467
- code: "not_found",
468
- message: "Not found",
469
- });
470
- });
471
-
472
- it("answers an unauthenticated caller with a flat unauthorized", async () => {
473
- writeState(okState);
474
-
475
- const result = await getUpdate(buildEvent());
476
- const wire = formatResponse(result as Record<string, unknown>);
477
-
478
- assert.equal(wire.statusCode, 401);
479
- assert.deepEqual(JSON.parse(wire.body), {
480
- code: "unauthorized",
481
- message: "Unauthorized",
482
- });
483
- });
484
-
485
- it("answers an unauthenticated install request the same way", async () => {
486
- writeState(okState);
487
-
488
- const result = await applyUpdate("v1.5.0", buildEvent());
489
- const wire = formatResponse(result as Record<string, unknown>);
490
-
491
- assert.equal(wire.statusCode, 401);
492
- assert.deepEqual(JSON.parse(wire.body), {
493
- code: "unauthorized",
494
- message: "Unauthorized",
495
- });
496
- });
497
- });