@exabugs/dynamodb-client 1.5.2 → 1.6.0
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/CHANGELOG.md +15 -0
- package/dist/server/handler.cjs +10855 -6862
- package/dist/server/handler.cjs.map +4 -4
- package/dist/server/media/magicBytes.d.ts +25 -0
- package/dist/server/media/magicBytes.d.ts.map +1 -0
- package/dist/server/media/magicBytes.js +53 -0
- package/dist/server/media/magicBytes.js.map +1 -0
- package/dist/server/media/media-handler.d.ts +13 -0
- package/dist/server/media/media-handler.d.ts.map +1 -0
- package/dist/server/media/media-handler.js +177 -0
- package/dist/server/media/media-handler.js.map +1 -0
- package/dist/server/media/metadata.d.ts +29 -0
- package/dist/server/media/metadata.d.ts.map +1 -0
- package/dist/server/media/metadata.js +57 -0
- package/dist/server/media/metadata.js.map +1 -0
- package/dist/server/media/presign.d.ts +9 -0
- package/dist/server/media/presign.d.ts.map +1 -0
- package/dist/server/media/presign.js +90 -0
- package/dist/server/media/presign.js.map +1 -0
- package/dist/server/media/process-handler-dlq.d.ts +3 -0
- package/dist/server/media/process-handler-dlq.d.ts.map +1 -0
- package/dist/server/media/process-handler-dlq.js +94 -0
- package/dist/server/media/process-handler-dlq.js.map +1 -0
- package/dist/server/media/process-handler.d.ts +12 -0
- package/dist/server/media/process-handler.d.ts.map +1 -0
- package/dist/server/media/process-handler.js +153 -0
- package/dist/server/media/process-handler.js.map +1 -0
- package/dist/server/media/sign.d.ts +11 -0
- package/dist/server/media/sign.d.ts.map +1 -0
- package/dist/server/media/sign.js +49 -0
- package/dist/server/media/sign.js.map +1 -0
- package/dist/server/media/ssmParams.d.ts +29 -0
- package/dist/server/media/ssmParams.d.ts.map +1 -0
- package/dist/server/media/ssmParams.js +74 -0
- package/dist/server/media/ssmParams.js.map +1 -0
- package/dist/server/media/types.d.ts +109 -0
- package/dist/server/media/types.d.ts.map +1 -0
- package/dist/server/media/types.js +2 -0
- package/dist/server/media/types.js.map +1 -0
- package/dist/server/media/upload-handler.d.ts +50 -0
- package/dist/server/media/upload-handler.d.ts.map +1 -0
- package/dist/server/media/upload-handler.js +115 -0
- package/dist/server/media/upload-handler.js.map +1 -0
- package/dist/server/media-handler.cjs +57474 -0
- package/dist/server/media-handler.cjs.map +7 -0
- package/dist/server/media-process-handler-dlq.cjs +48138 -0
- package/dist/server/media-process-handler-dlq.cjs.map +7 -0
- package/dist/server/media-process-handler.cjs +60680 -0
- package/dist/server/media-process-handler.cjs.map +7 -0
- package/dist/sharp-layer.zip +0 -0
- package/package.json +38 -3
- package/terraform/main.tf +3 -2
- package/terraform/media/README.md +67 -0
- package/terraform/media/main.tf +791 -0
- package/terraform/media/outputs.tf +66 -0
- package/terraform/media/variables.tf +117 -0
- package/terraform/modules/parameter-store/main.tf +26 -1
- package/terraform/modules/parameter-store/variables.tf +7 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { CopyObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3';
|
|
3
|
+
import { SSMClient } from '@aws-sdk/client-ssm';
|
|
4
|
+
import { createLogger } from '../../shared/index.js';
|
|
5
|
+
import { handleFindMany } from '../operations/findMany.js';
|
|
6
|
+
import { handleUpdateOne } from '../operations/updateOne.js';
|
|
7
|
+
import { isProcessableImage, verifyMagicBytes } from './magicBytes.js';
|
|
8
|
+
import { buildContentDisposition, decodeOriginalFilename, extractAppMetadata } from './metadata.js';
|
|
9
|
+
import { getImagePolicy } from './ssmParams.js';
|
|
10
|
+
const logger = createLogger({ service: 'media-process-handler' });
|
|
11
|
+
const s3Client = new S3Client({ region: process.env.AWS_REGION });
|
|
12
|
+
const ssmClient = new SSMClient({ region: process.env.AWS_REGION });
|
|
13
|
+
/** sharp のDoS対策(極端に大きい画素数の画像を拒否) */
|
|
14
|
+
const MAX_INPUT_PIXELS = 100_000_000;
|
|
15
|
+
/**
|
|
16
|
+
* マジックナンバー照合に必要な先頭バイト数。現状の照合表(magicBytes.ts)は
|
|
17
|
+
* 最大12バイト程度までしか見ないが、将来の形式追加を見込んで余裕を持たせる。
|
|
18
|
+
*/
|
|
19
|
+
const MAGIC_BYTES_CHECK_RANGE = 255;
|
|
20
|
+
async function streamToBuffer(body) {
|
|
21
|
+
const bytes = await body.transformToByteArray();
|
|
22
|
+
return Buffer.from(bytes);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* マジックナンバー照合用に、先頭の一部バイトだけを取得する。
|
|
26
|
+
*
|
|
27
|
+
* 非画像ファイル(CopyObjectで足りる)は、これ以上ファイル全体をメモリに
|
|
28
|
+
* 読み込む必要が無い。大きな動画・PDF等でも、この先頭バイトの取得だけで
|
|
29
|
+
* 検証が完結し、Lambdaのメモリを無駄に消費しない(詳細: docs/adr/0002-media-design.md)。
|
|
30
|
+
*/
|
|
31
|
+
async function getHeadBytes(bucket, key) {
|
|
32
|
+
const result = await s3Client.send(new GetObjectCommand({ Bucket: bucket, Key: key, Range: `bytes=0-${MAGIC_BYTES_CHECK_RANGE}` }));
|
|
33
|
+
return streamToBuffer(result.Body);
|
|
34
|
+
}
|
|
35
|
+
async function writeMediaRecord(fileId, fields, requestId) {
|
|
36
|
+
const now = new Date().toISOString();
|
|
37
|
+
await handleUpdateOne('media', {
|
|
38
|
+
id: fileId,
|
|
39
|
+
data: {
|
|
40
|
+
$set: { ...fields, updatedAt: now },
|
|
41
|
+
$setOnInsert: { createdAt: now },
|
|
42
|
+
},
|
|
43
|
+
options: { upsert: true },
|
|
44
|
+
}, requestId);
|
|
45
|
+
}
|
|
46
|
+
async function processRecord(bucket, rawKey) {
|
|
47
|
+
const key = decodeURIComponent(rawKey.replace(/\+/g, ' '));
|
|
48
|
+
const fileId = key.replace(/^raw\//, '');
|
|
49
|
+
const requestId = `s3-event-${fileId}`;
|
|
50
|
+
// fileId→内容の不変性を保証する: 同じpresignのTTL内(既定30秒)に2回目のPOSTが
|
|
51
|
+
// 行われた場合、raw/{fileId}への2回目のPutObjectイベントが発火しうる。既にこの
|
|
52
|
+
// fileIdがcompletedとして処理済みなら、再処理してmasterを上書きしない
|
|
53
|
+
// (process-handler-dlq.tsの重複配信対策と同じガード。詳細: docs/media-design.md
|
|
54
|
+
// 「presigned POSTの再利用・master不変性について」)
|
|
55
|
+
const existing = await handleFindMany('media', { ids: [fileId] }, requestId);
|
|
56
|
+
const existingRecord = existing[0];
|
|
57
|
+
if (existingRecord?.status === 'completed') {
|
|
58
|
+
logger.warn('Media already completed, ignoring re-upload to the same fileId', {
|
|
59
|
+
requestId,
|
|
60
|
+
fileId,
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const head = await s3Client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
|
|
65
|
+
const contentType = head.ContentType || 'application/octet-stream';
|
|
66
|
+
const metadata = head.Metadata || {};
|
|
67
|
+
const originalFilename = decodeOriginalFilename(metadata['original-filename']);
|
|
68
|
+
// マジックナンバー照合は先頭バイトのみで完結する。非画像(CopyObjectで足りる)
|
|
69
|
+
// の場合、このあとファイル全体を読み込む必要が無い
|
|
70
|
+
const headBytes = await getHeadBytes(bucket, key);
|
|
71
|
+
if (!verifyMagicBytes(contentType, headBytes)) {
|
|
72
|
+
logger.warn('Content type does not match file signature', { requestId, fileId, contentType });
|
|
73
|
+
await writeMediaRecord(fileId, {
|
|
74
|
+
status: 'failed',
|
|
75
|
+
contentType,
|
|
76
|
+
error: 'Content type does not match file signature',
|
|
77
|
+
}, requestId);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const masterKey = `master/${fileId}`;
|
|
81
|
+
const appMetadata = extractAppMetadata(metadata);
|
|
82
|
+
if (isProcessableImage(contentType)) {
|
|
83
|
+
const imagePolicyParam = process.env.IMAGE_POLICY_PARAM;
|
|
84
|
+
if (!imagePolicyParam) {
|
|
85
|
+
throw new Error('IMAGE_POLICY_PARAM environment variable is not set');
|
|
86
|
+
}
|
|
87
|
+
const imagePolicy = await getImagePolicy(ssmClient, imagePolicyParam);
|
|
88
|
+
// 画像処理にはファイル全体が必要なため、ここで改めて全量を取得する
|
|
89
|
+
const getResult = await s3Client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
90
|
+
const buffer = await streamToBuffer(getResult.Body);
|
|
91
|
+
const image = sharp(buffer, { limitInputPixels: MAX_INPUT_PIXELS }).rotate();
|
|
92
|
+
const inputMeta = await image.metadata();
|
|
93
|
+
const maxDim = imagePolicy.masterMaxDimension;
|
|
94
|
+
if ((inputMeta.width ?? 0) > maxDim || (inputMeta.height ?? 0) > maxDim) {
|
|
95
|
+
image.resize(maxDim, maxDim, { fit: 'inside', withoutEnlargement: true });
|
|
96
|
+
}
|
|
97
|
+
const outputBuffer = await image.jpeg().toBuffer();
|
|
98
|
+
const outputMeta = await sharp(outputBuffer).metadata();
|
|
99
|
+
await s3Client.send(new PutObjectCommand({
|
|
100
|
+
Bucket: bucket,
|
|
101
|
+
Key: masterKey,
|
|
102
|
+
Body: outputBuffer,
|
|
103
|
+
ContentType: 'image/jpeg',
|
|
104
|
+
}));
|
|
105
|
+
await writeMediaRecord(fileId, {
|
|
106
|
+
status: 'completed',
|
|
107
|
+
contentType,
|
|
108
|
+
outputContentType: 'image/jpeg',
|
|
109
|
+
dimensions: { width: outputMeta.width ?? 0, height: outputMeta.height ?? 0 },
|
|
110
|
+
size: outputBuffer.length,
|
|
111
|
+
...(originalFilename ? { originalFilename } : {}),
|
|
112
|
+
...appMetadata,
|
|
113
|
+
}, requestId);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
await s3Client.send(new CopyObjectCommand({
|
|
117
|
+
Bucket: bucket,
|
|
118
|
+
Key: masterKey,
|
|
119
|
+
CopySource: `${bucket}/${key}`,
|
|
120
|
+
MetadataDirective: 'REPLACE',
|
|
121
|
+
ContentType: contentType,
|
|
122
|
+
ContentDisposition: buildContentDisposition(originalFilename),
|
|
123
|
+
}));
|
|
124
|
+
await writeMediaRecord(fileId, {
|
|
125
|
+
status: 'completed',
|
|
126
|
+
contentType,
|
|
127
|
+
outputContentType: contentType,
|
|
128
|
+
size: head.ContentLength ?? 0,
|
|
129
|
+
...(originalFilename ? { originalFilename } : {}),
|
|
130
|
+
...appMetadata,
|
|
131
|
+
}, requestId);
|
|
132
|
+
}
|
|
133
|
+
logger.info('Media processed', { requestId, fileId, contentType });
|
|
134
|
+
}
|
|
135
|
+
export const handler = async (event) => {
|
|
136
|
+
for (const record of event.Records) {
|
|
137
|
+
const bucket = record.s3.bucket.name;
|
|
138
|
+
const key = record.s3.object.key;
|
|
139
|
+
try {
|
|
140
|
+
await processRecord(bucket, key);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
logger.error('Failed to process media', {
|
|
144
|
+
bucket,
|
|
145
|
+
key,
|
|
146
|
+
error: error instanceof Error ? error.message : String(error),
|
|
147
|
+
});
|
|
148
|
+
// 再スロー: Lambda非同期呼び出しのリトライに乗せ、尽きたらon-failure宛先(DLQ)へ送る
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
//# sourceMappingURL=process-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-handler.js","sourceRoot":"","sources":["../../../src/server/media/process-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SignMediaUrlParams, SignMediaUrlResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* メディア配信用の署名付きURLを生成する。
|
|
4
|
+
*
|
|
5
|
+
* width指定時は /resize/{fileId}?width=N、省略時は /master/{fileId} に署名する
|
|
6
|
+
* (呼び出し元が具体的な幅を決めてから呼ぶこと。ワイルドカード署名はしない)。
|
|
7
|
+
*
|
|
8
|
+
* 秘密鍵の取得はこの関数の責務外(引数で受け取るだけ)。
|
|
9
|
+
*/
|
|
10
|
+
export declare function signMediaUrl(params: SignMediaUrlParams): SignMediaUrlResult;
|
|
11
|
+
//# sourceMappingURL=sign.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sign.d.ts","sourceRoot":"","sources":["../../../src/server/media/sign.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAmBzE;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,CAqB3E"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* メディア配信用の署名付きURL生成(CloudFront Canned Policy)
|
|
3
|
+
*
|
|
4
|
+
* 「誰に署名URLを発行するか」の判断はこの関数の責務外(呼び出し側アプリが担う)。
|
|
5
|
+
* この関数は「有効なリクエストになら署名する」機構のみを提供する(詳細: docs/media-design.md)。
|
|
6
|
+
*/
|
|
7
|
+
import { getSignedUrl } from '@aws-sdk/cloudfront-signer';
|
|
8
|
+
/**
|
|
9
|
+
* Expires(epoch seconds)を計算する。
|
|
10
|
+
*
|
|
11
|
+
* 生の「現在時刻+TTL」をそのまま使うと、署名APIを呼ぶたびに毎回異なる署名URLになり
|
|
12
|
+
* クライアント側のブラウザキャッシュが効かない。TTL単位の境界に切り上げることで、
|
|
13
|
+
* 同じ時間帯内は同一URLになりブラウザキャッシュも活用できる。
|
|
14
|
+
*
|
|
15
|
+
* 必ず「現在時刻+TTLを切り上げる」のであって、「現在時刻を切り上げる」のではない
|
|
16
|
+
* ことに注意する。後者だと、TTL境界の直前に発行した場合に有効期間がTTLよりずっと
|
|
17
|
+
* 短くなってしまう(例: TTL=3600秒で境界の1秒前に発行すると有効期間は1秒になる)。
|
|
18
|
+
* この実装は常に [TTL, 2*TTL) の範囲の有効期間を保証する。
|
|
19
|
+
*/
|
|
20
|
+
function computeExpiry(nowSeconds, ttlSeconds) {
|
|
21
|
+
const target = nowSeconds + ttlSeconds;
|
|
22
|
+
return Math.ceil(target / ttlSeconds) * ttlSeconds;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* メディア配信用の署名付きURLを生成する。
|
|
26
|
+
*
|
|
27
|
+
* width指定時は /resize/{fileId}?width=N、省略時は /master/{fileId} に署名する
|
|
28
|
+
* (呼び出し元が具体的な幅を決めてから呼ぶこと。ワイルドカード署名はしない)。
|
|
29
|
+
*
|
|
30
|
+
* 秘密鍵の取得はこの関数の責務外(引数で受け取るだけ)。
|
|
31
|
+
*/
|
|
32
|
+
export function signMediaUrl(params) {
|
|
33
|
+
const { fileId, width, keyPairId, privateKey, baseUrl, expiresInSeconds } = params;
|
|
34
|
+
if (width !== undefined && width < 1) {
|
|
35
|
+
throw new Error(`Invalid width: ${width}`);
|
|
36
|
+
}
|
|
37
|
+
const path = width !== undefined ? `/resize/${fileId}?width=${width}` : `/master/${fileId}`;
|
|
38
|
+
const targetUrl = `${baseUrl.replace(/\/$/, '')}${path}`;
|
|
39
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
40
|
+
const expiresAt = computeExpiry(nowSeconds, expiresInSeconds);
|
|
41
|
+
const url = getSignedUrl({
|
|
42
|
+
url: targetUrl,
|
|
43
|
+
dateLessThan: new Date(expiresAt * 1000).toISOString(),
|
|
44
|
+
keyPairId,
|
|
45
|
+
privateKey,
|
|
46
|
+
});
|
|
47
|
+
return { fileId, width, url, expiresAt };
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=sign.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sign.js","sourceRoot":"","sources":["../../../src/server/media/sign.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAI1D;;;;;;;;;;;GAWG;AACH,SAAS,aAAa,CAAC,UAAkB,EAAE,UAAkB;IAC3D,MAAM,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;IACvC,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,GAAG,UAAU,CAAC;AACrD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,MAA0B;IACrD,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAAC;IAEnF,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,kBAAkB,KAAK,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,MAAM,UAAU,KAAK,EAAE,CAAC,CAAC,CAAC,WAAW,MAAM,EAAE,CAAC;IAC5F,MAAM,SAAS,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC;IAEzD,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IACjD,MAAM,SAAS,GAAG,aAAa,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAC;IAE9D,MAAM,GAAG,GAAG,YAAY,CAAC;QACvB,GAAG,EAAE,SAAS;QACd,YAAY,EAAE,IAAI,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE;QACtD,SAAS;QACT,UAAU;KACX,CAAC,CAAC;IAEH,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;AAC3C,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* image policy・署名鍵をParameter Storeから取得する共通ロジック
|
|
3
|
+
*
|
|
4
|
+
* モジュールスコープにキャッシュし、コールドスタートごとに再取得する
|
|
5
|
+
* (詳細: docs/media-design.md「image policyの管理」)。
|
|
6
|
+
*
|
|
7
|
+
* 取得失敗時(未登録・不正なJSON)はfail-closedとする(例外を投げる)。
|
|
8
|
+
* 「取得できない=無制限を許可」になる実装ミスを避けるため、呼び出し側は
|
|
9
|
+
* この例外をそのままリクエスト拒否として扱うこと。
|
|
10
|
+
*/
|
|
11
|
+
import { type SSMClient } from '@aws-sdk/client-ssm';
|
|
12
|
+
import type { ImagePolicy } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* image policyをParameter Storeから取得する(モジュールスコープにキャッシュ)。
|
|
15
|
+
*
|
|
16
|
+
* @throws パラメータが未登録、または値が不正なJSON/スキーマの場合
|
|
17
|
+
*/
|
|
18
|
+
export declare function getImagePolicy(ssmClient: SSMClient, paramName: string): Promise<ImagePolicy>;
|
|
19
|
+
/**
|
|
20
|
+
* 署名用秘密鍵をParameter Storeから取得する(モジュールスコープにキャッシュ)。
|
|
21
|
+
*
|
|
22
|
+
* @throws パラメータが未登録の場合
|
|
23
|
+
*/
|
|
24
|
+
export declare function getSigningPrivateKey(ssmClient: SSMClient, paramName: string): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* キャッシュをクリアする(テスト専用)。
|
|
27
|
+
*/
|
|
28
|
+
export declare function clearSsmParamsCache(): void;
|
|
29
|
+
//# sourceMappingURL=ssmParams.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssmParams.d.ts","sourceRoot":"","sources":["../../../src/server/media/ssmParams.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAE1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAkB9C;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,WAAW,CAAC,CAsBtB;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CAajB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAG1C"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* image policy・署名鍵をParameter Storeから取得する共通ロジック
|
|
3
|
+
*
|
|
4
|
+
* モジュールスコープにキャッシュし、コールドスタートごとに再取得する
|
|
5
|
+
* (詳細: docs/media-design.md「image policyの管理」)。
|
|
6
|
+
*
|
|
7
|
+
* 取得失敗時(未登録・不正なJSON)はfail-closedとする(例外を投げる)。
|
|
8
|
+
* 「取得できない=無制限を許可」になる実装ミスを避けるため、呼び出し側は
|
|
9
|
+
* この例外をそのままリクエスト拒否として扱うこと。
|
|
10
|
+
*/
|
|
11
|
+
import { GetParameterCommand } from '@aws-sdk/client-ssm';
|
|
12
|
+
let cachedImagePolicy;
|
|
13
|
+
let cachedSigningPrivateKey;
|
|
14
|
+
function isValidImagePolicy(value) {
|
|
15
|
+
if (typeof value !== 'object' || value === null)
|
|
16
|
+
return false;
|
|
17
|
+
const v = value;
|
|
18
|
+
return (typeof v.masterMaxDimension === 'number' &&
|
|
19
|
+
v.masterMaxDimension > 0 &&
|
|
20
|
+
Array.isArray(v.allowedContentTypes) &&
|
|
21
|
+
v.allowedContentTypes.every((t) => typeof t === 'string') &&
|
|
22
|
+
typeof v.maxUploadSize === 'number' &&
|
|
23
|
+
v.maxUploadSize > 0);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* image policyをParameter Storeから取得する(モジュールスコープにキャッシュ)。
|
|
27
|
+
*
|
|
28
|
+
* @throws パラメータが未登録、または値が不正なJSON/スキーマの場合
|
|
29
|
+
*/
|
|
30
|
+
export async function getImagePolicy(ssmClient, paramName) {
|
|
31
|
+
if (cachedImagePolicy)
|
|
32
|
+
return cachedImagePolicy;
|
|
33
|
+
const result = await ssmClient.send(new GetParameterCommand({ Name: paramName }));
|
|
34
|
+
const rawValue = result.Parameter?.Value;
|
|
35
|
+
if (!rawValue) {
|
|
36
|
+
throw new Error(`Image policy parameter not found: ${paramName}`);
|
|
37
|
+
}
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(rawValue);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error(`Image policy parameter is not valid JSON: ${paramName}`);
|
|
44
|
+
}
|
|
45
|
+
if (!isValidImagePolicy(parsed)) {
|
|
46
|
+
throw new Error(`Image policy parameter has invalid shape: ${paramName}`);
|
|
47
|
+
}
|
|
48
|
+
cachedImagePolicy = parsed;
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 署名用秘密鍵をParameter Storeから取得する(モジュールスコープにキャッシュ)。
|
|
53
|
+
*
|
|
54
|
+
* @throws パラメータが未登録の場合
|
|
55
|
+
*/
|
|
56
|
+
export async function getSigningPrivateKey(ssmClient, paramName) {
|
|
57
|
+
if (cachedSigningPrivateKey)
|
|
58
|
+
return cachedSigningPrivateKey;
|
|
59
|
+
const result = await ssmClient.send(new GetParameterCommand({ Name: paramName, WithDecryption: true }));
|
|
60
|
+
const value = result.Parameter?.Value;
|
|
61
|
+
if (!value) {
|
|
62
|
+
throw new Error(`Signing private key parameter not found: ${paramName}`);
|
|
63
|
+
}
|
|
64
|
+
cachedSigningPrivateKey = value;
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* キャッシュをクリアする(テスト専用)。
|
|
69
|
+
*/
|
|
70
|
+
export function clearSsmParamsCache() {
|
|
71
|
+
cachedImagePolicy = undefined;
|
|
72
|
+
cachedSigningPrivateKey = undefined;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=ssmParams.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssmParams.js","sourceRoot":"","sources":["../../../src/server/media/ssmParams.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,mBAAmB,EAAkB,MAAM,qBAAqB,CAAC;AAI1E,IAAI,iBAA0C,CAAC;AAC/C,IAAI,uBAA2C,CAAC;AAEhD,SAAS,kBAAkB,CAAC,KAAc;IACxC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,OAAO,CACL,OAAO,CAAC,CAAC,kBAAkB,KAAK,QAAQ;QACxC,CAAC,CAAC,kBAAkB,GAAG,CAAC;QACxB,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,mBAAmB,CAAC;QACpC,CAAC,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC;QACzD,OAAO,CAAC,CAAC,aAAa,KAAK,QAAQ;QACnC,CAAC,CAAC,aAAa,GAAG,CAAC,CACpB,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,SAAoB,EACpB,SAAiB;IAEjB,IAAI,iBAAiB;QAAE,OAAO,iBAAiB,CAAC;IAEhD,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,mBAAmB,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;IAClF,MAAM,QAAQ,GAAG,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC;IACzC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,qCAAqC,SAAS,EAAE,CAAC,CAAC;IACpE,CAAC;IAED,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,6CAA6C,SAAS,EAAE,CAAC,CAAC;IAC5E,CAAC;IAED,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,6CAA6C,SAAS,EAAE,CAAC,CAAC;IAC5E,CAAC;IAED,iBAAiB,GAAG,MAAM,CAAC;IAC3B,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,SAAoB,EACpB,SAAiB;IAEjB,IAAI,uBAAuB;QAAE,OAAO,uBAAuB,CAAC;IAE5D,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,IAAI,CACjC,IAAI,mBAAmB,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CACnE,CAAC;IACF,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC;IACtC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,4CAA4C,SAAS,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED,uBAAuB,GAAG,KAAK,CAAC;IAChC,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB;IACjC,iBAAiB,GAAG,SAAS,CAAC;IAC9B,uBAAuB,GAAG,SAAS,CAAC;AACtC,CAAC"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* メディア機能の型定義
|
|
3
|
+
*
|
|
4
|
+
* 詳細設計: docs/media-design.md
|
|
5
|
+
*/
|
|
6
|
+
import type { S3Client } from '@aws-sdk/client-s3';
|
|
7
|
+
/**
|
|
8
|
+
* メディアレコードの処理状態
|
|
9
|
+
*
|
|
10
|
+
* `pending`は存在しない。レコードが無ければ処理中を意味する
|
|
11
|
+
* (presign発行時点ではDynamoDBに触れない設計のため)。
|
|
12
|
+
*/
|
|
13
|
+
export type MediaStatus = 'completed' | 'failed';
|
|
14
|
+
/**
|
|
15
|
+
* メディアレコード(共通フィールドのみ)
|
|
16
|
+
*
|
|
17
|
+
* アプリ固有の追加フィールドは、process-handlerがS3オブジェクトメタデータから
|
|
18
|
+
* 読み取った値をそのまま書き込む(本型はフィールド名を規定しない)。
|
|
19
|
+
*/
|
|
20
|
+
export interface MediaRecord {
|
|
21
|
+
/** fileId(ULID) */
|
|
22
|
+
id: string;
|
|
23
|
+
status: MediaStatus;
|
|
24
|
+
/** アップロード時に申告され、実バイトと照合済みのMIMEタイプ */
|
|
25
|
+
contentType: string;
|
|
26
|
+
/**
|
|
27
|
+
* masterとして実際に保存される形式(画像なら'image/jpeg'、それ以外はcontentTypeと同じ)。
|
|
28
|
+
* contentTypeとの不一致を明示的に区別する
|
|
29
|
+
*/
|
|
30
|
+
outputContentType: string;
|
|
31
|
+
originalFilename?: string;
|
|
32
|
+
/** master(画像の場合のみ)の寸法 */
|
|
33
|
+
dimensions?: {
|
|
34
|
+
width: number;
|
|
35
|
+
height: number;
|
|
36
|
+
};
|
|
37
|
+
/** masterのファイルサイズ(バイト) */
|
|
38
|
+
size?: number;
|
|
39
|
+
error?: string;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
updatedAt: string;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* image policy(Parameter Store経由で実行時取得するJSON)
|
|
46
|
+
*
|
|
47
|
+
* 呼び出し側のpresign発行処理(allowedContentTypes/maxUploadSize)、
|
|
48
|
+
* process-handler(masterMaxDimension)、
|
|
49
|
+
* media-handler(widthの健全性チェック上限として masterMaxDimension を流用)が参照する。
|
|
50
|
+
*
|
|
51
|
+
* 下限は固定 `1`(独立フィールドは持たない。署名付きURL必須のため、
|
|
52
|
+
* 健全性チェック以外の制約は不要という設計判断。詳細: docs/media-design.md)。
|
|
53
|
+
*/
|
|
54
|
+
export interface ImagePolicy {
|
|
55
|
+
/** process-handlerがmaster生成時に使う長辺上限。media-handlerのwidth上限チェックにも流用する */
|
|
56
|
+
masterMaxDimension: number;
|
|
57
|
+
/** presign発行時に使う許可MIMEタイプ */
|
|
58
|
+
allowedContentTypes: string[];
|
|
59
|
+
/** presign発行時に使う最大アップロードサイズ(バイト) */
|
|
60
|
+
maxUploadSize: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* generatePresignedUpload の引数
|
|
64
|
+
*/
|
|
65
|
+
export interface PresignedUploadParams {
|
|
66
|
+
s3Client: S3Client;
|
|
67
|
+
bucket: string;
|
|
68
|
+
contentType: string;
|
|
69
|
+
originalFilename?: string;
|
|
70
|
+
fileSize: number;
|
|
71
|
+
allowedContentTypes: string[];
|
|
72
|
+
maxUploadSize: number;
|
|
73
|
+
/** x-amz-meta-{key} としてpolicyのeq条件に固定するアプリ固有メタデータ */
|
|
74
|
+
metadata: Record<string, string>;
|
|
75
|
+
/** presigned POSTの有効期限(秒)。省略時は30秒(presign.ts参照) */
|
|
76
|
+
expiresInSeconds?: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* generatePresignedUpload の戻り値
|
|
80
|
+
*/
|
|
81
|
+
export interface PresignedUploadResult {
|
|
82
|
+
fileId: string;
|
|
83
|
+
uploadUrl: string;
|
|
84
|
+
fields: Record<string, string>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* signMediaUrl の引数
|
|
88
|
+
*/
|
|
89
|
+
export interface SignMediaUrlParams {
|
|
90
|
+
fileId: string;
|
|
91
|
+
/** 指定時は /resize/{fileId}?width=N、省略時は /master/{fileId} に署名する */
|
|
92
|
+
width?: number;
|
|
93
|
+
keyPairId: string;
|
|
94
|
+
privateKey: string;
|
|
95
|
+
baseUrl: string;
|
|
96
|
+
/** 有効期限(秒)。Expiresはこの粒度に切り上げてから署名する */
|
|
97
|
+
expiresInSeconds: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* signMediaUrl の戻り値
|
|
101
|
+
*/
|
|
102
|
+
export interface SignMediaUrlResult {
|
|
103
|
+
fileId: string;
|
|
104
|
+
width?: number;
|
|
105
|
+
url: string;
|
|
106
|
+
/** 署名に使ったExpires(epoch seconds、丸め込み後) */
|
|
107
|
+
expiresAt: number;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/server/media/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,mBAAmB;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,WAAW,CAAC;IACpB,qCAAqC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yBAAyB;IACzB,UAAU,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,0BAA0B;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAW;IAC1B,uEAAuE;IACvE,kBAAkB,EAAE,MAAM,CAAC;IAC3B,6BAA6B;IAC7B,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,oCAAoC;IACpC,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;CACnB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/server/media/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* presign発行・署名付きURL発行を扱うLambda Function URL用ハンドラー(標準実装)
|
|
3
|
+
*
|
|
4
|
+
* 「誰にpresign/署名URLを発行してよいか」の判断はこのライブラリの関心事ではない
|
|
5
|
+
* (詳細: docs/media-design.md)。このハンドラーは`authorize`フックを通じて
|
|
6
|
+
* 呼び出し側アプリの認証・認可ロジックを注入できるようにする。
|
|
7
|
+
*
|
|
8
|
+
* より高度な認可(例: リソース単位の権限チェック)が必要な場合、呼び出し側は
|
|
9
|
+
* このハンドラーを使わず、`./presign.js`・`./sign.js`を自前のルーティング・
|
|
10
|
+
* 認可レイヤーから直接呼び出すこともできる。
|
|
11
|
+
*/
|
|
12
|
+
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
|
|
13
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
14
|
+
import { SSMClient } from '@aws-sdk/client-ssm';
|
|
15
|
+
/**
|
|
16
|
+
* リクエストを認可し、presign発行時にpolicyへ固定するメタデータを返す。
|
|
17
|
+
* 認可できない場合はnullを返す(401を返す)。
|
|
18
|
+
*/
|
|
19
|
+
export type AuthorizeFn = (event: APIGatewayProxyEventV2) => Promise<Record<string, string> | null>;
|
|
20
|
+
export interface UploadHandlerConfig {
|
|
21
|
+
bucket: string;
|
|
22
|
+
imagePolicyParam: string;
|
|
23
|
+
signingPrivateKeyParam: string;
|
|
24
|
+
keyPairId: string;
|
|
25
|
+
baseUrl: string;
|
|
26
|
+
authorize: AuthorizeFn;
|
|
27
|
+
signExpiresInSeconds?: number;
|
|
28
|
+
s3Client?: S3Client;
|
|
29
|
+
ssmClient?: SSMClient;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 設定を注入してLambda Function URLハンドラーを生成する。
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* export const handler = createUploadHandler({
|
|
37
|
+
* bucket: process.env.MEDIA_BUCKET!,
|
|
38
|
+
* imagePolicyParam: process.env.IMAGE_POLICY_PARAM!,
|
|
39
|
+
* signingPrivateKeyParam: process.env.SIGNING_PRIVATE_KEY_PARAM!,
|
|
40
|
+
* keyPairId: process.env.CLOUDFRONT_KEY_PAIR_ID!,
|
|
41
|
+
* baseUrl: process.env.MEDIA_BASE_URL!,
|
|
42
|
+
* authorize: async (event) => {
|
|
43
|
+
* const userId = await verifyMyAppJwt(event.headers.authorization);
|
|
44
|
+
* return userId ? { ownerId: userId } : null;
|
|
45
|
+
* },
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function createUploadHandler(config: UploadHandlerConfig): (event: APIGatewayProxyEventV2) => Promise<APIGatewayProxyStructuredResultV2>;
|
|
50
|
+
//# sourceMappingURL=upload-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-handler.d.ts","sourceRoot":"","sources":["../../../src/server/media/upload-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,sBAAsB,EAAE,iCAAiC,EAAE,MAAM,YAAY,CAAC;AAE5F,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAWhD;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,KAAK,EAAE,sBAAsB,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;AAEpG,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,WAAW,CAAC;IACvB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AA0FD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,mBAAmB,GAC1B,CAAC,KAAK,EAAE,sBAAsB,KAAK,OAAO,CAAC,iCAAiC,CAAC,CAiC/E"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
2
|
+
import { SSMClient } from '@aws-sdk/client-ssm';
|
|
3
|
+
import { createLogger } from '../../shared/index.js';
|
|
4
|
+
import { generatePresignedUpload } from './presign.js';
|
|
5
|
+
import { signMediaUrl } from './sign.js';
|
|
6
|
+
import { getImagePolicy, getSigningPrivateKey } from './ssmParams.js';
|
|
7
|
+
const logger = createLogger({ service: 'media-upload-handler' });
|
|
8
|
+
const DEFAULT_SIGN_EXPIRES_IN_SECONDS = 3600;
|
|
9
|
+
function jsonResponse(statusCode, body) {
|
|
10
|
+
return {
|
|
11
|
+
statusCode,
|
|
12
|
+
headers: { 'Content-Type': 'application/json' },
|
|
13
|
+
body: JSON.stringify(body),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function parseBody(event) {
|
|
17
|
+
if (!event.body)
|
|
18
|
+
return {};
|
|
19
|
+
const raw = event.isBase64Encoded
|
|
20
|
+
? Buffer.from(event.body, 'base64').toString('utf-8')
|
|
21
|
+
: event.body;
|
|
22
|
+
return JSON.parse(raw);
|
|
23
|
+
}
|
|
24
|
+
async function handlePresign(event, config, s3Client, ssmClient, metadata, requestId) {
|
|
25
|
+
const body = parseBody(event);
|
|
26
|
+
if (!body.contentType || !body.fileSize) {
|
|
27
|
+
return jsonResponse(400, { error: 'contentType and fileSize are required' });
|
|
28
|
+
}
|
|
29
|
+
const imagePolicy = await getImagePolicy(ssmClient, config.imagePolicyParam);
|
|
30
|
+
const result = await generatePresignedUpload({
|
|
31
|
+
s3Client,
|
|
32
|
+
bucket: config.bucket,
|
|
33
|
+
contentType: body.contentType,
|
|
34
|
+
originalFilename: body.originalFilename,
|
|
35
|
+
fileSize: body.fileSize,
|
|
36
|
+
allowedContentTypes: imagePolicy.allowedContentTypes,
|
|
37
|
+
maxUploadSize: imagePolicy.maxUploadSize,
|
|
38
|
+
metadata,
|
|
39
|
+
});
|
|
40
|
+
logger.info('Presign issued', { requestId, fileId: result.fileId });
|
|
41
|
+
return jsonResponse(200, result);
|
|
42
|
+
}
|
|
43
|
+
async function handleSign(event, config, ssmClient, requestId) {
|
|
44
|
+
const body = parseBody(event);
|
|
45
|
+
if (!Array.isArray(body.items) || body.items.length === 0) {
|
|
46
|
+
return jsonResponse(400, { error: 'items is required and must be non-empty' });
|
|
47
|
+
}
|
|
48
|
+
const privateKey = await getSigningPrivateKey(ssmClient, config.signingPrivateKeyParam);
|
|
49
|
+
const expiresInSeconds = config.signExpiresInSeconds ?? DEFAULT_SIGN_EXPIRES_IN_SECONDS;
|
|
50
|
+
const items = body.items.map((item) => {
|
|
51
|
+
if (!item.fileId) {
|
|
52
|
+
throw new Error('Each item must have fileId');
|
|
53
|
+
}
|
|
54
|
+
return signMediaUrl({
|
|
55
|
+
fileId: item.fileId,
|
|
56
|
+
width: item.width,
|
|
57
|
+
keyPairId: config.keyPairId,
|
|
58
|
+
privateKey,
|
|
59
|
+
baseUrl: config.baseUrl,
|
|
60
|
+
expiresInSeconds,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
logger.info('Media URLs signed', { requestId, count: items.length });
|
|
64
|
+
return jsonResponse(200, { items });
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 設定を注入してLambda Function URLハンドラーを生成する。
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* export const handler = createUploadHandler({
|
|
72
|
+
* bucket: process.env.MEDIA_BUCKET!,
|
|
73
|
+
* imagePolicyParam: process.env.IMAGE_POLICY_PARAM!,
|
|
74
|
+
* signingPrivateKeyParam: process.env.SIGNING_PRIVATE_KEY_PARAM!,
|
|
75
|
+
* keyPairId: process.env.CLOUDFRONT_KEY_PAIR_ID!,
|
|
76
|
+
* baseUrl: process.env.MEDIA_BASE_URL!,
|
|
77
|
+
* authorize: async (event) => {
|
|
78
|
+
* const userId = await verifyMyAppJwt(event.headers.authorization);
|
|
79
|
+
* return userId ? { ownerId: userId } : null;
|
|
80
|
+
* },
|
|
81
|
+
* });
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function createUploadHandler(config) {
|
|
85
|
+
const s3Client = config.s3Client ?? new S3Client({ region: process.env.AWS_REGION });
|
|
86
|
+
const ssmClient = config.ssmClient ?? new SSMClient({ region: process.env.AWS_REGION });
|
|
87
|
+
return async (event) => {
|
|
88
|
+
const requestId = event.requestContext.requestId;
|
|
89
|
+
const method = event.requestContext.http.method;
|
|
90
|
+
const path = event.rawPath;
|
|
91
|
+
try {
|
|
92
|
+
const metadata = await config.authorize(event);
|
|
93
|
+
if (!metadata) {
|
|
94
|
+
return jsonResponse(401, { error: 'Unauthorized' });
|
|
95
|
+
}
|
|
96
|
+
if (method === 'POST' && path.endsWith('/presign')) {
|
|
97
|
+
return await handlePresign(event, config, s3Client, ssmClient, metadata, requestId);
|
|
98
|
+
}
|
|
99
|
+
if (method === 'POST' && path.endsWith('/sign')) {
|
|
100
|
+
return await handleSign(event, config, ssmClient, requestId);
|
|
101
|
+
}
|
|
102
|
+
return jsonResponse(404, { error: 'Not found' });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
logger.error('Upload handler error', {
|
|
106
|
+
requestId,
|
|
107
|
+
error: error instanceof Error ? error.message : String(error),
|
|
108
|
+
});
|
|
109
|
+
return jsonResponse(400, {
|
|
110
|
+
error: error instanceof Error ? error.message : 'Bad request',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=upload-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-handler.js","sourceRoot":"","sources":["../../../src/server/media/upload-handler.ts"],"names":[],"mappings":"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"}
|