@remit/storage-service 0.0.15 → 0.0.16

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.
@@ -168,8 +168,10 @@ export const createFilesystemStorageService = (basePath, uploadUrls) => {
168
168
  };
169
169
  };
170
170
  const deleteOutboxAttachments = async (accountConfigId, accountId, outboxMessageId) => {
171
- const dirPath = join(basePath, buildOutboxAttachmentPrefix(accountConfigId, accountId, outboxMessageId));
172
- await rm(dirPath, { recursive: true, force: true });
171
+ await deletePrefix(buildOutboxAttachmentPrefix(accountConfigId, accountId, outboxMessageId));
172
+ };
173
+ const deletePrefix = async (prefix) => {
174
+ await rm(join(basePath, prefix), { recursive: true, force: true });
173
175
  };
174
176
  const storeDeduplicated = (params) => {
175
177
  const { accountConfigId, accountId, content } = params;
@@ -242,6 +244,7 @@ export const createFilesystemStorageService = (basePath, uploadUrls) => {
242
244
  listOutboxAttachments,
243
245
  listOutboxDraftsWithAttachments,
244
246
  deleteOutboxAttachments,
247
+ deletePrefix,
245
248
  deleteOutboxAttachment,
246
249
  statOutboxAttachment,
247
250
  retrieveOutboxAttachment,
@@ -194,8 +194,7 @@ export const createS3StorageService = (client, bucketName) => {
194
194
  }), { expiresIn });
195
195
  return { uploadUrl };
196
196
  };
197
- const deleteOutboxAttachments = async (accountConfigId, accountId, outboxMessageId) => {
198
- const prefix = buildOutboxAttachmentPrefix(accountConfigId, accountId, outboxMessageId);
197
+ const deletePrefix = async (prefix) => {
199
198
  let continuationToken;
200
199
  do {
201
200
  const response = await client.send(new ListObjectsV2Command({
@@ -217,6 +216,7 @@ export const createS3StorageService = (client, bucketName) => {
217
216
  : undefined;
218
217
  } while (continuationToken);
219
218
  };
219
+ const deleteOutboxAttachments = (accountConfigId, accountId, outboxMessageId) => deletePrefix(buildOutboxAttachmentPrefix(accountConfigId, accountId, outboxMessageId));
220
220
  const storeDeduplicated = (params) => {
221
221
  const { accountConfigId, accountId, content, contentType } = params;
222
222
  const checksumSha256 = computeChecksum(content);
@@ -358,6 +358,7 @@ export const createS3StorageService = (client, bucketName) => {
358
358
  listOutboxAttachments,
359
359
  listOutboxDraftsWithAttachments,
360
360
  deleteOutboxAttachments,
361
+ deletePrefix,
361
362
  deleteOutboxAttachment,
362
363
  statOutboxAttachment,
363
364
  retrieveOutboxAttachment,
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { type OutboxSweepResult, sweepAbandonedOutboxAttachments, } from "./outbox-sweep.js";
2
2
  export { type AuthorizeUploadInput, type AuthorizeUploadResult, authorizeUploadRequest, UPLOAD_ROUTE_PREFIX, UPLOAD_URL_SIGNING_LABEL, UPLOAD_URL_TTL_SECONDS, } from "./outbox-upload-url.js";
3
3
  export { type SignedPathFailure, type SignedPathResult, signStoragePath, verifyStoragePath, } from "./signed-path.js";
4
- export { buildBodyPartKey, buildDeduplicatedKey, buildExportArchiveKey, buildMessageBodyKey, buildOutboxAttachmentKey, buildOutboxAttachmentPrefix, buildParsedBodyKey, type ContentEncodingValue, type CreateOutboxAttachmentUploadUrlParams, computeChecksum, createMockStorageService, isStorageNotFoundError, type OutboxAttachmentListItem, type OutboxAttachmentUploadTarget, type ParsedAttachmentMeta, type ParsedBody, type ParsedContentStorageKey, type ParsedOutboxAttachmentKey, parseContentStorageKey, parseOutboxAttachmentKey, type StorageReference, type StorageService, type StorageTypeValue, type StoreBodyPartParams, type StoreDeduplicatedParams, type StoreMessageBodyParams, type StoreOutboxAttachmentParams, type StoreParsedBodyParams, } from "./storage.js";
4
+ export { buildBodyPartKey, buildDeduplicatedKey, buildExportArchiveKey, buildMessageBodyKey, buildOutboxAttachmentKey, buildOutboxAttachmentPrefix, buildParsedBodyKey, type ContentEncodingValue, type CreateOutboxAttachmentUploadUrlParams, computeChecksum, createMockStorageService, isStorageNotFoundError, messageObjectPrefixOf, type OutboxAttachmentListItem, type OutboxAttachmentUploadTarget, type ParsedAttachmentMeta, type ParsedBody, type ParsedContentStorageKey, type ParsedOutboxAttachmentKey, parseContentStorageKey, parseOutboxAttachmentKey, type StorageReference, type StorageService, type StorageTypeValue, type StoreBodyPartParams, type StoreDeduplicatedParams, type StoreMessageBodyParams, type StoreOutboxAttachmentParams, type StoreParsedBodyParams, } from "./storage.js";
5
5
  export { buildStorageUri, type ParsedStorageUri, parseStorageUri, } from "./uri.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { sweepAbandonedOutboxAttachments, } from "./outbox-sweep.js";
2
2
  export { authorizeUploadRequest, UPLOAD_ROUTE_PREFIX, UPLOAD_URL_SIGNING_LABEL, UPLOAD_URL_TTL_SECONDS, } from "./outbox-upload-url.js";
3
3
  export { signStoragePath, verifyStoragePath, } from "./signed-path.js";
4
- export { buildBodyPartKey, buildDeduplicatedKey, buildExportArchiveKey, buildMessageBodyKey, buildOutboxAttachmentKey, buildOutboxAttachmentPrefix, buildParsedBodyKey, computeChecksum, createMockStorageService, isStorageNotFoundError, parseContentStorageKey, parseOutboxAttachmentKey, } from "./storage.js";
4
+ export { buildBodyPartKey, buildDeduplicatedKey, buildExportArchiveKey, buildMessageBodyKey, buildOutboxAttachmentKey, buildOutboxAttachmentPrefix, buildParsedBodyKey, computeChecksum, createMockStorageService, isStorageNotFoundError, messageObjectPrefixOf, parseContentStorageKey, parseOutboxAttachmentKey, } from "./storage.js";
5
5
  export { buildStorageUri, parseStorageUri, } from "./uri.js";
package/dist/storage.d.ts CHANGED
@@ -190,6 +190,11 @@ export interface StorageService {
190
190
  exists(uri: string): Promise<boolean>;
191
191
  /** Delete content by URI */
192
192
  delete(uri: string): Promise<void>;
193
+ /**
194
+ * Delete every object whose key starts with `keyPrefix`, a key path ending
195
+ * in `/`. Deleting a prefix that holds nothing is a no-op.
196
+ */
197
+ deletePrefix(keyPrefix: string): Promise<void>;
193
198
  /**
194
199
  * Stream a finished export archive (already-compressed ZIP) to the export
195
200
  * key for the given config + request, uncompressed at rest. The body is
@@ -240,6 +245,7 @@ export interface ParsedContentStorageKey {
240
245
  * above). Returns null when the shape doesn't match.
241
246
  */
242
247
  export declare const parseContentStorageKey: (storageKey: string) => ParsedContentStorageKey | null;
248
+ export declare const messageObjectPrefixOf: (bodyStorageUri: string) => string;
243
249
  export declare const computeChecksum: (content: Buffer) => string;
244
250
  export declare const buildExportArchiveKey: (accountConfigId: string, exportRequestId: string) => string;
245
251
  export declare const isStorageNotFoundError: (error: unknown) => boolean;
package/dist/storage.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Readable } from "node:stream";
3
3
  import { ContentEncoding, StorageType } from "@remit/domain-enums";
4
+ import { parseStorageUri } from "./uri.js";
4
5
  // Path builders - centralized path formatting per RFC 011 + #224.
5
6
  // The outer segment is `accountConfigId` (Cognito-derived tenant scope) so
6
7
  // the edge can verify cross-tenant isolation at the URL prefix without
@@ -51,6 +52,14 @@ export const parseContentStorageKey = (storageKey) => {
51
52
  messageId: match[3],
52
53
  };
53
54
  };
55
+ const MESSAGE_OBJECT_PREFIX = /(?:^|\/)(accounts\/[^/]+\/(?:[^/]+\/)?messages\/[^/]+\/)[^/]+$/;
56
+ export const messageObjectPrefixOf = (bodyStorageUri) => {
57
+ const match = parseStorageUri(bodyStorageUri).storageKey.match(MESSAGE_OBJECT_PREFIX);
58
+ if (!match) {
59
+ throw new Error(`${bodyStorageUri} is not a message body key; its objects cannot be found to delete`);
60
+ }
61
+ return match[1];
62
+ };
54
63
  export const computeChecksum = (content) => createHash("sha256").update(content).digest("hex");
55
64
  export const buildExportArchiveKey = (accountConfigId, exportRequestId) => `exports/${accountConfigId}/${exportRequestId}/export.zip`;
56
65
  export const isStorageNotFoundError = (error) => {
@@ -65,15 +74,16 @@ export const isStorageNotFoundError = (error) => {
65
74
  return true;
66
75
  return false;
67
76
  };
77
+ const MOCK_ROOT = "s3://mock/";
68
78
  export const createMockStorageService = () => {
69
79
  const storage = new Map();
70
80
  const storeInternal = (key, content) => {
71
81
  const checksumSha256 = computeChecksum(content);
72
- const uri = `mock://${key}`;
82
+ const uri = `${MOCK_ROOT}${key}`;
73
83
  storage.set(uri, content);
74
84
  return {
75
85
  uri,
76
- storageType: StorageType.Filesystem,
86
+ storageType: StorageType.S3,
77
87
  storageLocation: "mock",
78
88
  storageKey: key,
79
89
  sizeBytes: content.length,
@@ -94,21 +104,21 @@ export const createMockStorageService = () => {
94
104
  return storeInternal(buildOutboxAttachmentKey(accountConfigId, accountId, outboxMessageId, outboxAttachmentId), content);
95
105
  };
96
106
  const listOutboxAttachments = async (accountConfigId, accountId, outboxMessageId) => {
97
- const prefix = `mock://${buildOutboxAttachmentPrefix(accountConfigId, accountId, outboxMessageId)}`;
107
+ const prefix = `${MOCK_ROOT}${buildOutboxAttachmentPrefix(accountConfigId, accountId, outboxMessageId)}`;
98
108
  const items = [];
99
109
  for (const [uri, content] of storage.entries()) {
100
110
  if (!uri.startsWith(prefix))
101
111
  continue;
102
112
  items.push({
103
113
  outboxAttachmentId: uri.slice(prefix.length),
104
- key: uri.slice("mock://".length),
114
+ key: uri.slice(MOCK_ROOT.length),
105
115
  sizeBytes: content.length,
106
116
  });
107
117
  }
108
118
  return items;
109
119
  };
110
120
  const listOutboxDraftsWithAttachments = async (accountConfigId, accountId) => {
111
- const root = `mock://accounts/${accountConfigId}/${accountId}/outbox/`;
121
+ const root = `${MOCK_ROOT}accounts/${accountConfigId}/${accountId}/outbox/`;
112
122
  const drafts = new Set();
113
123
  for (const uri of storage.keys()) {
114
124
  if (!uri.startsWith(root))
@@ -121,23 +131,23 @@ export const createMockStorageService = () => {
121
131
  return [...drafts];
122
132
  };
123
133
  const deleteOutboxAttachment = async (accountConfigId, accountId, outboxMessageId, outboxAttachmentId) => {
124
- storage.delete(`mock://${buildOutboxAttachmentKey(accountConfigId, accountId, outboxMessageId, outboxAttachmentId)}`);
134
+ storage.delete(`${MOCK_ROOT}${buildOutboxAttachmentKey(accountConfigId, accountId, outboxMessageId, outboxAttachmentId)}`);
125
135
  };
126
136
  const statOutboxAttachment = async (accountConfigId, accountId, outboxMessageId, outboxAttachmentId) => {
127
- const content = storage.get(`mock://${buildOutboxAttachmentKey(accountConfigId, accountId, outboxMessageId, outboxAttachmentId)}`);
137
+ const content = storage.get(`${MOCK_ROOT}${buildOutboxAttachmentKey(accountConfigId, accountId, outboxMessageId, outboxAttachmentId)}`);
128
138
  return content ? { sizeBytes: content.length } : null;
129
139
  };
130
- const retrieveOutboxAttachment = async (accountConfigId, accountId, outboxMessageId, outboxAttachmentId) => storage.get(`mock://${buildOutboxAttachmentKey(accountConfigId, accountId, outboxMessageId, outboxAttachmentId)}`) ?? null;
140
+ const retrieveOutboxAttachment = async (accountConfigId, accountId, outboxMessageId, outboxAttachmentId) => storage.get(`${MOCK_ROOT}${buildOutboxAttachmentKey(accountConfigId, accountId, outboxMessageId, outboxAttachmentId)}`) ?? null;
131
141
  const createOutboxAttachmentUploadUrl = async (params) => ({
132
142
  uploadUrl: `mock://upload/${buildOutboxAttachmentKey(params.accountConfigId, params.accountId, params.outboxMessageId, params.outboxAttachmentId)}?max=${params.sizeBytes}&exp=${params.expiresAt}`,
133
143
  });
134
- const deleteOutboxAttachments = async (accountConfigId, accountId, outboxMessageId) => {
135
- const prefix = `mock://${buildOutboxAttachmentPrefix(accountConfigId, accountId, outboxMessageId)}`;
144
+ const deletePrefix = async (keyPrefix) => {
136
145
  for (const uri of [...storage.keys()]) {
137
- if (uri.startsWith(prefix))
146
+ if (uri.startsWith(`${MOCK_ROOT}${keyPrefix}`))
138
147
  storage.delete(uri);
139
148
  }
140
149
  };
150
+ const deleteOutboxAttachments = (accountConfigId, accountId, outboxMessageId) => deletePrefix(buildOutboxAttachmentPrefix(accountConfigId, accountId, outboxMessageId));
141
151
  const storeDeduplicated = async (params) => {
142
152
  const { accountConfigId, accountId, content } = params;
143
153
  const checksumSha256 = computeChecksum(content);
@@ -149,29 +159,29 @@ export const createMockStorageService = () => {
149
159
  return storeInternal(buildParsedBodyKey(accountConfigId, accountId, messageId), content);
150
160
  };
151
161
  const retrieveParsedBody = async (accountConfigId, accountId, messageId) => {
152
- const uri = `mock://${buildParsedBodyKey(accountConfigId, accountId, messageId)}`;
162
+ const uri = `${MOCK_ROOT}${buildParsedBodyKey(accountConfigId, accountId, messageId)}`;
153
163
  const content = storage.get(uri);
154
164
  if (!content)
155
165
  return null;
156
166
  return JSON.parse(content.toString("utf8"));
157
167
  };
158
168
  const retrieveBodyPart = async (accountConfigId, accountId, messageId, partPath) => {
159
- const uri = `mock://${buildBodyPartKey(accountConfigId, accountId, messageId, partPath)}`;
169
+ const uri = `${MOCK_ROOT}${buildBodyPartKey(accountConfigId, accountId, messageId, partPath)}`;
160
170
  return storage.get(uri) ?? null;
161
171
  };
162
172
  const retrieveMessageBody = async (accountConfigId, accountId, messageId) => {
163
- const uri = `mock://${buildMessageBodyKey(accountConfigId, accountId, messageId)}`;
173
+ const uri = `${MOCK_ROOT}${buildMessageBodyKey(accountConfigId, accountId, messageId)}`;
164
174
  return storage.get(uri) ?? null;
165
175
  };
166
176
  const retrieveMessageBodyStream = async (accountConfigId, accountId, messageId) => {
167
- const uri = `mock://${buildMessageBodyKey(accountConfigId, accountId, messageId)}`;
177
+ const uri = `${MOCK_ROOT}${buildMessageBodyKey(accountConfigId, accountId, messageId)}`;
168
178
  const content = storage.get(uri);
169
179
  if (!content)
170
180
  return null;
171
181
  return Readable.from(content);
172
182
  };
173
183
  const bodyPartExists = async (accountConfigId, accountId, messageId, partPath) => {
174
- const uri = `mock://${buildBodyPartKey(accountConfigId, accountId, messageId, partPath)}`;
184
+ const uri = `${MOCK_ROOT}${buildBodyPartKey(accountConfigId, accountId, messageId, partPath)}`;
175
185
  return storage.has(uri);
176
186
  };
177
187
  return {
@@ -183,6 +193,7 @@ export const createMockStorageService = () => {
183
193
  listOutboxAttachments,
184
194
  listOutboxDraftsWithAttachments,
185
195
  deleteOutboxAttachments,
196
+ deletePrefix,
186
197
  deleteOutboxAttachment,
187
198
  statOutboxAttachment,
188
199
  retrieveOutboxAttachment,
@@ -211,11 +222,11 @@ export const createMockStorageService = () => {
211
222
  for await (const chunk of body) {
212
223
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
213
224
  }
214
- storage.set(`mock://${key}`, Buffer.concat(chunks));
225
+ storage.set(`${MOCK_ROOT}${key}`, Buffer.concat(chunks));
215
226
  return key;
216
227
  },
217
228
  deleteExportArchive: async (accountConfigId, exportRequestId) => {
218
- storage.delete(`mock://${buildExportArchiveKey(accountConfigId, exportRequestId)}`);
229
+ storage.delete(`${MOCK_ROOT}${buildExportArchiveKey(accountConfigId, exportRequestId)}`);
219
230
  },
220
231
  getPresignedDownloadUrl: async (key, _expiresInSeconds) => `mock://presigned/${key}`,
221
232
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/storage-service",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",