@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.
- package/dist/dev-server/content-handler.d.ts +38 -0
- package/dist/dev-server/content-handler.js +46 -0
- package/dist/src/api.d.ts +5 -0
- package/dist/src/api.js +73 -0
- package/dist/src/auth.d.ts +24 -0
- package/dist/src/auth.js +81 -0
- package/dist/src/config/api-config.d.ts +20 -0
- package/dist/src/config/api-config.js +1 -0
- package/dist/src/config/from-env.d.ts +2 -0
- package/dist/src/config/from-env.js +65 -0
- package/dist/src/config/msoauth-secrets-manager.d.ts +2 -0
- package/dist/src/config/msoauth-secrets-manager.js +28 -0
- package/dist/src/config/msoauth.d.ts +19 -0
- package/dist/src/config/msoauth.js +6 -0
- package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
- package/dist/src/derive/autoMoved.js +49 -0
- package/dist/src/derive/contentSignature.d.ts +40 -0
- package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +23 -77
- package/dist/src/derive/contentUrl.d.ts +33 -0
- package/dist/src/derive/contentUrl.js +27 -0
- package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
- package/dist/src/derive/deriveMuted.js +7 -0
- package/dist/src/derive/enrichThreadRows.d.ts +118 -0
- package/dist/src/derive/enrichThreadRows.js +151 -0
- package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
- package/dist/src/derive/filterThreadCriteria.js +24 -0
- package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
- package/dist/src/derive/pendingMoveCounts.js +53 -0
- package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
- package/dist/src/derive/senderTrust.js +19 -0
- package/dist/src/error.d.ts +21 -0
- package/dist/src/error.js +85 -0
- package/dist/src/handlers/account-guards.d.ts +36 -0
- package/dist/src/handlers/account-guards.js +107 -0
- package/dist/src/handlers/account-oauth.d.ts +15 -0
- package/dist/src/handlers/account-oauth.js +334 -0
- package/dist/src/handlers/account-overrides.d.ts +53 -0
- package/dist/src/handlers/account-overrides.js +233 -0
- package/dist/src/handlers/account-signature.d.ts +28 -0
- package/dist/src/handlers/account-signature.js +85 -0
- package/dist/src/handlers/account.d.ts +7 -0
- package/dist/src/handlers/account.js +337 -0
- package/dist/src/handlers/address.d.ts +35 -0
- package/dist/src/handlers/address.js +133 -0
- package/dist/src/handlers/calendar-event.d.ts +70 -0
- package/dist/src/handlers/calendar-event.js +417 -0
- package/dist/src/handlers/calendar-feed.d.ts +47 -0
- package/dist/src/handlers/calendar-feed.js +184 -0
- package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
- package/dist/src/handlers/calendar-suggestion.js +204 -0
- package/dist/src/handlers/calendar.d.ts +116 -0
- package/dist/src/handlers/calendar.js +323 -0
- package/dist/src/handlers/config.d.ts +43 -0
- package/dist/src/handlers/config.js +164 -0
- package/dist/src/handlers/ensure-account-config.d.ts +9 -0
- package/dist/src/handlers/ensure-account-config.js +27 -0
- package/dist/src/handlers/filter.d.ts +3 -0
- package/dist/src/handlers/filter.js +88 -0
- package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
- package/dist/src/handlers/folder-role-appointments.js +168 -0
- package/dist/src/handlers/folder-role-labels.d.ts +26 -0
- package/dist/src/handlers/folder-role-labels.js +40 -0
- package/dist/src/handlers/folder-role.d.ts +9 -0
- package/dist/src/handlers/folder-role.js +37 -0
- package/dist/src/handlers/index.d.ts +88 -0
- package/dist/src/handlers/index.js +62 -0
- package/dist/src/handlers/label.d.ts +43 -0
- package/dist/src/handlers/label.js +107 -0
- package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
- package/dist/src/handlers/mailbox-bindings.js +61 -0
- package/dist/src/handlers/mailbox.d.ts +23 -0
- package/dist/src/handlers/mailbox.js +211 -0
- package/dist/src/handlers/me.d.ts +2 -0
- package/dist/src/handlers/me.js +117 -0
- package/dist/src/handlers/message.d.ts +82 -0
- package/dist/src/handlers/message.js +595 -0
- package/dist/src/handlers/organize.d.ts +10 -0
- package/dist/src/handlers/organize.js +103 -0
- package/dist/src/handlers/outbox-attachment.d.ts +25 -0
- package/dist/src/handlers/outbox-attachment.js +77 -0
- package/dist/src/handlers/outbox.d.ts +4 -0
- package/dist/src/handlers/outbox.js +136 -0
- package/dist/src/handlers/sync-progress.d.ts +46 -0
- package/dist/src/handlers/sync-progress.js +63 -0
- package/dist/src/handlers/sync.d.ts +28 -0
- package/dist/src/handlers/sync.js +73 -0
- package/dist/src/handlers/system-update.d.ts +2 -0
- package/dist/src/handlers/system-update.js +159 -0
- package/dist/src/handlers/thread.d.ts +148 -0
- package/dist/src/handlers/thread.js +228 -0
- package/dist/src/handlers/unified-threads.d.ts +247 -0
- package/dist/src/handlers/unified-threads.js +406 -0
- package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
- package/dist/src/handlers/vip-suggestions.js +16 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +53 -0
- package/{src/json.ts → dist/src/json.d.ts} +1 -4
- package/dist/src/json.js +10 -0
- package/dist/src/jwt-auth.d.ts +15 -0
- package/dist/src/jwt-auth.js +45 -0
- package/dist/src/lambda.d.ts +13 -0
- package/dist/src/lambda.js +33 -0
- package/dist/src/request.d.ts +9 -0
- package/dist/src/request.js +17 -0
- package/dist/src/response.d.ts +44 -0
- package/dist/src/response.js +80 -0
- package/dist/src/serve.d.ts +31 -0
- package/dist/src/serve.js +117 -0
- package/dist/src/service/account-deletion.d.ts +20 -0
- package/dist/src/service/account-deletion.js +33 -0
- package/dist/src/service/compose-sqlite.d.ts +3 -0
- package/dist/src/service/compose-sqlite.js +13 -0
- package/dist/src/service/create-remit-client.d.ts +109 -0
- package/dist/src/service/create-remit-client.js +188 -0
- package/dist/src/service/data-client.d.ts +12 -0
- package/dist/src/service/data-client.js +42 -0
- package/dist/src/service/export-identity.d.ts +4 -0
- package/dist/src/service/export-identity.js +33 -0
- package/dist/src/service/fire-and-forget.d.ts +54 -0
- package/dist/src/service/fire-and-forget.js +37 -0
- package/dist/src/service/organize.d.ts +181 -0
- package/dist/src/service/organize.js +367 -0
- package/dist/src/service/queues.d.ts +12 -0
- package/dist/src/service/queues.js +1 -0
- package/dist/src/service/refusal.d.ts +4 -0
- package/dist/src/service/refusal.js +20 -0
- package/dist/src/service/sender-category.d.ts +51 -0
- package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
- package/dist/src/service/shared-deps.d.ts +6 -0
- package/dist/src/service/shared-deps.js +24 -0
- package/dist/src/service/trigger-sync.d.ts +57 -0
- package/dist/src/service/trigger-sync.js +31 -0
- package/dist/src/types.d.ts +72 -0
- package/dist/src/types.js +1 -0
- package/package.json +125 -98
- package/.env.test +0 -4
- package/dev-server/content-auth.test.ts +0 -113
- package/dev-server/content-auth.ts +0 -54
- package/dev-server/content-handler.test.ts +0 -105
- package/dev-server/content-handler.ts +0 -79
- package/dev-server/content-path.test.ts +0 -37
- package/dev-server/content-path.ts +0 -27
- package/dev-server/cors.test.ts +0 -48
- package/dev-server/cors.ts +0 -25
- package/dev-server/keep-alive.test.ts +0 -24
- package/dev-server/lambda-helpers.ts +0 -69
- package/dev-server/log-output.test.ts +0 -173
- package/dev-server/oauth-callback-route.test.ts +0 -66
- package/dev-server/relational-health.test.ts +0 -83
- package/dev-server/relational-health.ts +0 -39
- package/dev-server/server.ts +0 -384
- package/dev-server/sync-age.test.ts +0 -91
- package/dev-server/sync-age.ts +0 -55
- package/dev-server/upload-handler.ts +0 -132
- package/dev-server/upload-route.test.ts +0 -196
- package/scripts/backfill-classification.ts +0 -91
- package/scripts/backfill-list-id.ts +0 -83
- package/scripts/config-save.ts +0 -128
- package/src/auth.test.ts +0 -12
- package/src/auth.ts +0 -92
- package/src/config/msoauth.test.ts +0 -48
- package/src/config/msoauth.ts +0 -48
- package/src/data-backend.test.ts +0 -23
- package/src/data-backend.ts +0 -14
- package/src/derive/autoMoved.test.ts +0 -104
- package/src/derive/contentSignature.test.ts +0 -161
- package/src/derive/contentUrl.test.ts +0 -172
- package/src/derive/contentUrl.ts +0 -86
- package/src/derive/deriveMuted.test.ts +0 -34
- package/src/derive/enrichThreadRows.test.ts +0 -360
- package/src/derive/enrichThreadRows.ts +0 -234
- package/src/derive/filterThreadCriteria.test.ts +0 -126
- package/src/derive/filterThreadCriteria.ts +0 -51
- package/src/derive/senderTrust.test.ts +0 -67
- package/src/error.test.ts +0 -201
- package/src/error.ts +0 -123
- package/src/handlers/account-guards.test.ts +0 -67
- package/src/handlers/account-guards.ts +0 -169
- package/src/handlers/account-oauth-callback.test.ts +0 -155
- package/src/handlers/account-oauth-consent.test.ts +0 -336
- package/src/handlers/account-oauth-start.test.ts +0 -191
- package/src/handlers/account-oauth.test.ts +0 -439
- package/src/handlers/account-oauth.ts +0 -635
- package/src/handlers/account-overrides.test.ts +0 -88
- package/src/handlers/account-overrides.ts +0 -323
- package/src/handlers/account-ownership.ts +0 -25
- package/src/handlers/account-signature.ts +0 -117
- package/src/handlers/account-synced-services.test.ts +0 -245
- package/src/handlers/account.ts +0 -552
- package/src/handlers/address.test.ts +0 -381
- package/src/handlers/address.ts +0 -200
- package/src/handlers/calendar-event.test.ts +0 -685
- package/src/handlers/calendar-event.ts +0 -738
- package/src/handlers/calendar-feed-validators.test.ts +0 -228
- package/src/handlers/calendar-feed.test.ts +0 -439
- package/src/handlers/calendar-feed.ts +0 -291
- package/src/handlers/calendar-sqlite-fixture.ts +0 -36
- package/src/handlers/calendar-suggestion.test.ts +0 -1269
- package/src/handlers/calendar-suggestion.ts +0 -425
- package/src/handlers/calendar.test.ts +0 -1080
- package/src/handlers/calendar.ts +0 -517
- package/src/handlers/config.test.ts +0 -245
- package/src/handlers/config.ts +0 -332
- package/src/handlers/ensure-account-config.ts +0 -30
- package/src/handlers/filter.test.ts +0 -644
- package/src/handlers/filter.ts +0 -518
- package/src/handlers/folder-role-appointments.test.ts +0 -438
- package/src/handlers/folder-role-appointments.ts +0 -249
- package/src/handlers/folder-role-labels.ts +0 -63
- package/src/handlers/folder-role.test.ts +0 -209
- package/src/handlers/folder-role.ts +0 -80
- package/src/handlers/index.ts +0 -83
- package/src/handlers/label.test.ts +0 -112
- package/src/handlers/label.ts +0 -210
- package/src/handlers/mailbox-bindings.ts +0 -111
- package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
- package/src/handlers/mailbox-settled-target.test.ts +0 -251
- package/src/handlers/mailbox.test.ts +0 -280
- package/src/handlers/mailbox.ts +0 -581
- package/src/handlers/me.test.ts +0 -81
- package/src/handlers/me.ts +0 -203
- package/src/handlers/message-read-backfill.test.ts +0 -103
- package/src/handlers/message.test.ts +0 -131
- package/src/handlers/message.ts +0 -1131
- package/src/handlers/organize.test.ts +0 -277
- package/src/handlers/organize.ts +0 -197
- package/src/handlers/outbox-attachment.test.ts +0 -765
- package/src/handlers/outbox-attachment.ts +0 -126
- package/src/handlers/outbox-list.test.ts +0 -179
- package/src/handlers/outbox.test.ts +0 -772
- package/src/handlers/outbox.ts +0 -258
- package/src/handlers/search.test.ts +0 -161
- package/src/handlers/search.ts +0 -105
- package/src/handlers/sync-progress.test.ts +0 -205
- package/src/handlers/sync-progress.ts +0 -99
- package/src/handlers/sync.test.ts +0 -168
- package/src/handlers/sync.ts +0 -118
- package/src/handlers/system-update.test.ts +0 -497
- package/src/handlers/system-update.ts +0 -230
- package/src/handlers/thread.test.ts +0 -396
- package/src/handlers/thread.ts +0 -427
- package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
- package/src/handlers/unified-threads.test.ts +0 -785
- package/src/handlers/unified-threads.ts +0 -669
- package/src/index.test.ts +0 -83
- package/src/index.ts +0 -228
- package/src/jwt-auth.test.ts +0 -152
- package/src/jwt-auth.ts +0 -132
- package/src/log-redaction.test.ts +0 -144
- package/src/openapi-spec.test.ts +0 -118
- package/src/request-context.test.ts +0 -61
- package/src/request-context.ts +0 -33
- package/src/request-pipeline.test.ts +0 -88
- package/src/request.ts +0 -10
- package/src/response.test.ts +0 -130
- package/src/response.ts +0 -144
- package/src/service/compose-sqlite.ts +0 -98
- package/src/service/config-import.ts +0 -39
- package/src/service/create-remit-client.ts +0 -522
- package/src/service/data-client.test.ts +0 -97
- package/src/service/data-client.ts +0 -58
- package/src/service/export-identity.ts +0 -39
- package/src/service/filter.ts +0 -40
- package/src/service/fire-and-forget.test.ts +0 -126
- package/src/service/fire-and-forget.ts +0 -79
- package/src/service/localhost-env-config.test.ts +0 -36
- package/src/service/organize-queue.ts +0 -98
- package/src/service/organize.test.ts +0 -1451
- package/src/service/organize.ts +0 -1025
- package/src/service/read-path-filter-config.test.ts +0 -243
- package/src/service/semantic-capability.test.ts +0 -78
- package/src/service/semantic-capability.ts +0 -95
- package/src/service/sender-category.test.ts +0 -235
- package/src/service/sqs.ts +0 -4
- package/src/service/trigger-sync.test.ts +0 -205
- package/src/service/trigger-sync.ts +0 -100
- package/src/stdout-capture-fixture.ts +0 -40
- package/src/types.ts +0 -271
- package/tsconfig.json +0 -7
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import type { Readable } from "node:stream";
|
|
2
|
-
import {
|
|
3
|
-
authorizeUploadRequest,
|
|
4
|
-
parseOutboxAttachmentKey,
|
|
5
|
-
type StorageService,
|
|
6
|
-
} from "@remit/storage-service";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The self-hosted receiver for an attachment upload.
|
|
10
|
-
*
|
|
11
|
-
* A hosted deployment mints a presigned PUT and none of this runs: the browser
|
|
12
|
-
* writes to block storage and the size is bound by the storage service itself.
|
|
13
|
-
* Where there is no storage service to presign against, this route is the
|
|
14
|
-
* equivalent, and it has to bind the same things — one attachment, one size,
|
|
15
|
-
* for a while — on its own.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
export type CollectedBody =
|
|
19
|
-
| { readonly outcome: "Collected"; readonly content: Buffer }
|
|
20
|
-
| { readonly outcome: "TooLarge" };
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Read a request body, refusing it the moment it passes what was reserved.
|
|
24
|
-
*
|
|
25
|
-
* The count is checked per chunk and the stream destroyed on the one that
|
|
26
|
-
* crosses the line, so a client that reserved a megabyte and started sending a
|
|
27
|
-
* hundred is cut off after the first megabyte rather than after all of it.
|
|
28
|
-
*/
|
|
29
|
-
export const collectLimitedBody = async (
|
|
30
|
-
stream: Readable,
|
|
31
|
-
maxBytes: number,
|
|
32
|
-
): Promise<CollectedBody> => {
|
|
33
|
-
const chunks: Buffer[] = [];
|
|
34
|
-
let received = 0;
|
|
35
|
-
|
|
36
|
-
for await (const chunk of stream) {
|
|
37
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
38
|
-
received += buffer.length;
|
|
39
|
-
if (received > maxBytes) {
|
|
40
|
-
// Stop reading, but leave the socket alone: destroying it here races the
|
|
41
|
-
// 413 the caller is about to write, and the client sees a connection
|
|
42
|
-
// reset instead of the reason it was refused.
|
|
43
|
-
stream.pause();
|
|
44
|
-
return { outcome: "TooLarge" };
|
|
45
|
-
}
|
|
46
|
-
chunks.push(buffer);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return { outcome: "Collected", content: Buffer.concat(chunks) };
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export interface ReceiveUploadInput {
|
|
53
|
-
/** Decoded storage key, the `/outbox-upload/` prefix already stripped. */
|
|
54
|
-
storageKey: string;
|
|
55
|
-
exp: string | undefined;
|
|
56
|
-
max: string | undefined;
|
|
57
|
-
sig: string | undefined;
|
|
58
|
-
body: Readable;
|
|
59
|
-
nowSeconds: number;
|
|
60
|
-
secret: string | undefined;
|
|
61
|
-
/**
|
|
62
|
-
* Whether the database still holds a live reservation for this attachment.
|
|
63
|
-
* Injected rather than reached for, so the receiver stays a function of its
|
|
64
|
-
* request and the storage it writes to.
|
|
65
|
-
*/
|
|
66
|
-
findLiveReservation: (
|
|
67
|
-
accountConfigId: string,
|
|
68
|
-
outboxAttachmentId: string,
|
|
69
|
-
nowSeconds: number,
|
|
70
|
-
) => Promise<boolean>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface UploadResult {
|
|
74
|
-
status: number;
|
|
75
|
-
reason: string;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export const receiveUpload = async (
|
|
79
|
-
storage: StorageService,
|
|
80
|
-
input: ReceiveUploadInput,
|
|
81
|
-
): Promise<UploadResult> => {
|
|
82
|
-
const target = parseOutboxAttachmentKey(input.storageKey);
|
|
83
|
-
if (!target) return { status: 404, reason: "not-an-attachment-key" };
|
|
84
|
-
|
|
85
|
-
const auth = authorizeUploadRequest({
|
|
86
|
-
secret: input.secret,
|
|
87
|
-
relativePath: input.storageKey,
|
|
88
|
-
exp: input.exp,
|
|
89
|
-
max: input.max,
|
|
90
|
-
sig: input.sig,
|
|
91
|
-
nowSeconds: input.nowSeconds,
|
|
92
|
-
});
|
|
93
|
-
if (!auth.authorized) {
|
|
94
|
-
return { status: auth.status, reason: auth.reason };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// A signed URL outlives the draft it was minted for, so possession of one is
|
|
98
|
-
// not enough: the database has to still hold a live reservation for this
|
|
99
|
-
// attachment. Without this a URL minted before a discard writes bytes back
|
|
100
|
-
// under a draft that no longer exists. The hosted shape cannot make this
|
|
101
|
-
// check — block storage receives the PUT directly — which is what the sweep
|
|
102
|
-
// exists for.
|
|
103
|
-
const reserved = await input.findLiveReservation(
|
|
104
|
-
target.accountConfigId,
|
|
105
|
-
target.outboxAttachmentId,
|
|
106
|
-
input.nowSeconds,
|
|
107
|
-
);
|
|
108
|
-
if (!reserved) return { status: 409, reason: "no-live-reservation" };
|
|
109
|
-
|
|
110
|
-
const collected = await collectLimitedBody(input.body, auth.maxBytes);
|
|
111
|
-
if (collected.outcome === "TooLarge") {
|
|
112
|
-
return { status: 413, reason: "over-reserved-size" };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Exactly the reserved size, not merely under it — the same thing S3 binds
|
|
116
|
-
// when it recomputes a presigned PUT's signature over the content-length it
|
|
117
|
-
// was sent. A short body is a failed upload, and the completion would refuse
|
|
118
|
-
// it anyway; refusing here means no partial object is left to refuse.
|
|
119
|
-
if (collected.content.length !== auth.maxBytes) {
|
|
120
|
-
return { status: 400, reason: "size-mismatch" };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
await storage.storeOutboxAttachment({
|
|
124
|
-
accountConfigId: target.accountConfigId,
|
|
125
|
-
accountId: target.accountId,
|
|
126
|
-
outboxMessageId: target.outboxMessageId,
|
|
127
|
-
outboxAttachmentId: target.outboxAttachmentId,
|
|
128
|
-
content: collected.content,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
return { status: 204, reason: "stored" };
|
|
132
|
-
};
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Issue #679: the upload route through the real dev-server app.
|
|
3
|
-
*
|
|
4
|
-
* Not a copy of its middleware order — the app object itself, imported from
|
|
5
|
-
* `server.ts`. A copy is what let a JSON-typed upload silently read zero bytes:
|
|
6
|
-
* `express.json` drains the stream before any route below it sees the body, and
|
|
7
|
-
* a test that rebuilds the stack stays green when the real one is reordered.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
12
|
-
import type { Server } from "node:http";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import { after, before, describe, it } from "node:test";
|
|
15
|
-
import type {
|
|
16
|
-
IOutboxAttachmentRepository,
|
|
17
|
-
OutboxAttachmentItem,
|
|
18
|
-
} from "@remit/data-ports";
|
|
19
|
-
import { NotFoundError } from "@remit/data-ports/errors";
|
|
20
|
-
import { OutboxAttachmentService } from "@remit/mailbox-service";
|
|
21
|
-
import {
|
|
22
|
-
type StorageService,
|
|
23
|
-
UPLOAD_ROUTE_PREFIX,
|
|
24
|
-
} from "@remit/storage-service";
|
|
25
|
-
import { createFilesystemStorageService } from "@remit/storage-service/filesystem";
|
|
26
|
-
import {
|
|
27
|
-
_resetForTest,
|
|
28
|
-
type RemitClient,
|
|
29
|
-
setClient,
|
|
30
|
-
} from "../src/service/data-client.js";
|
|
31
|
-
|
|
32
|
-
const SECRET = "a-signing-secret-of-at-least-32-characters";
|
|
33
|
-
const CFG = "cfg-upload";
|
|
34
|
-
const ACC = "acc-upload";
|
|
35
|
-
const DRAFT = "draft-upload";
|
|
36
|
-
|
|
37
|
-
let storage: StorageService;
|
|
38
|
-
let basePath: string;
|
|
39
|
-
let rows: Map<string, OutboxAttachmentItem>;
|
|
40
|
-
let server: Server;
|
|
41
|
-
let port: number;
|
|
42
|
-
|
|
43
|
-
const repository = (): IOutboxAttachmentRepository =>
|
|
44
|
-
({
|
|
45
|
-
get: async (accountConfigId: string, outboxAttachmentId: string) => {
|
|
46
|
-
const row = rows.get(outboxAttachmentId);
|
|
47
|
-
if (!row || row.accountConfigId !== accountConfigId) {
|
|
48
|
-
throw new NotFoundError("gone");
|
|
49
|
-
}
|
|
50
|
-
return row;
|
|
51
|
-
},
|
|
52
|
-
listByOutboxMessage: async () => [...rows.values()],
|
|
53
|
-
}) as unknown as IOutboxAttachmentRepository;
|
|
54
|
-
|
|
55
|
-
const reserve = async (
|
|
56
|
-
outboxAttachmentId: string,
|
|
57
|
-
sizeBytes: number,
|
|
58
|
-
): Promise<string> => {
|
|
59
|
-
const expiresAt = Math.floor(Date.now() / 1000) + 900;
|
|
60
|
-
rows.set(outboxAttachmentId, {
|
|
61
|
-
outboxAttachmentId,
|
|
62
|
-
outboxMessageId: DRAFT,
|
|
63
|
-
accountId: ACC,
|
|
64
|
-
accountConfigId: CFG,
|
|
65
|
-
filename: `${outboxAttachmentId}.bin`,
|
|
66
|
-
contentType: "application/octet-stream",
|
|
67
|
-
sizeBytes,
|
|
68
|
-
state: "Pending",
|
|
69
|
-
storageKey: "",
|
|
70
|
-
reservationExpiresAt: expiresAt,
|
|
71
|
-
createdAt: 0,
|
|
72
|
-
updatedAt: 0,
|
|
73
|
-
});
|
|
74
|
-
const { uploadUrl } = await storage.createOutboxAttachmentUploadUrl({
|
|
75
|
-
accountConfigId: CFG,
|
|
76
|
-
accountId: ACC,
|
|
77
|
-
outboxMessageId: DRAFT,
|
|
78
|
-
outboxAttachmentId,
|
|
79
|
-
sizeBytes,
|
|
80
|
-
expiresAt,
|
|
81
|
-
});
|
|
82
|
-
const url = new URL(uploadUrl);
|
|
83
|
-
return `http://127.0.0.1:${port}${url.pathname}${url.search}`;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
before(async () => {
|
|
87
|
-
// Repo-local scratch, not the machine's shared temp directory.
|
|
88
|
-
basePath = await mkdtemp(join(process.cwd(), ".tmp-upload-route-"));
|
|
89
|
-
storage = createFilesystemStorageService(basePath, {
|
|
90
|
-
origin: "http://127.0.0.1",
|
|
91
|
-
signingSecret: SECRET,
|
|
92
|
-
});
|
|
93
|
-
rows = new Map();
|
|
94
|
-
|
|
95
|
-
setClient({
|
|
96
|
-
storage,
|
|
97
|
-
outboxAttachment: new OutboxAttachmentService({
|
|
98
|
-
outboxMessageService: {} as never,
|
|
99
|
-
outboxAttachmentService: repository(),
|
|
100
|
-
storage,
|
|
101
|
-
}),
|
|
102
|
-
} as unknown as RemitClient);
|
|
103
|
-
|
|
104
|
-
process.env.BETTER_AUTH_SECRET = SECRET;
|
|
105
|
-
// Port 0: the app binds whatever is free and tells us which.
|
|
106
|
-
process.env.SERVER_PORT = "0";
|
|
107
|
-
process.env.CORS_ALLOWED_ORIGINS = "*";
|
|
108
|
-
|
|
109
|
-
const imported = await import("./server.js");
|
|
110
|
-
server = imported.listener;
|
|
111
|
-
if (!server.listening) {
|
|
112
|
-
await new Promise<void>((resolve) => server.once("listening", resolve));
|
|
113
|
-
}
|
|
114
|
-
const address = server.address();
|
|
115
|
-
port = typeof address === "object" && address ? address.port : 0;
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
after(async () => {
|
|
119
|
-
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
120
|
-
await rm(basePath, { recursive: true, force: true });
|
|
121
|
-
_resetForTest();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe("PUT /outbox-upload through the dev-server's own app", () => {
|
|
125
|
-
it("stores every byte of a file the browser labelled application/json", async () => {
|
|
126
|
-
const content = Buffer.from(JSON.stringify({ note: "x".repeat(100) }));
|
|
127
|
-
const url = await reserve("attjson", content.length);
|
|
128
|
-
|
|
129
|
-
const response = await fetch(url, {
|
|
130
|
-
method: "PUT",
|
|
131
|
-
body: content,
|
|
132
|
-
// What `fetch(url, { body: file })` sends for a .json file: File.type.
|
|
133
|
-
headers: { "content-type": "application/json" },
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
assert.equal(response.status, 204);
|
|
137
|
-
assert.equal(
|
|
138
|
-
(await storage.statOutboxAttachment(CFG, ACC, DRAFT, "attjson"))
|
|
139
|
-
?.sizeBytes,
|
|
140
|
-
content.length,
|
|
141
|
-
);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("stores every byte of a form-urlencoded-looking upload too", async () => {
|
|
145
|
-
const content = Buffer.from(`a=1&b=2&c=${"z".repeat(200)}`);
|
|
146
|
-
const url = await reserve("attform", content.length);
|
|
147
|
-
|
|
148
|
-
const response = await fetch(url, {
|
|
149
|
-
method: "PUT",
|
|
150
|
-
body: content,
|
|
151
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
assert.equal(response.status, 204);
|
|
155
|
-
assert.equal(
|
|
156
|
-
(await storage.statOutboxAttachment(CFG, ACC, DRAFT, "attform"))
|
|
157
|
-
?.sizeBytes,
|
|
158
|
-
content.length,
|
|
159
|
-
);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("stores binary unchanged", async () => {
|
|
163
|
-
const content = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0xff, 0x10]);
|
|
164
|
-
const url = await reserve("attpng", content.length);
|
|
165
|
-
|
|
166
|
-
const response = await fetch(url, {
|
|
167
|
-
method: "PUT",
|
|
168
|
-
body: content,
|
|
169
|
-
headers: { "content-type": "image/png" },
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
assert.equal(response.status, 204);
|
|
173
|
-
assert.equal(
|
|
174
|
-
(await storage.statOutboxAttachment(CFG, ACC, DRAFT, "attpng"))
|
|
175
|
-
?.sizeBytes,
|
|
176
|
-
content.length,
|
|
177
|
-
);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("refuses a PUT once the draft no longer has a live reservation", async () => {
|
|
181
|
-
const url = await reserve("attgone", 8);
|
|
182
|
-
rows.delete("attgone");
|
|
183
|
-
|
|
184
|
-
const response = await fetch(url, {
|
|
185
|
-
method: "PUT",
|
|
186
|
-
body: Buffer.alloc(8),
|
|
187
|
-
headers: { "content-type": "application/octet-stream" },
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
assert.equal(response.status, 409);
|
|
191
|
-
assert.equal(
|
|
192
|
-
await storage.statOutboxAttachment(CFG, ACC, DRAFT, "attgone"),
|
|
193
|
-
null,
|
|
194
|
-
);
|
|
195
|
-
});
|
|
196
|
-
});
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname } from "node:path";
|
|
4
|
-
import { logger } from "@remit/logger-lambda";
|
|
5
|
-
import {
|
|
6
|
-
backfillClassifications,
|
|
7
|
-
type ClassificationBackfillCheckpoint,
|
|
8
|
-
type ClassificationBackfillCheckpointStore,
|
|
9
|
-
} from "@remit/mailbox-service";
|
|
10
|
-
import { getClient } from "../src/service/data-client.js";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* One-time, full-corpus classification backfill over the stored-body cohort
|
|
14
|
-
* the classifier never examined (issue #1197): rows whose bodies were synced
|
|
15
|
-
* before the classifier reached them, which carry `NotExamined` on
|
|
16
|
-
* `Message.classificationState` and so stay `uncategorized` in the list. The
|
|
17
|
-
* same alternate-entrypoint shape `backfill-list-id.mjs` uses: baked into the
|
|
18
|
-
* backend image, never wired into a compose one-shot, run by hand once per
|
|
19
|
-
* install:
|
|
20
|
-
*
|
|
21
|
-
* docker compose -f docker-compose.sqlite.yml run --rm backend \
|
|
22
|
-
* node backfill-classification.mjs
|
|
23
|
-
*
|
|
24
|
-
* Safe to interrupt: progress is checkpointed to disk after every page and
|
|
25
|
-
* picked back up on the next run, every already-examined row reads as
|
|
26
|
-
* `alreadyExamined` on a rerun, and a row whose category was already decided
|
|
27
|
-
* is recorded without re-deriving it, so a partial pass resumes rather than
|
|
28
|
-
* repeats.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
const CHECKPOINT_PATH =
|
|
32
|
-
process.env.CLASSIFICATION_BACKFILL_CHECKPOINT_PATH ??
|
|
33
|
-
"/data/sqlite/classification-backfill-checkpoint.json";
|
|
34
|
-
|
|
35
|
-
const isCheckpoint = (
|
|
36
|
-
value: unknown,
|
|
37
|
-
): value is ClassificationBackfillCheckpoint => {
|
|
38
|
-
if (typeof value !== "object" || value === null) return false;
|
|
39
|
-
if (!("accountConfigId" in value)) return false;
|
|
40
|
-
const candidate = value as Record<string, unknown>;
|
|
41
|
-
return (
|
|
42
|
-
typeof candidate.accountConfigId === "string" &&
|
|
43
|
-
(candidate.continuationToken === undefined ||
|
|
44
|
-
typeof candidate.continuationToken === "string")
|
|
45
|
-
);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const fileCheckpointStore: ClassificationBackfillCheckpointStore = {
|
|
49
|
-
load: async () => {
|
|
50
|
-
if (!existsSync(CHECKPOINT_PATH)) return undefined;
|
|
51
|
-
const raw = await readFile(CHECKPOINT_PATH, "utf8");
|
|
52
|
-
const parsed: unknown = JSON.parse(raw);
|
|
53
|
-
return isCheckpoint(parsed) ? parsed : undefined;
|
|
54
|
-
},
|
|
55
|
-
save: async (checkpoint) => {
|
|
56
|
-
await mkdir(dirname(CHECKPOINT_PATH), { recursive: true });
|
|
57
|
-
const staging = `${CHECKPOINT_PATH}.writing`;
|
|
58
|
-
await writeFile(staging, JSON.stringify(checkpoint));
|
|
59
|
-
await rename(staging, CHECKPOINT_PATH);
|
|
60
|
-
},
|
|
61
|
-
clear: async () => {
|
|
62
|
-
await rm(CHECKPOINT_PATH, { force: true });
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const run = async (): Promise<void> => {
|
|
67
|
-
const client = await getClient();
|
|
68
|
-
|
|
69
|
-
const result = await backfillClassifications(
|
|
70
|
-
{
|
|
71
|
-
accountConfigService: client.accountConfig,
|
|
72
|
-
accountService: client.account,
|
|
73
|
-
mailboxSpecialUseService: client.mailboxSpecialUse,
|
|
74
|
-
addressService: client.address,
|
|
75
|
-
threadMessageService: client.threadMessage,
|
|
76
|
-
messageService: client.message,
|
|
77
|
-
storageService: client.storage,
|
|
78
|
-
},
|
|
79
|
-
{ checkpointStore: fileCheckpointStore, logger },
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
// biome-ignore lint/plugin/no-logger-info: a completed full-corpus backfill is an audit-grade signal
|
|
83
|
-
logger.info({ result }, "backfill done");
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
run()
|
|
87
|
-
.then(() => process.exit(0))
|
|
88
|
-
.catch((error: unknown) => {
|
|
89
|
-
logger.error({ error }, "backfill failed");
|
|
90
|
-
process.exit(1);
|
|
91
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname } from "node:path";
|
|
4
|
-
import { logger } from "@remit/logger-lambda";
|
|
5
|
-
import {
|
|
6
|
-
backfillListIds,
|
|
7
|
-
type ListIdBackfillCheckpoint,
|
|
8
|
-
type ListIdBackfillCheckpointStore,
|
|
9
|
-
} from "@remit/mailbox-service";
|
|
10
|
-
import { getClient } from "../src/service/data-client.js";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* One-time, full-corpus `ThreadMessage.listId` backfill (issue #263). Ships as
|
|
14
|
-
* an alternate entrypoint baked into the backend image — "the backend image
|
|
15
|
-
* with a command", the same shape `migrate.mjs` uses — rather than a service
|
|
16
|
-
* of its own. Unlike `migrate`, it is never wired into a compose one-shot: a
|
|
17
|
-
* full pass runs once per install, not on every restart. Invoke it by hand:
|
|
18
|
-
*
|
|
19
|
-
* docker compose -f docker-compose.sqlite.yml run --rm backend \
|
|
20
|
-
* node backfill-list-id.mjs
|
|
21
|
-
*
|
|
22
|
-
* Safe to interrupt: progress is checkpointed to disk after every page and
|
|
23
|
-
* picked back up on the next run, and every row it touches is read-only
|
|
24
|
-
* against the stored message, so a rerun after a partial pass is a no-op for
|
|
25
|
-
* everything already done.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
const CHECKPOINT_PATH =
|
|
29
|
-
process.env.LIST_ID_BACKFILL_CHECKPOINT_PATH ??
|
|
30
|
-
"/data/sqlite/list-id-backfill-checkpoint.json";
|
|
31
|
-
|
|
32
|
-
const isCheckpoint = (value: unknown): value is ListIdBackfillCheckpoint => {
|
|
33
|
-
if (typeof value !== "object" || value === null) return false;
|
|
34
|
-
if (!("accountConfigId" in value)) return false;
|
|
35
|
-
const candidate = value as Record<string, unknown>;
|
|
36
|
-
return (
|
|
37
|
-
typeof candidate.accountConfigId === "string" &&
|
|
38
|
-
(candidate.continuationToken === undefined ||
|
|
39
|
-
typeof candidate.continuationToken === "string")
|
|
40
|
-
);
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const fileCheckpointStore: ListIdBackfillCheckpointStore = {
|
|
44
|
-
load: async () => {
|
|
45
|
-
if (!existsSync(CHECKPOINT_PATH)) return undefined;
|
|
46
|
-
const raw = await readFile(CHECKPOINT_PATH, "utf8");
|
|
47
|
-
const parsed: unknown = JSON.parse(raw);
|
|
48
|
-
return isCheckpoint(parsed) ? parsed : undefined;
|
|
49
|
-
},
|
|
50
|
-
save: async (checkpoint) => {
|
|
51
|
-
await mkdir(dirname(CHECKPOINT_PATH), { recursive: true });
|
|
52
|
-
const staging = `${CHECKPOINT_PATH}.writing`;
|
|
53
|
-
await writeFile(staging, JSON.stringify(checkpoint));
|
|
54
|
-
await rename(staging, CHECKPOINT_PATH);
|
|
55
|
-
},
|
|
56
|
-
clear: async () => {
|
|
57
|
-
await rm(CHECKPOINT_PATH, { force: true });
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const run = async (): Promise<void> => {
|
|
62
|
-
const client = await getClient();
|
|
63
|
-
|
|
64
|
-
const result = await backfillListIds(
|
|
65
|
-
{
|
|
66
|
-
accountConfigService: client.accountConfig,
|
|
67
|
-
threadMessageService: client.threadMessage,
|
|
68
|
-
messageService: client.message,
|
|
69
|
-
storageService: client.storage,
|
|
70
|
-
},
|
|
71
|
-
{ checkpointStore: fileCheckpointStore, logger },
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
// biome-ignore lint/plugin/no-logger-info: a completed full-corpus backfill is an audit-grade signal
|
|
75
|
-
logger.info({ result }, "backfill done");
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
run()
|
|
79
|
-
.then(() => process.exit(0))
|
|
80
|
-
.catch((error: unknown) => {
|
|
81
|
-
logger.error({ error }, "backfill failed");
|
|
82
|
-
process.exit(1);
|
|
83
|
-
});
|
package/scripts/config-save.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { readConfigForExport } from "@remit/config-transfer";
|
|
2
|
-
import { env } from "expect-env";
|
|
3
|
-
// Reached by module path rather than through either package's entry point, the
|
|
4
|
-
// same way the migrate entrypoint reaches its repairs: this file is bundled by
|
|
5
|
-
// esbuild, and a barrel import would drag every repository and the native
|
|
6
|
-
// driver behind it into a script that reads one table.
|
|
7
|
-
import { auth_user } from "../../auth-service/src/schema/auth-schema-sqlite.js";
|
|
8
|
-
import { createSqliteDatabase } from "../../drizzle-service/src/sqlite-client.js";
|
|
9
|
-
import { deriveAccountConfigId } from "../src/auth.js";
|
|
10
|
-
import { getClient } from "../src/service/data-client.js";
|
|
11
|
-
import { exportIdentity } from "../src/service/export-identity.js";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* `remit config save` (issue #1021). Writes one configuration out as a
|
|
15
|
-
* versioned JSON document on stdout, over the same reader the export endpoint
|
|
16
|
-
* uses. Ships as an alternate entrypoint in the backend image — "the backend
|
|
17
|
-
* image with a command", the shape `migrate.mjs` and `backfill-list-id.mjs`
|
|
18
|
-
* already use — because the operator runs it before a migration drops the
|
|
19
|
-
* database, with no browser and no session to authenticate.
|
|
20
|
-
*
|
|
21
|
-
* The document goes to stdout and never to a path inside the container: the
|
|
22
|
-
* wrapper redirects it to a host file, so the file lands where the operator can
|
|
23
|
-
* see it and with their ownership, rather than root-owned inside a volume.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
const USAGE = `Usage: node config-save.mjs [--user <email>]
|
|
27
|
-
|
|
28
|
-
Writes the configuration as a versioned JSON document to stdout. Contains no
|
|
29
|
-
credential: each account records which one it will need back instead.
|
|
30
|
-
|
|
31
|
-
--user <email> Which sign-in to export. Optional on an instance that holds
|
|
32
|
-
exactly one configuration.
|
|
33
|
-
`;
|
|
34
|
-
|
|
35
|
-
interface Options {
|
|
36
|
-
user: string | undefined;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const parseArguments = (argv: readonly string[]): Options => {
|
|
40
|
-
const options: Options = { user: undefined };
|
|
41
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
42
|
-
const argument = argv[index];
|
|
43
|
-
if (argument === "--user") {
|
|
44
|
-
const value = argv[index + 1];
|
|
45
|
-
if (value === undefined || value.startsWith("--")) {
|
|
46
|
-
throw new Error("--user needs an email address");
|
|
47
|
-
}
|
|
48
|
-
options.user = value;
|
|
49
|
-
index += 1;
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
throw new Error(`unknown option '${argument}'`);
|
|
53
|
-
}
|
|
54
|
-
return options;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* The configuration behind a sign-in. Identity lives in better-auth's own
|
|
59
|
-
* tables in the same database file, and the configuration id derives from the
|
|
60
|
-
* user id, so the email on the sign-in screen is the one thing an operator can
|
|
61
|
-
* be expected to know.
|
|
62
|
-
*/
|
|
63
|
-
const accountConfigIdForUser = async (email: string): Promise<string> => {
|
|
64
|
-
const { db, close } = await createSqliteDatabase(
|
|
65
|
-
{ auth_user },
|
|
66
|
-
{ filename: env.SQLITE_DB_PATH },
|
|
67
|
-
);
|
|
68
|
-
try {
|
|
69
|
-
const users = await db
|
|
70
|
-
.select({ id: auth_user.id, email: auth_user.email })
|
|
71
|
-
.from(auth_user);
|
|
72
|
-
// Matched in JS rather than in the query: an address is
|
|
73
|
-
// case-insensitive on the part that matters, and which collation the
|
|
74
|
-
// column happens to carry is not something an operator should have to
|
|
75
|
-
// know before their own email matches.
|
|
76
|
-
const wanted = email.toLowerCase();
|
|
77
|
-
const user = users.find((row) => row.email.toLowerCase() === wanted);
|
|
78
|
-
if (!user) throw new Error(`no user signs in as ${email}`);
|
|
79
|
-
return deriveAccountConfigId(user.id);
|
|
80
|
-
} finally {
|
|
81
|
-
await close();
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const soleAccountConfigId = async (
|
|
86
|
-
listAll: () => Promise<Array<{ accountConfigId: string }>>,
|
|
87
|
-
): Promise<string> => {
|
|
88
|
-
const configs = await listAll();
|
|
89
|
-
const only = configs[0];
|
|
90
|
-
if (!only) throw new Error("this instance holds no configuration to export");
|
|
91
|
-
if (configs.length > 1) {
|
|
92
|
-
throw new Error(
|
|
93
|
-
`this instance holds ${configs.length} configurations — name one with --user <email>`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
return only.accountConfigId;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const run = async (): Promise<void> => {
|
|
100
|
-
const argv = process.argv.slice(2);
|
|
101
|
-
if (argv.includes("--help") || argv.includes("-h")) {
|
|
102
|
-
process.stdout.write(USAGE);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const options = parseArguments(argv);
|
|
107
|
-
const client = await getClient();
|
|
108
|
-
const accountConfigId = options.user
|
|
109
|
-
? await accountConfigIdForUser(options.user)
|
|
110
|
-
: await soleAccountConfigId(() => client.accountConfig.listAll());
|
|
111
|
-
|
|
112
|
-
const document = await readConfigForExport(
|
|
113
|
-
client,
|
|
114
|
-
accountConfigId,
|
|
115
|
-
exportIdentity(),
|
|
116
|
-
);
|
|
117
|
-
process.stdout.write(`${JSON.stringify(document, null, "\t")}\n`);
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
// No `process.exit` on the way out: the document is written to stdout, and
|
|
121
|
-
// stdout is a pipe under `compose run`, so exiting drops whatever write is
|
|
122
|
-
// still pending. Setting the code and letting the process end delivers it.
|
|
123
|
-
await run().catch((error: unknown) => {
|
|
124
|
-
process.stderr.write(
|
|
125
|
-
`config save: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
126
|
-
);
|
|
127
|
-
process.exitCode = 1;
|
|
128
|
-
});
|
package/src/auth.test.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, test } from "node:test";
|
|
3
|
-
import { deriveAccountConfigId } from "./auth.js";
|
|
4
|
-
|
|
5
|
-
describe("derived ids are stored primary keys: a changed value orphans every row already written and needs a migration, never a new expectation here", () => {
|
|
6
|
-
test("deriveAccountConfigId pins the account a Cognito subject resolves to", () => {
|
|
7
|
-
assert.equal(
|
|
8
|
-
deriveAccountConfigId("golden-cognito-sub"),
|
|
9
|
-
"97h2fhd0c32ocenk6ju12oxto",
|
|
10
|
-
);
|
|
11
|
-
});
|
|
12
|
-
});
|