@exabugs/dynamodb-client 1.6.1 → 1.7.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 +29 -0
- package/README.md +1 -0
- package/dist/server/handler.cjs +133 -50
- package/dist/server/handler.cjs.map +4 -4
- package/dist/server/media-handler.cjs +2 -2
- package/dist/server/media-process-handler-dlq.cjs +131 -53
- package/dist/server/media-process-handler-dlq.cjs.map +4 -4
- package/dist/server/media-process-handler.cjs +131 -53
- package/dist/server/media-process-handler.cjs.map +4 -4
- package/dist/server/operations/find/utils.js +2 -2
- package/dist/server/operations/find/utils.js.map +1 -1
- package/dist/server/operations/updateMany.d.ts.map +1 -1
- package/dist/server/operations/updateMany.js +6 -76
- package/dist/server/operations/updateMany.js.map +1 -1
- package/dist/server/operations/updateOne.d.ts.map +1 -1
- package/dist/server/operations/updateOne.js +9 -1
- package/dist/server/operations/updateOne.js.map +1 -1
- package/dist/server/utils/auth.d.ts.map +1 -1
- package/dist/server/utils/auth.js +8 -0
- package/dist/server/utils/auth.js.map +1 -1
- package/dist/server/utils/authHandler.js +1 -1
- package/dist/server/utils/authHandler.js.map +1 -1
- package/dist/server/utils/mergePatch.d.ts +8 -0
- package/dist/server/utils/mergePatch.d.ts.map +1 -0
- package/dist/server/utils/mergePatch.js +127 -0
- package/dist/server/utils/mergePatch.js.map +1 -0
- package/dist/server/utils/timestamps.d.ts.map +1 -1
- package/dist/server/utils/timestamps.js +2 -1
- package/dist/server/utils/timestamps.js.map +1 -1
- package/dist/sharp-layer.zip +0 -0
- package/package.json +1 -1
- package/terraform/README.md +5 -0
- package/terraform/variables.tf +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.7.0] - 2026-10-12
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- 更新のドット記法パス(例: `sections.1.t`)が配列をオブジェクトに壊していた。配列は要素単位で更新し、範囲外・非数値の添字は 400 で拒否する。`__proto__` を含むパスも拒否する (#48)
|
|
15
|
+
- 更新時に `updatedAt` が変わらなかった(既存の値が優先されていた)。パッチ側に付与して毎回更新する。クライアントが値を指定した場合はそれを尊重し、`null` / `undefined` は未指定として扱う (#48)
|
|
16
|
+
- 解釈できないフィルタ指定が 500 (`CONFIG_ERROR`) になっていた。400 (`INVALID_FILTER`) を返す (#48)
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **`updatedAt` を明示しない更新で、値が毎回変わる**ようになった。`updatedAt` 順の一覧は更新のたびに並びが変わり、`updatedAt` のシャドーの再作成により 1 レコードあたりの書き込み項目が増える
|
|
21
|
+
- ドット記法で、存在しない(またはオブジェクト以外の)親に対する `null`(削除)は何もしなくなった(従来は `{a: null}` のような残骸が残っていた)
|
|
22
|
+
|
|
23
|
+
## [1.6.2] - 2026-10-11
|
|
24
|
+
|
|
25
|
+
### Security
|
|
26
|
+
|
|
27
|
+
- `COGNITO_CLIENT_ID` 未設定のとき、JWT の `aud` 検証が無効であることを示す**警告ログ**を出す(コールドスタートごとに1回)。挙動は変えない。未設定だと同じユーザープールの別アプリクライアントのトークンも通る
|
|
28
|
+
- デバッグログ(`Cognito JWT verified`)から `email` を除いた(個人情報。追跡は `sub`)
|
|
29
|
+
|
|
30
|
+
### CI
|
|
31
|
+
|
|
32
|
+
- リポジトリを非公開にしたため、`npm publish` から `--provenance` を外した(provenance は公開リポジトリでのみ生成できる)。公開リポジトリに戻す場合は復活させる
|
|
33
|
+
|
|
34
|
+
### Docs
|
|
35
|
+
|
|
36
|
+
- `docs/AUTHORIZATION.md` を新設: 認可モデル(JWT の有効性しか検証しない)、管理者専用ユーザープール、フェデレーションログインの許可制(Pre Token Generation の例)、`aud` 検証、MFA の限界、チェックリスト
|
|
37
|
+
- `terraform/README.md`: pnpm 利用時、版を上げた後は `.terraform/modules` を削除して `terraform init` する注意を追加。`terraform/variables.tf` の `cognito_client_id` の説明を明確化
|
|
38
|
+
|
|
10
39
|
## [1.6.1] - 2026-10-11
|
|
11
40
|
|
|
12
41
|
### Fixed
|
package/README.md
CHANGED
|
@@ -194,6 +194,7 @@ const article = await articles.findOne({ title: 'Hello DynamoDB' });
|
|
|
194
194
|
- **[Architecture](docs/ARCHITECTURE.md)** - System architecture and design
|
|
195
195
|
- **[Client Usage](docs/CLIENT_USAGE.md)** - Client-side API guide
|
|
196
196
|
- **[React Admin Integration](docs/react-admin-integration.md)** - Admin UI setup and many-to-many relationships
|
|
197
|
+
- **[Authorization & Secure Setup](docs/AUTHORIZATION.md)** - 認可モデルと安全な構成(必読。JWT の有効性しか検証しない/管理者専用ユーザープール/`aud` 検証)
|
|
197
198
|
- **[Deployment](docs/DEPLOYMENT.md)** - Production deployment guide
|
|
198
199
|
- **[Terraform Modules](terraform/README.md)** - Infrastructure as Code
|
|
199
200
|
|
package/dist/server/handler.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// @exabugs/dynamodb-client v1.
|
|
2
|
-
// Built: 2026-10-
|
|
1
|
+
// @exabugs/dynamodb-client v1.7.0
|
|
2
|
+
// Built: 2026-10-11T16:38:03.698Z
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
@@ -73,7 +73,7 @@ var init_types = __esm({
|
|
|
73
73
|
function isAppError(error2) {
|
|
74
74
|
return error2 instanceof AppError;
|
|
75
75
|
}
|
|
76
|
-
var AppError, AuthError, ConfigError, InvalidTokenError, ItemNotFoundError;
|
|
76
|
+
var AppError, AuthError, ConfigError, InvalidFilterError, InvalidTokenError, ItemNotFoundError;
|
|
77
77
|
var init_errors = __esm({
|
|
78
78
|
"src/shared/errors/index.ts"() {
|
|
79
79
|
"use strict";
|
|
@@ -117,6 +117,14 @@ var init_errors = __esm({
|
|
|
117
117
|
super("CONFIG_ERROR" /* CONFIG_ERROR */, message, 500, details);
|
|
118
118
|
}
|
|
119
119
|
};
|
|
120
|
+
InvalidFilterError = class extends AppError {
|
|
121
|
+
static {
|
|
122
|
+
__name(this, "InvalidFilterError");
|
|
123
|
+
}
|
|
124
|
+
constructor(message, details) {
|
|
125
|
+
super("INVALID_FILTER" /* INVALID_FILTER */, message, 400, details);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
120
128
|
InvalidTokenError = class extends AppError {
|
|
121
129
|
static {
|
|
122
130
|
__name(this, "InvalidTokenError");
|
|
@@ -34459,7 +34467,7 @@ function parseFilters(filter) {
|
|
|
34459
34467
|
fieldKey,
|
|
34460
34468
|
error: error2 instanceof Error ? error2.message : String(error2)
|
|
34461
34469
|
});
|
|
34462
|
-
throw new
|
|
34470
|
+
throw new InvalidFilterError(`Invalid filter field syntax: ${fieldKey}`, {
|
|
34463
34471
|
field: fieldKey,
|
|
34464
34472
|
error: error2 instanceof Error ? error2.message : String(error2)
|
|
34465
34473
|
});
|
|
@@ -35918,8 +35926,8 @@ function addUpdateTimestamp(data) {
|
|
|
35918
35926
|
}
|
|
35919
35927
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
35920
35928
|
return {
|
|
35921
|
-
|
|
35922
|
-
|
|
35929
|
+
...data,
|
|
35930
|
+
[timestampFields.updatedAt]: data[timestampFields.updatedAt] ?? now
|
|
35923
35931
|
};
|
|
35924
35932
|
}
|
|
35925
35933
|
var init_timestamps = __esm({
|
|
@@ -36219,51 +36227,118 @@ var init_insertMany = __esm({
|
|
|
36219
36227
|
}
|
|
36220
36228
|
});
|
|
36221
36229
|
|
|
36222
|
-
// src/server/
|
|
36223
|
-
|
|
36224
|
-
|
|
36225
|
-
|
|
36226
|
-
|
|
36227
|
-
|
|
36228
|
-
|
|
36230
|
+
// src/server/utils/mergePatch.ts
|
|
36231
|
+
function isPlainObject(value) {
|
|
36232
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36233
|
+
}
|
|
36234
|
+
function invalidPath(path, reason) {
|
|
36235
|
+
return new AppError("VALIDATION_ERROR" /* VALIDATION_ERROR */, `Invalid patch path "${path}": ${reason}`, 400, {
|
|
36236
|
+
path
|
|
36237
|
+
});
|
|
36238
|
+
}
|
|
36239
|
+
function mergeInto(target, patch) {
|
|
36229
36240
|
for (const [key, value] of Object.entries(patch)) {
|
|
36230
36241
|
if (key.includes(".")) {
|
|
36231
|
-
|
|
36232
|
-
let current = result;
|
|
36233
|
-
for (let i4 = 0; i4 < keys.length - 1; i4++) {
|
|
36234
|
-
const k4 = keys[i4];
|
|
36235
|
-
if (!(k4 in current)) {
|
|
36236
|
-
current[k4] = {};
|
|
36237
|
-
} else if (typeof current[k4] !== "object" || Array.isArray(current[k4])) {
|
|
36238
|
-
current[k4] = {};
|
|
36239
|
-
}
|
|
36240
|
-
current = current[k4];
|
|
36241
|
-
}
|
|
36242
|
-
const lastKey = keys[keys.length - 1];
|
|
36243
|
-
current[lastKey] = value;
|
|
36242
|
+
setByPath(target, key, value);
|
|
36244
36243
|
} else {
|
|
36245
|
-
|
|
36244
|
+
setKey(target, key, value, key);
|
|
36246
36245
|
}
|
|
36247
36246
|
}
|
|
36248
|
-
return result;
|
|
36249
36247
|
}
|
|
36250
|
-
function
|
|
36251
|
-
|
|
36252
|
-
|
|
36253
|
-
|
|
36254
|
-
|
|
36255
|
-
|
|
36256
|
-
|
|
36257
|
-
|
|
36258
|
-
|
|
36259
|
-
|
|
36260
|
-
|
|
36248
|
+
function setKey(target, key, value, path) {
|
|
36249
|
+
if (key === "__proto__") {
|
|
36250
|
+
throw invalidPath(path, "forbidden segment");
|
|
36251
|
+
}
|
|
36252
|
+
if (value === null) {
|
|
36253
|
+
delete target[key];
|
|
36254
|
+
} else if (isPlainObject(value) && isPlainObject(target[key])) {
|
|
36255
|
+
mergeInto(target[key], value);
|
|
36256
|
+
} else {
|
|
36257
|
+
target[key] = value;
|
|
36258
|
+
}
|
|
36259
|
+
}
|
|
36260
|
+
function setByPath(target, path, value) {
|
|
36261
|
+
const keys = path.split(".");
|
|
36262
|
+
if (keys.some((k4) => k4 === "")) {
|
|
36263
|
+
throw invalidPath(path, "empty segment");
|
|
36264
|
+
}
|
|
36265
|
+
let current = target;
|
|
36266
|
+
for (let i4 = 0; i4 < keys.length; i4++) {
|
|
36267
|
+
const key = keys[i4];
|
|
36268
|
+
const isLast = i4 === keys.length - 1;
|
|
36269
|
+
if (Array.isArray(current)) {
|
|
36270
|
+
if (!/^(0|[1-9]\d*)$/.test(key) || Number(key) >= current.length) {
|
|
36271
|
+
throw invalidPath(path, `"${key}" is not a valid index (length ${current.length})`);
|
|
36272
|
+
}
|
|
36273
|
+
const index = Number(key);
|
|
36274
|
+
if (isLast) {
|
|
36275
|
+
if (value === null) {
|
|
36276
|
+
throw invalidPath(path, "array elements cannot be deleted by path");
|
|
36277
|
+
}
|
|
36278
|
+
const existing = current[index];
|
|
36279
|
+
if (isPlainObject(value) && isPlainObject(existing)) {
|
|
36280
|
+
mergeInto(existing, value);
|
|
36281
|
+
} else {
|
|
36282
|
+
current[index] = value;
|
|
36283
|
+
}
|
|
36284
|
+
return;
|
|
36285
|
+
}
|
|
36286
|
+
const next2 = current[index];
|
|
36287
|
+
if (isPlainObject(next2) || Array.isArray(next2)) {
|
|
36288
|
+
current = next2;
|
|
36289
|
+
} else {
|
|
36290
|
+
if (value === null) {
|
|
36291
|
+
return;
|
|
36292
|
+
}
|
|
36293
|
+
const created = {};
|
|
36294
|
+
current[index] = created;
|
|
36295
|
+
current = created;
|
|
36296
|
+
}
|
|
36297
|
+
continue;
|
|
36298
|
+
}
|
|
36299
|
+
if (isLast) {
|
|
36300
|
+
setKey(current, key, value, path);
|
|
36301
|
+
return;
|
|
36302
|
+
}
|
|
36303
|
+
if (key === "__proto__") {
|
|
36304
|
+
throw invalidPath(path, "forbidden segment");
|
|
36305
|
+
}
|
|
36306
|
+
const next = current[key];
|
|
36307
|
+
if (isPlainObject(next) || Array.isArray(next)) {
|
|
36308
|
+
current = next;
|
|
36261
36309
|
} else {
|
|
36262
|
-
|
|
36310
|
+
if (value === null) {
|
|
36311
|
+
return;
|
|
36312
|
+
}
|
|
36313
|
+
const created = {};
|
|
36314
|
+
current[key] = created;
|
|
36315
|
+
current = created;
|
|
36263
36316
|
}
|
|
36264
36317
|
}
|
|
36318
|
+
}
|
|
36319
|
+
function applyJsonMergePatch(target, patch) {
|
|
36320
|
+
const result = structuredClone(target);
|
|
36321
|
+
mergeInto(result, structuredClone(patch));
|
|
36265
36322
|
return result;
|
|
36266
36323
|
}
|
|
36324
|
+
var init_mergePatch = __esm({
|
|
36325
|
+
"src/server/utils/mergePatch.ts"() {
|
|
36326
|
+
"use strict";
|
|
36327
|
+
init_shared();
|
|
36328
|
+
__name(isPlainObject, "isPlainObject");
|
|
36329
|
+
__name(invalidPath, "invalidPath");
|
|
36330
|
+
__name(mergeInto, "mergeInto");
|
|
36331
|
+
__name(setKey, "setKey");
|
|
36332
|
+
__name(setByPath, "setByPath");
|
|
36333
|
+
__name(applyJsonMergePatch, "applyJsonMergePatch");
|
|
36334
|
+
}
|
|
36335
|
+
});
|
|
36336
|
+
|
|
36337
|
+
// src/server/operations/updateMany.ts
|
|
36338
|
+
var updateMany_exports = {};
|
|
36339
|
+
__export(updateMany_exports, {
|
|
36340
|
+
handleUpdateMany: () => handleUpdateMany
|
|
36341
|
+
});
|
|
36267
36342
|
async function handleUpdateMany(resource, params, requestId) {
|
|
36268
36343
|
const { data: patchData, options } = params;
|
|
36269
36344
|
const upsert = options?.upsert ?? false;
|
|
@@ -36391,11 +36466,9 @@ async function handleUpdateMany(resource, params, requestId) {
|
|
|
36391
36466
|
const id = existingData.id;
|
|
36392
36467
|
const oldShadowKeys = item.__shadowKeys ?? existingData.__shadowKeys ?? [];
|
|
36393
36468
|
const actualPatchData2 = patchData.$set ? patchData.$set : patchData;
|
|
36394
|
-
const
|
|
36395
|
-
const
|
|
36396
|
-
|
|
36397
|
-
id
|
|
36398
|
-
});
|
|
36469
|
+
const timestampedPatch = addUpdateTimestamp(actualPatchData2);
|
|
36470
|
+
const mergedData = applyJsonMergePatch(removeShadowKeys(existingData), timestampedPatch);
|
|
36471
|
+
const updatedData = { ...mergedData, id };
|
|
36399
36472
|
const newShadowRecords = generateShadowRecords(updatedData, resource, shadowConfig);
|
|
36400
36473
|
const newShadowKeys = newShadowRecords.map((shadow) => shadow.SK);
|
|
36401
36474
|
const mainSK = generateMainRecordSK2(id);
|
|
@@ -36676,10 +36749,9 @@ var init_updateMany = __esm({
|
|
|
36676
36749
|
init_chunking();
|
|
36677
36750
|
init_cost_tracker();
|
|
36678
36751
|
init_dynamodb3();
|
|
36752
|
+
init_mergePatch();
|
|
36679
36753
|
init_timestamps();
|
|
36680
36754
|
logger18 = createLogger({ service: "records-lambda" });
|
|
36681
|
-
__name(expandDotNotation, "expandDotNotation");
|
|
36682
|
-
__name(applyJsonMergePatch, "applyJsonMergePatch");
|
|
36683
36755
|
__name(handleUpdateMany, "handleUpdateMany");
|
|
36684
36756
|
__name(getPreparationErrorCode2, "getPreparationErrorCode");
|
|
36685
36757
|
}
|
|
@@ -37115,6 +37187,9 @@ async function handleUpdateOne(resource, params, requestId) {
|
|
|
37115
37187
|
if (updateManyResult.count === 0) {
|
|
37116
37188
|
const error2 = Object.values(updateManyResult.errors || {})[0];
|
|
37117
37189
|
if (error2) {
|
|
37190
|
+
if (error2.code === "VALIDATION_ERROR" /* VALIDATION_ERROR */) {
|
|
37191
|
+
throw new AppError("VALIDATION_ERROR" /* VALIDATION_ERROR */, error2.message ?? "Invalid patch", 400);
|
|
37192
|
+
}
|
|
37118
37193
|
throw new Error(`Failed to update record: ${error2.message}`);
|
|
37119
37194
|
} else {
|
|
37120
37195
|
throw new Error(`Failed to update record: ${targetId}`);
|
|
@@ -37154,6 +37229,9 @@ async function handleUpdateOne(resource, params, requestId) {
|
|
|
37154
37229
|
if (updateManyResult.count === 0) {
|
|
37155
37230
|
const error2 = Object.values(updateManyResult.errors || {})[0];
|
|
37156
37231
|
if (error2) {
|
|
37232
|
+
if (error2.code === "VALIDATION_ERROR" /* VALIDATION_ERROR */) {
|
|
37233
|
+
throw new AppError("VALIDATION_ERROR" /* VALIDATION_ERROR */, error2.message ?? "Invalid patch", 400);
|
|
37234
|
+
}
|
|
37157
37235
|
throw new Error(`Failed to update record: ${error2.message}`);
|
|
37158
37236
|
} else {
|
|
37159
37237
|
throw new Error(`No records found matching filter`);
|
|
@@ -38417,6 +38495,12 @@ var verifierInstance = null;
|
|
|
38417
38495
|
function getVerifier(userPoolId, clientId) {
|
|
38418
38496
|
if (!verifierInstance) {
|
|
38419
38497
|
logger21.debug("Creating JWT verifier instance", { userPoolId, clientId });
|
|
38498
|
+
if (!clientId) {
|
|
38499
|
+
logger21.warn(
|
|
38500
|
+
"COGNITO_CLIENT_ID is not set: JWT audience (aud) verification is DISABLED. Tokens issued to ANY app client of this user pool are accepted. Set cognito_client_id (Terraform) / COGNITO_CLIENT_ID in production. See docs/AUTHORIZATION.md",
|
|
38501
|
+
{ userPoolId }
|
|
38502
|
+
);
|
|
38503
|
+
}
|
|
38420
38504
|
verifierInstance = CognitoJwtVerifier.create({
|
|
38421
38505
|
userPoolId,
|
|
38422
38506
|
tokenUse: "id",
|
|
@@ -38513,8 +38597,7 @@ async function handleCognitoAuthentication(authHeader, requestId) {
|
|
|
38513
38597
|
const jwtPayload = await verifyAuthHeader(authHeader, userPoolId, clientId);
|
|
38514
38598
|
logger22.debug("Cognito JWT verified", {
|
|
38515
38599
|
requestId,
|
|
38516
|
-
sub: jwtPayload.sub
|
|
38517
|
-
email: jwtPayload.email
|
|
38600
|
+
sub: jwtPayload.sub
|
|
38518
38601
|
});
|
|
38519
38602
|
}
|
|
38520
38603
|
__name(handleCognitoAuthentication, "handleCognitoAuthentication");
|
|
@@ -38701,7 +38784,7 @@ async function handler(event) {
|
|
|
38701
38784
|
return createCorsResponse(HTTP_STATUS.OK);
|
|
38702
38785
|
}
|
|
38703
38786
|
if (event.requestContext.http.method === "GET" && event.requestContext.http.path === "/version") {
|
|
38704
|
-
const version = "1.
|
|
38787
|
+
const version = "1.7.0";
|
|
38705
38788
|
return createSuccessResponse({ version, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, requestId);
|
|
38706
38789
|
}
|
|
38707
38790
|
if (event.requestContext.http.method !== "POST") {
|