@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -0
  3. package/dist/server/handler.cjs +133 -50
  4. package/dist/server/handler.cjs.map +4 -4
  5. package/dist/server/media-handler.cjs +2 -2
  6. package/dist/server/media-process-handler-dlq.cjs +131 -53
  7. package/dist/server/media-process-handler-dlq.cjs.map +4 -4
  8. package/dist/server/media-process-handler.cjs +131 -53
  9. package/dist/server/media-process-handler.cjs.map +4 -4
  10. package/dist/server/operations/find/utils.js +2 -2
  11. package/dist/server/operations/find/utils.js.map +1 -1
  12. package/dist/server/operations/updateMany.d.ts.map +1 -1
  13. package/dist/server/operations/updateMany.js +6 -76
  14. package/dist/server/operations/updateMany.js.map +1 -1
  15. package/dist/server/operations/updateOne.d.ts.map +1 -1
  16. package/dist/server/operations/updateOne.js +9 -1
  17. package/dist/server/operations/updateOne.js.map +1 -1
  18. package/dist/server/utils/auth.d.ts.map +1 -1
  19. package/dist/server/utils/auth.js +8 -0
  20. package/dist/server/utils/auth.js.map +1 -1
  21. package/dist/server/utils/authHandler.js +1 -1
  22. package/dist/server/utils/authHandler.js.map +1 -1
  23. package/dist/server/utils/mergePatch.d.ts +8 -0
  24. package/dist/server/utils/mergePatch.d.ts.map +1 -0
  25. package/dist/server/utils/mergePatch.js +127 -0
  26. package/dist/server/utils/mergePatch.js.map +1 -0
  27. package/dist/server/utils/timestamps.d.ts.map +1 -1
  28. package/dist/server/utils/timestamps.js +2 -1
  29. package/dist/server/utils/timestamps.js.map +1 -1
  30. package/dist/sharp-layer.zip +0 -0
  31. package/package.json +1 -1
  32. package/terraform/README.md +5 -0
  33. 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
 
@@ -1,5 +1,5 @@
1
- // @exabugs/dynamodb-client v1.6.1
2
- // Built: 2026-10-11T12:45:55.244Z
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 ConfigError(`Invalid filter field syntax: ${fieldKey}`, {
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
- [timestampFields.updatedAt]: now,
35922
- ...data
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/operations/updateMany.ts
36223
- var updateMany_exports = {};
36224
- __export(updateMany_exports, {
36225
- handleUpdateMany: () => handleUpdateMany
36226
- });
36227
- function expandDotNotation(patch) {
36228
- const result = {};
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
- const keys = key.split(".");
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
- result[key] = value;
36244
+ setKey(target, key, value, key);
36246
36245
  }
36247
36246
  }
36248
- return result;
36249
36247
  }
36250
- function applyJsonMergePatch(target, patch) {
36251
- const expandedPatch = expandDotNotation(patch);
36252
- const result = { ...target };
36253
- for (const [key, value] of Object.entries(expandedPatch)) {
36254
- if (value === null) {
36255
- delete result[key];
36256
- } else if (typeof value === "object" && !Array.isArray(value) && value !== null && typeof result[key] === "object" && !Array.isArray(result[key]) && result[key] !== null) {
36257
- result[key] = applyJsonMergePatch(
36258
- result[key],
36259
- value
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
- result[key] = value;
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 mergedData = applyJsonMergePatch(removeShadowKeys(existingData), actualPatchData2);
36395
- const updatedData = addUpdateTimestamp({
36396
- ...mergedData,
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.6.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") {