All files / lib/attachment-archive bucket-routing.ts

50% Statements 20/40
63.63% Branches 21/33
62.5% Functions 5/8
50% Lines 20/40

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122          3x     3x     4x       10x 5x     5x   5x 3x 3x     2x 2x       2x   2x                 2x 2x         2x       10x 10x                     15x   15x                                                                                                              
import { S3Client } from "@aws-sdk/client-s3";
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
 
export type AttachmentBucketMap = Record<string, string>;
 
const NO_MAP_CONFIGURED = "__NO_MAP_CONFIGURED__";
 
let cachedAttachmentBucketMap: AttachmentBucketMap | undefined;
let cachedAttachmentBucketMapRaw = NO_MAP_CONFIGURED;
 
export function isLegacyUploadBucket(bucket: string): boolean {
  return bucket.startsWith("uploads");
}
 
export function parseAttachmentBucketMap(rawMap?: string): AttachmentBucketMap {
  if (cachedAttachmentBucketMap && cachedAttachmentBucketMapRaw === (rawMap ?? NO_MAP_CONFIGURED)) {
    return cachedAttachmentBucketMap;
  }
 
  cachedAttachmentBucketMapRaw = rawMap ?? NO_MAP_CONFIGURED;
 
  if (!rawMap) {
    cachedAttachmentBucketMap = {};
    return cachedAttachmentBucketMap;
  }
 
  const parsedMap: unknown = JSON.parse(rawMap);
  Iif (typeof parsedMap !== "object" || parsedMap === null || Array.isArray(parsedMap)) {
    throw new Error("LEGACY_ATTACHMENT_BUCKET_MAP must be a JSON object");
  }
 
  cachedAttachmentBucketMap = Object.entries(parsedMap).reduce<AttachmentBucketMap>(
    (acc, [sourceBucket, destinationBucket]) => {
      Iif (
        typeof sourceBucket !== "string" ||
        sourceBucket.length === 0 ||
        typeof destinationBucket !== "string" ||
        destinationBucket.length === 0
      ) {
        throw new Error("LEGACY_ATTACHMENT_BUCKET_MAP must map non-empty strings");
      }
 
      acc[sourceBucket] = destinationBucket;
      return acc;
    },
    {},
  );
 
  return cachedAttachmentBucketMap;
}
 
export function getAttachmentBucketMap(rawMap?: string, onInvalid?: (message: string) => void) {
  try {
    return parseAttachmentBucketMap(rawMap);
  } catch (error) {
    onInvalid?.(error instanceof Error ? error.message : String(error));
    return {};
  }
}
 
export function resolveTargetBucket(
  sourceBucket: string,
  attachmentBucketMap: AttachmentBucketMap,
) {
  const destinationBucket = attachmentBucketMap[sourceBucket] || sourceBucket;
 
  return {
    sourceBucket,
    destinationBucket,
    remapped: destinationBucket !== sourceBucket,
  };
}
 
export function createAttachmentBucketClientFactory({
  region,
  legacyS3AccessRoleArn,
}: {
  region?: string;
  legacyS3AccessRoleArn?: string;
}) {
  const clientCache = new Map<string, Promise<S3Client>>();
  const stsClient = new STSClient({ region });
 
  return async (bucket: string) => {
    const cachedClient = clientCache.get(bucket);
    if (cachedClient) {
      return cachedClient;
    }
 
    const clientPromise = (async () => {
      if (!isLegacyUploadBucket(bucket) || !legacyS3AccessRoleArn) {
        return new S3Client({ region });
      }
 
      const assumedRoleResponse = await stsClient.send(
        new AssumeRoleCommand({
          RoleArn: legacyS3AccessRoleArn,
          RoleSessionName: "AttachmentArchiveLegacyS3Access",
        }),
      );
 
      const assumedCredentials = assumedRoleResponse.Credentials;
 
      if (!assumedCredentials) {
        throw new Error("No assumed credentials returned for legacy S3 access role");
      }
 
      return new S3Client({
        region,
        credentials: {
          accessKeyId: assumedCredentials.AccessKeyId as string,
          secretAccessKey: assumedCredentials.SecretAccessKey as string,
          sessionToken: assumedCredentials.SessionToken,
        },
      });
    })();
 
    clientCache.set(bucket, clientPromise);
    return clientPromise;
  };
}