@kensio/yulin 1.21.12 → 1.21.13
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/dist/service/dynamodb/command/authorize/sim-dynamodb-authorizer.d.ts +1 -1
- package/dist/service/dynamodb/command/authorize/sim-dynamodb-authorizer.js +23 -4
- package/dist/service/dynamodb/command/authorize/sim-dynamodb-leading-keys.d.ts +33 -0
- package/dist/service/dynamodb/command/authorize/sim-dynamodb-leading-keys.js +45 -0
- package/dist/service/dynamodb/command/batch/sim-dynamodb-batch-get-item.js +2 -0
- package/dist/service/dynamodb/command/batch/sim-dynamodb-batch-tables.d.ts +10 -2
- package/dist/service/dynamodb/command/batch/sim-dynamodb-batch-tables.js +1 -1
- package/dist/service/dynamodb/command/batch/sim-dynamodb-batch-write-item.js +7 -1
- package/dist/service/dynamodb/command/batch/sim-dynamodb-batch-write.d.ts +6 -0
- package/dist/service/dynamodb/command/batch/sim-dynamodb-batch-write.js +8 -8
- package/dist/service/dynamodb/command/item/sim-dynamodb-delete-item.js +2 -1
- package/dist/service/dynamodb/command/item/sim-dynamodb-get-item.js +2 -1
- package/dist/service/dynamodb/command/item/sim-dynamodb-put-item.js +2 -1
- package/dist/service/dynamodb/command/item/sim-dynamodb-update-item.js +2 -1
- package/dist/service/dynamodb/command/query/sim-dynamodb-query.js +6 -1
- package/dist/service/dynamodb/command/table/sim-dynamodb-table-access.d.ts +16 -6
- package/dist/service/dynamodb/command/table/sim-dynamodb-table-access.js +34 -8
- package/dist/service/iam/authorize/match/condition/sim-iam-condition-operator.d.ts +4 -1
- package/dist/service/iam/authorize/match/condition/string/all-values/sim-iam-for-all-values-string-condition-operator.d.ts +5 -1
- package/dist/service/iam/authorize/match/condition/string/all-values/sim-iam-for-all-values-string-condition-operator.js +5 -1
- package/docs/services/dynamodb/README.md +108 -0
- package/docs/services/iam/README.md +8 -2
- package/package.json +2 -2
|
@@ -29,7 +29,7 @@ export declare class SimDynamoDbAuthorizer {
|
|
|
29
29
|
* omitted caller, which defaults to Account root, from an explicit anonymous
|
|
30
30
|
* caller.
|
|
31
31
|
*/
|
|
32
|
-
authorizeTable(action: string, tableName: string, caller?: SimAwsCaller): SimAwsResolvedCaller;
|
|
32
|
+
authorizeTable(action: string, tableName: string, caller?: SimAwsCaller, leadingKeys?: readonly string[]): SimAwsResolvedCaller;
|
|
33
33
|
/**
|
|
34
34
|
* Ensure the caller may perform an action on a table's stream.
|
|
35
35
|
*
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { SimIamAccessDenied } from "../../../iam/error/sim-iam.error.js";
|
|
2
2
|
import { simDynamoDbTableArn } from "../../table/sim-dynamodb-table-arn.js";
|
|
3
|
+
import { simDynamoDbLeadingKeysConditionKey } from "./sim-dynamodb-leading-keys.js";
|
|
4
|
+
/**
|
|
5
|
+
* The condition values a request carries for the table it names.
|
|
6
|
+
*
|
|
7
|
+
* A request reaching no partition key value supplies none rather than an empty
|
|
8
|
+
* list, leaving the key absent from the context as it is on AWS for an
|
|
9
|
+
* operation that names no item.
|
|
10
|
+
*/
|
|
11
|
+
function conditionContextOf(leadingKeys) {
|
|
12
|
+
if (leadingKeys === undefined || leadingKeys.length === 0) {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
return { [simDynamoDbLeadingKeysConditionKey]: leadingKeys };
|
|
16
|
+
}
|
|
3
17
|
/**
|
|
4
18
|
* Applies simulated IAM authorization to DynamoDB requests.
|
|
5
19
|
*
|
|
@@ -26,8 +40,8 @@ export class SimDynamoDbAuthorizer {
|
|
|
26
40
|
* omitted caller, which defaults to Account root, from an explicit anonymous
|
|
27
41
|
* caller.
|
|
28
42
|
*/
|
|
29
|
-
authorizeTable(action, tableName, caller) {
|
|
30
|
-
return this.authorizeResource(action, simDynamoDbTableArn(this.accountRegionScope, tableName), caller);
|
|
43
|
+
authorizeTable(action, tableName, caller, leadingKeys) {
|
|
44
|
+
return this.authorizeResource(action, simDynamoDbTableArn(this.accountRegionScope, tableName), caller, leadingKeys);
|
|
31
45
|
}
|
|
32
46
|
/**
|
|
33
47
|
* Ensure the caller may perform an action on a table's stream.
|
|
@@ -51,8 +65,13 @@ export class SimDynamoDbAuthorizer {
|
|
|
51
65
|
authorizeAnyTable(action, caller) {
|
|
52
66
|
return this.authorizeResource(action, "*", caller);
|
|
53
67
|
}
|
|
54
|
-
authorizeResource(action, resource, caller) {
|
|
55
|
-
const decision = this.iam.authorize({
|
|
68
|
+
authorizeResource(action, resource, caller, leadingKeys) {
|
|
69
|
+
const decision = this.iam.authorize({
|
|
70
|
+
action,
|
|
71
|
+
resource,
|
|
72
|
+
caller,
|
|
73
|
+
conditionContext: conditionContextOf(leadingKeys),
|
|
74
|
+
});
|
|
56
75
|
if (decision.isDenied) {
|
|
57
76
|
throw new SimIamAccessDenied({
|
|
58
77
|
principal: decision.caller.principal,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SimDynamoDbItem } from "../../item/sim-dynamodb-item.js";
|
|
2
|
+
import type { SimDynamoDbValue } from "../../item/sim-dynamodb-value.js";
|
|
3
|
+
import type { SimDynamoDbTable } from "../../table/sim-dynamodb-table.js";
|
|
4
|
+
/**
|
|
5
|
+
* The condition key DynamoDB names a request's partition key values by.
|
|
6
|
+
*/
|
|
7
|
+
export declare const simDynamoDbLeadingKeysConditionKey = "dynamodb:LeadingKeys";
|
|
8
|
+
/**
|
|
9
|
+
* The partition key values one request reaches in one table.
|
|
10
|
+
*
|
|
11
|
+
* Which attribute holds the partition key is the table's to say, so a command
|
|
12
|
+
* hands over how to read the values rather than the values themselves. The
|
|
13
|
+
* table is found before the caller is authorized against it, and the values
|
|
14
|
+
* are read from it in between.
|
|
15
|
+
*/
|
|
16
|
+
export type SimDynamoDbLeadingKeys = (table: SimDynamoDbTable) => readonly string[];
|
|
17
|
+
/**
|
|
18
|
+
* Read the partition key values a run of items or keys carries.
|
|
19
|
+
*
|
|
20
|
+
* The items are read when authorization asks for them rather than at the call
|
|
21
|
+
* site, which is what keeps a request DynamoDB would refuse being refused
|
|
22
|
+
* after the caller has been authorized rather than before.
|
|
23
|
+
*
|
|
24
|
+
* An item with no readable partition key value is left out.
|
|
25
|
+
*/
|
|
26
|
+
export declare function simDynamoDbLeadingKeysOf(items: () => readonly SimDynamoDbItem[]): SimDynamoDbLeadingKeys;
|
|
27
|
+
/**
|
|
28
|
+
* Read the one partition key value a key condition names.
|
|
29
|
+
*
|
|
30
|
+
* The value is already held against the key schema of what is being read, so
|
|
31
|
+
* the table says nothing more about it here.
|
|
32
|
+
*/
|
|
33
|
+
export declare function simDynamoDbLeadingKeyOf(value: SimDynamoDbValue): readonly string[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The condition key DynamoDB names a request's partition key values by.
|
|
3
|
+
*/
|
|
4
|
+
export const simDynamoDbLeadingKeysConditionKey = "dynamodb:LeadingKeys";
|
|
5
|
+
/**
|
|
6
|
+
* Write one partition key value the way an IAM policy condition holds it.
|
|
7
|
+
*
|
|
8
|
+
* A policy condition value is a string. A string key is its text and a number
|
|
9
|
+
* key is its digits. Binary has no form documented for the condition key, and
|
|
10
|
+
* a request carrying a binary partition key leaves the value out rather than
|
|
11
|
+
* inventing an encoding for it.
|
|
12
|
+
*/
|
|
13
|
+
function leadingKeyText(value) {
|
|
14
|
+
if (value?.kind === "S") {
|
|
15
|
+
return value.text;
|
|
16
|
+
}
|
|
17
|
+
if (value?.kind === "N") {
|
|
18
|
+
return value.number.text;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Read the partition key values a run of items or keys carries.
|
|
24
|
+
*
|
|
25
|
+
* The items are read when authorization asks for them rather than at the call
|
|
26
|
+
* site, which is what keeps a request DynamoDB would refuse being refused
|
|
27
|
+
* after the caller has been authorized rather than before.
|
|
28
|
+
*
|
|
29
|
+
* An item with no readable partition key value is left out.
|
|
30
|
+
*/
|
|
31
|
+
export function simDynamoDbLeadingKeysOf(items) {
|
|
32
|
+
return (table) => items()
|
|
33
|
+
.map((item) => leadingKeyText(item.attribute(table.keySchema.hashKeyAttributeName)))
|
|
34
|
+
.filter((text) => text !== undefined);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read the one partition key value a key condition names.
|
|
38
|
+
*
|
|
39
|
+
* The value is already held against the key schema of what is being read, so
|
|
40
|
+
* the table says nothing more about it here.
|
|
41
|
+
*/
|
|
42
|
+
export function simDynamoDbLeadingKeyOf(value) {
|
|
43
|
+
const text = leadingKeyText(value);
|
|
44
|
+
return text === undefined ? [] : [text];
|
|
45
|
+
}
|
|
@@ -2,6 +2,7 @@ import { readSimDynamoDbBatchReads, } from "./sim-dynamodb-batch-reads.js";
|
|
|
2
2
|
import { assertDistinctBatchItems } from "./sim-dynamodb-batch-request-items.js";
|
|
3
3
|
import { reachSimDynamoDbBatchTables, } from "./sim-dynamodb-batch-tables.js";
|
|
4
4
|
import { refuseUnsimulatedBatchReadInput } from "./sim-dynamodb-unsimulated-batch-input.js";
|
|
5
|
+
import { simDynamoDbLeadingKeysOf } from "../authorize/sim-dynamodb-leading-keys.js";
|
|
5
6
|
/**
|
|
6
7
|
* The BatchGetItem command.
|
|
7
8
|
*
|
|
@@ -27,6 +28,7 @@ export class SimDynamoDbBatchGetItem {
|
|
|
27
28
|
access: this.access,
|
|
28
29
|
operation: "BatchGetItem",
|
|
29
30
|
caller: options?.caller,
|
|
31
|
+
leadingKeys: (entry) => simDynamoDbLeadingKeysOf(() => entry.keys),
|
|
30
32
|
});
|
|
31
33
|
const responses = Object.fromEntries(reached.map((entry) => [entry.requested.reference, readItems(entry)]));
|
|
32
34
|
// Nothing here throttles or stops at a response size, so no key is ever
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SimAwsCaller } from "../../../aws/caller/sim-aws-caller.js";
|
|
2
2
|
import type { SimDynamoDbTable } from "../../table/sim-dynamodb-table.js";
|
|
3
3
|
import type { SimDynamoDbTableAccess } from "../table/sim-dynamodb-table-access.js";
|
|
4
|
+
import type { SimDynamoDbLeadingKeys } from "../authorize/sim-dynamodb-leading-keys.js";
|
|
4
5
|
/**
|
|
5
6
|
* What one table of a batch asks for, against the table it names.
|
|
6
7
|
*/
|
|
@@ -8,10 +9,17 @@ export interface SimDynamoDbBatchTable<Requested> {
|
|
|
8
9
|
readonly table: SimDynamoDbTable;
|
|
9
10
|
readonly requested: Requested;
|
|
10
11
|
}
|
|
11
|
-
interface SimDynamoDbBatchReach {
|
|
12
|
+
interface SimDynamoDbBatchReach<Requested> {
|
|
12
13
|
readonly access: SimDynamoDbTableAccess;
|
|
13
14
|
readonly operation: string;
|
|
14
15
|
readonly caller: SimAwsCaller | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* How to read the partition key values one table of the batch is asked for.
|
|
18
|
+
*
|
|
19
|
+
* Each table is authorized on its own, so each carries the keys the batch
|
|
20
|
+
* names in that table and no others.
|
|
21
|
+
*/
|
|
22
|
+
readonly leadingKeys: (requested: Requested) => SimDynamoDbLeadingKeys;
|
|
15
23
|
}
|
|
16
24
|
/**
|
|
17
25
|
* Reach every table a batch names, authorizing the caller against each.
|
|
@@ -24,5 +32,5 @@ interface SimDynamoDbBatchReach {
|
|
|
24
32
|
*/
|
|
25
33
|
export declare function reachSimDynamoDbBatchTables<Requested extends {
|
|
26
34
|
readonly reference: string;
|
|
27
|
-
}>(requested: readonly Requested[], reach: SimDynamoDbBatchReach): readonly SimDynamoDbBatchTable<Requested>[];
|
|
35
|
+
}>(requested: readonly Requested[], reach: SimDynamoDbBatchReach<Requested>): readonly SimDynamoDbBatchTable<Requested>[];
|
|
28
36
|
export {};
|
|
@@ -11,7 +11,7 @@ import { SimDynamoDbValidationException } from "../../error/dynamodb.error.js";
|
|
|
11
11
|
export function reachSimDynamoDbBatchTables(requested, reach) {
|
|
12
12
|
const action = `dynamodb:${reach.operation}`;
|
|
13
13
|
const reached = requested.map((entry) => ({
|
|
14
|
-
table: reach.access.required(action, entry.reference, reach.caller),
|
|
14
|
+
table: reach.access.required(action, entry.reference, reach.caller, reach.leadingKeys(entry)),
|
|
15
15
|
requested: entry,
|
|
16
16
|
}));
|
|
17
17
|
assertDistinctTables(reached, reach.operation);
|
|
@@ -2,6 +2,7 @@ import { assertDistinctBatchItems } from "./sim-dynamodb-batch-request-items.js"
|
|
|
2
2
|
import { reachSimDynamoDbBatchTables } from "./sim-dynamodb-batch-tables.js";
|
|
3
3
|
import { readSimDynamoDbBatchWrites } from "./sim-dynamodb-batch-writes.js";
|
|
4
4
|
import { refuseUnsimulatedBatchWriteInput } from "./sim-dynamodb-unsimulated-batch-input.js";
|
|
5
|
+
import { simDynamoDbLeadingKeysOf } from "../authorize/sim-dynamodb-leading-keys.js";
|
|
5
6
|
const operation = "BatchWriteItem";
|
|
6
7
|
/**
|
|
7
8
|
* The BatchWriteItem command.
|
|
@@ -25,7 +26,12 @@ export class SimDynamoDbBatchWriteItem {
|
|
|
25
26
|
handle(command, options) {
|
|
26
27
|
const input = command.input;
|
|
27
28
|
refuseUnsimulatedBatchWriteInput(input);
|
|
28
|
-
const reached = reachSimDynamoDbBatchTables(readSimDynamoDbBatchWrites(input.RequestItems), {
|
|
29
|
+
const reached = reachSimDynamoDbBatchTables(readSimDynamoDbBatchWrites(input.RequestItems), {
|
|
30
|
+
access: this.access,
|
|
31
|
+
operation,
|
|
32
|
+
caller: options?.caller,
|
|
33
|
+
leadingKeys: (entry) => simDynamoDbLeadingKeysOf(() => entry.writes.map((write) => write.names)),
|
|
34
|
+
});
|
|
29
35
|
// Marshalling every key checks it against the table's key schema, and is
|
|
30
36
|
// what tells two operations on one item apart. Every table is checked
|
|
31
37
|
// before any of them is written to.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SimDynamoDbItem } from "../../item/sim-dynamodb-item.js";
|
|
1
2
|
import type { SimDynamoDbTable } from "../../table/sim-dynamodb-table.js";
|
|
2
3
|
import type { SimDynamoDbWriteRequest } from "./batch.command.js";
|
|
3
4
|
/**
|
|
@@ -8,6 +9,11 @@ import type { SimDynamoDbWriteRequest } from "./batch.command.js";
|
|
|
8
9
|
* before it applies anything, since the check and the write read the same item.
|
|
9
10
|
*/
|
|
10
11
|
export interface SimDynamoDbBatchWrite {
|
|
12
|
+
/**
|
|
13
|
+
* The item or key this write names, which is where a policy condition reads
|
|
14
|
+
* its partition key value from.
|
|
15
|
+
*/
|
|
16
|
+
readonly names: SimDynamoDbItem;
|
|
11
17
|
/**
|
|
12
18
|
* The primary key this write works on, as the table marshals it.
|
|
13
19
|
*/
|
|
@@ -5,15 +5,15 @@ import { readSimDynamoDbKey } from "../item/sim-dynamodb-key-input.js";
|
|
|
5
5
|
* A put in a batch write, which replaces the whole item as PutItem does.
|
|
6
6
|
*/
|
|
7
7
|
class SimDynamoDbBatchPut {
|
|
8
|
-
|
|
8
|
+
names;
|
|
9
9
|
constructor(item) {
|
|
10
|
-
this.
|
|
10
|
+
this.names = item;
|
|
11
11
|
}
|
|
12
12
|
keyIn(table) {
|
|
13
|
-
return table.keyOfItem(this.
|
|
13
|
+
return table.keyOfItem(this.names);
|
|
14
14
|
}
|
|
15
15
|
applyTo(table) {
|
|
16
|
-
table.putItem(this.
|
|
16
|
+
table.putItem(this.names);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
@@ -21,15 +21,15 @@ class SimDynamoDbBatchPut {
|
|
|
21
21
|
* that is already free is deleted successfully.
|
|
22
22
|
*/
|
|
23
23
|
class SimDynamoDbBatchDelete {
|
|
24
|
-
|
|
24
|
+
names;
|
|
25
25
|
constructor(key) {
|
|
26
|
-
this.
|
|
26
|
+
this.names = key;
|
|
27
27
|
}
|
|
28
28
|
keyIn(table) {
|
|
29
|
-
return table.keyOfKey(this.
|
|
29
|
+
return table.keyOfKey(this.names);
|
|
30
30
|
}
|
|
31
31
|
applyTo(table) {
|
|
32
|
-
table.deleteItem(this.
|
|
32
|
+
table.deleteItem(this.names);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { simDynamoDbLeadingKeysOf } from "../authorize/sim-dynamodb-leading-keys.js";
|
|
1
2
|
import { readSimDynamoDbKey } from "./sim-dynamodb-key-input.js";
|
|
2
3
|
import { SimDynamoDbConditionCheck } from "./sim-dynamodb-condition-check.js";
|
|
3
4
|
import { SimDynamoDbReturnValues } from "./sim-dynamodb-return-values.js";
|
|
@@ -26,7 +27,7 @@ export class SimDynamoDbDeleteItem {
|
|
|
26
27
|
// The condition is read before the table is reached, so an expression
|
|
27
28
|
// DynamoDB would refuse is refused whether or not the key holds anything.
|
|
28
29
|
const check = SimDynamoDbConditionCheck.read(input, "DeleteItem");
|
|
29
|
-
const table = this.access.required("dynamodb:DeleteItem", input.TableName, options?.caller);
|
|
30
|
+
const table = this.access.required("dynamodb:DeleteItem", input.TableName, options?.caller, simDynamoDbLeadingKeysOf(() => [readSimDynamoDbKey(input.Key)]));
|
|
30
31
|
const asked = SimDynamoDbReturnValues.read(input.ReturnValues, "DeleteItem");
|
|
31
32
|
const key = readSimDynamoDbKey(input.Key);
|
|
32
33
|
check.assertHoldsFor(table.getItem(key));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { simDynamoDbLeadingKeysOf } from "../authorize/sim-dynamodb-leading-keys.js";
|
|
1
2
|
import { readSimDynamoDbKey } from "./sim-dynamodb-key-input.js";
|
|
2
3
|
import { refuseUnsimulatedItemReadInput } from "./sim-dynamodb-unsimulated-item-read-input.js";
|
|
3
4
|
import { readSimDynamoDbProjection } from "../../expression/projection/sim-dynamodb-projection-expression.js";
|
|
@@ -26,7 +27,7 @@ export class SimDynamoDbGetItem {
|
|
|
26
27
|
// The projection is read before the table is reached, so an expression
|
|
27
28
|
// DynamoDB would refuse is refused whether or not the key holds anything.
|
|
28
29
|
const projection = readSimDynamoDbProjection(input);
|
|
29
|
-
const table = this.access.required("dynamodb:GetItem", input.TableName, options?.caller);
|
|
30
|
+
const table = this.access.required("dynamodb:GetItem", input.TableName, options?.caller, simDynamoDbLeadingKeysOf(() => [readSimDynamoDbKey(input.Key)]));
|
|
30
31
|
const found = table.getItem(readSimDynamoDbKey(input.Key));
|
|
31
32
|
if (found === undefined) {
|
|
32
33
|
return { $metadata: {} };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SimDynamoDbValidationException } from "../../error/dynamodb.error.js";
|
|
2
2
|
import { SimDynamoDbItem } from "../../item/sim-dynamodb-item.js";
|
|
3
|
+
import { simDynamoDbLeadingKeysOf } from "../authorize/sim-dynamodb-leading-keys.js";
|
|
3
4
|
import { SimDynamoDbConditionCheck } from "./sim-dynamodb-condition-check.js";
|
|
4
5
|
import { SimDynamoDbReturnValues } from "./sim-dynamodb-return-values.js";
|
|
5
6
|
import { refuseUnsimulatedItemWriteInput } from "./sim-dynamodb-unsimulated-item-write-input.js";
|
|
@@ -36,7 +37,7 @@ export class SimDynamoDbPutItem {
|
|
|
36
37
|
// The condition is read before the table is reached, so an expression
|
|
37
38
|
// DynamoDB would refuse is refused whether or not the key holds anything.
|
|
38
39
|
const check = SimDynamoDbConditionCheck.read(input, "PutItem");
|
|
39
|
-
const table = this.access.required("dynamodb:PutItem", input.TableName, options?.caller);
|
|
40
|
+
const table = this.access.required("dynamodb:PutItem", input.TableName, options?.caller, simDynamoDbLeadingKeysOf(() => [readItem(input)]));
|
|
40
41
|
const asked = SimDynamoDbReturnValues.read(input.ReturnValues, "PutItem");
|
|
41
42
|
const item = readItem(input);
|
|
42
43
|
check.assertHoldsFor(table.itemUnder(item));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { simDynamoDbLeadingKeysOf } from "../authorize/sim-dynamodb-leading-keys.js";
|
|
1
2
|
import { readSimDynamoDbKey } from "./sim-dynamodb-key-input.js";
|
|
2
3
|
import { SimDynamoDbReturnValues } from "./sim-dynamodb-return-values.js";
|
|
3
4
|
import { refuseUnsimulatedItemUpdateInput } from "./sim-dynamodb-unsimulated-item-update-input.js";
|
|
@@ -28,7 +29,7 @@ export class SimDynamoDbUpdateItem {
|
|
|
28
29
|
// The expressions are read before the table is reached, so one DynamoDB
|
|
29
30
|
// would refuse is refused whether or not the key holds anything.
|
|
30
31
|
const plan = SimDynamoDbUpdatePlan.read(input, "UpdateItem");
|
|
31
|
-
const table = this.access.required("dynamodb:UpdateItem", input.TableName, options?.caller);
|
|
32
|
+
const table = this.access.required("dynamodb:UpdateItem", input.TableName, options?.caller, simDynamoDbLeadingKeysOf(() => [readSimDynamoDbKey(input.Key)]));
|
|
32
33
|
const asked = SimDynamoDbReturnValues.readForUpdate(input.ReturnValues, "UpdateItem");
|
|
33
34
|
const key = readSimDynamoDbKey(input.Key);
|
|
34
35
|
const existing = table.getItem(key);
|
|
@@ -2,6 +2,7 @@ import { SimDynamoDbItemPage } from "../item/sim-dynamodb-item-page.js";
|
|
|
2
2
|
import { SimDynamoDbReadAnswer } from "../read/sim-dynamodb-read-answer.js";
|
|
3
3
|
import { assertSimDynamoDbConsistentReadAnswerable } from "../read/sim-dynamodb-consistent-read.js";
|
|
4
4
|
import { SimDynamoDbSelect } from "../read/sim-dynamodb-select.js";
|
|
5
|
+
import { simDynamoDbLeadingKeyOf } from "../authorize/sim-dynamodb-leading-keys.js";
|
|
5
6
|
import { readSimDynamoDbQueryExpressions } from "./sim-dynamodb-query-expressions.js";
|
|
6
7
|
import { refuseSimDynamoDbQuerySegment } from "./sim-dynamodb-query-segment.js";
|
|
7
8
|
import { readSimDynamoDbQueryStartKey } from "./sim-dynamodb-query-start-key.js";
|
|
@@ -30,7 +31,11 @@ export class SimDynamoDbQuery {
|
|
|
30
31
|
refuseUnsimulatedQueryInput(input);
|
|
31
32
|
refuseSimDynamoDbQuerySegment(input);
|
|
32
33
|
const expressions = readSimDynamoDbQueryExpressions(input);
|
|
33
|
-
|
|
34
|
+
// The one partition key the query reads is what authorization needs, and
|
|
35
|
+
// reading it needs the view rather than the table, so it is read again
|
|
36
|
+
// here against whichever of the two the request names.
|
|
37
|
+
const table = this.access.required("dynamodb:Query", input.TableName, options?.caller, (reached) => simDynamoDbLeadingKeyOf(expressions.terms.forTable(reached.view(input.IndexName))
|
|
38
|
+
.partitionKeyValue));
|
|
34
39
|
// What is being read is settled here: the table, or one of its indexes. An
|
|
35
40
|
// IndexName the table does not have is refused rather than read as the
|
|
36
41
|
// table. Everything after this asks the view rather than the table, so a
|
|
@@ -4,6 +4,7 @@ import type { SimDynamoDbTable } from "../../table/sim-dynamodb-table.js";
|
|
|
4
4
|
import type { SimDynamoDbTableName } from "../../table/sim-dynamodb-table-name.js";
|
|
5
5
|
import type { SimDynamoDbTableStore } from "../../table/sim-dynamodb-table-store.js";
|
|
6
6
|
import type { SimDynamoDbAuthorizer } from "../authorize/sim-dynamodb-authorizer.js";
|
|
7
|
+
import type { SimDynamoDbLeadingKeys } from "../authorize/sim-dynamodb-leading-keys.js";
|
|
7
8
|
interface SimDynamoDbTableAccessProperties {
|
|
8
9
|
readonly tables: SimDynamoDbTableStore;
|
|
9
10
|
readonly authorizer: SimDynamoDbAuthorizer;
|
|
@@ -14,9 +15,14 @@ interface SimDynamoDbTableAccessProperties {
|
|
|
14
15
|
*
|
|
15
16
|
* Every table command goes through the same two steps in the same order: read
|
|
16
17
|
* the name or ARN the request carries, then authorize the caller against it
|
|
17
|
-
* before
|
|
18
|
-
* order the same for all of them, so no command can
|
|
19
|
-
* unauthorized caller which table names are taken.
|
|
18
|
+
* before the command is told whether the table is there. Keeping them here is
|
|
19
|
+
* what makes that order the same for all of them, so no command can
|
|
20
|
+
* accidentally tell an unauthorized caller which table names are taken.
|
|
21
|
+
*
|
|
22
|
+
* A command reaching particular items hands over how to read their partition
|
|
23
|
+
* key values, which authorization needs the table to read. The table is found
|
|
24
|
+
* first for that, and a caller refused against a table that is not there still
|
|
25
|
+
* hears AccessDenied rather than a missing table.
|
|
20
26
|
*/
|
|
21
27
|
export declare class SimDynamoDbTableAccess {
|
|
22
28
|
private readonly tables;
|
|
@@ -28,13 +34,17 @@ export declare class SimDynamoDbTableAccess {
|
|
|
28
34
|
*/
|
|
29
35
|
reference(tableName: string | undefined): SimDynamoDbTableName;
|
|
30
36
|
/**
|
|
31
|
-
* Find the table a request names, refusing the caller before
|
|
37
|
+
* Find the table a request names, refusing the caller before answering.
|
|
32
38
|
*/
|
|
33
|
-
required(action: string, tableName: string | undefined, caller: SimAwsCaller | undefined): SimDynamoDbTable;
|
|
39
|
+
required(action: string, tableName: string | undefined, caller: SimAwsCaller | undefined, leadingKeys?: SimDynamoDbLeadingKeys): SimDynamoDbTable;
|
|
34
40
|
/**
|
|
35
41
|
* Find a table by a name that has already been read.
|
|
42
|
+
*
|
|
43
|
+
* A caller with no permission hears AccessDenied whether or not the table is
|
|
44
|
+
* there, which keeps an unauthorized caller from finding out which table
|
|
45
|
+
* names are taken.
|
|
36
46
|
*/
|
|
37
|
-
requiredByName(action: string, name: SimDynamoDbTableName, caller: SimAwsCaller | undefined): SimDynamoDbTable;
|
|
47
|
+
requiredByName(action: string, name: SimDynamoDbTableName, caller: SimAwsCaller | undefined, leadingKeys?: SimDynamoDbLeadingKeys): SimDynamoDbTable;
|
|
38
48
|
/**
|
|
39
49
|
* Ensure the caller may perform an action that names no particular table.
|
|
40
50
|
*/
|
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
import { SimDynamoDbResourceNotFoundException } from "../../error/dynamodb.error.js";
|
|
2
2
|
import { readSimDynamoDbTableReference } from "../../table/sim-dynamodb-table-reference.js";
|
|
3
|
+
/**
|
|
4
|
+
* Read the partition key values a request reaches, or none where they cannot
|
|
5
|
+
* be read.
|
|
6
|
+
*
|
|
7
|
+
* Authorization runs ahead of the checks a command makes on what it was given,
|
|
8
|
+
* as it does on AWS. A request whose key or key condition is malformed is
|
|
9
|
+
* authorized carrying no values and refused by the check that follows, so
|
|
10
|
+
* whatever reading them throws is dropped here.
|
|
11
|
+
*/
|
|
12
|
+
function readLeadingKeys(leadingKeys, table) {
|
|
13
|
+
try {
|
|
14
|
+
return leadingKeys?.(table);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
3
20
|
/**
|
|
4
21
|
* How a command reaches the table a request names.
|
|
5
22
|
*
|
|
6
23
|
* Every table command goes through the same two steps in the same order: read
|
|
7
24
|
* the name or ARN the request carries, then authorize the caller against it
|
|
8
|
-
* before
|
|
9
|
-
* order the same for all of them, so no command can
|
|
10
|
-
* unauthorized caller which table names are taken.
|
|
25
|
+
* before the command is told whether the table is there. Keeping them here is
|
|
26
|
+
* what makes that order the same for all of them, so no command can
|
|
27
|
+
* accidentally tell an unauthorized caller which table names are taken.
|
|
28
|
+
*
|
|
29
|
+
* A command reaching particular items hands over how to read their partition
|
|
30
|
+
* key values, which authorization needs the table to read. The table is found
|
|
31
|
+
* first for that, and a caller refused against a table that is not there still
|
|
32
|
+
* hears AccessDenied rather than a missing table.
|
|
11
33
|
*/
|
|
12
34
|
export class SimDynamoDbTableAccess {
|
|
13
35
|
tables;
|
|
@@ -25,17 +47,21 @@ export class SimDynamoDbTableAccess {
|
|
|
25
47
|
return readSimDynamoDbTableReference(tableName, this.accountRegionScope);
|
|
26
48
|
}
|
|
27
49
|
/**
|
|
28
|
-
* Find the table a request names, refusing the caller before
|
|
50
|
+
* Find the table a request names, refusing the caller before answering.
|
|
29
51
|
*/
|
|
30
|
-
required(action, tableName, caller) {
|
|
31
|
-
return this.requiredByName(action, this.reference(tableName), caller);
|
|
52
|
+
required(action, tableName, caller, leadingKeys) {
|
|
53
|
+
return this.requiredByName(action, this.reference(tableName), caller, leadingKeys);
|
|
32
54
|
}
|
|
33
55
|
/**
|
|
34
56
|
* Find a table by a name that has already been read.
|
|
57
|
+
*
|
|
58
|
+
* A caller with no permission hears AccessDenied whether or not the table is
|
|
59
|
+
* there, which keeps an unauthorized caller from finding out which table
|
|
60
|
+
* names are taken.
|
|
35
61
|
*/
|
|
36
|
-
requiredByName(action, name, caller) {
|
|
37
|
-
this.authorizer.authorizeTable(action, name.value, caller);
|
|
62
|
+
requiredByName(action, name, caller, leadingKeys) {
|
|
38
63
|
const table = this.tables.find(name);
|
|
64
|
+
this.authorizer.authorizeTable(action, name.value, caller, table === undefined ? undefined : readLeadingKeys(leadingKeys, table));
|
|
39
65
|
if (table === undefined) {
|
|
40
66
|
throw new SimDynamoDbResourceNotFoundException(`No DynamoDB Table named ${name.value}`);
|
|
41
67
|
}
|
|
@@ -15,7 +15,10 @@ export interface SimIamConditionOperator {
|
|
|
15
15
|
* there is none for the policy value to equal.
|
|
16
16
|
*
|
|
17
17
|
* A `ForAnyValue` operator answers false whatever it wraps, because no
|
|
18
|
-
* request value is there to satisfy it.
|
|
18
|
+
* request value is there to satisfy it. A `ForAllValues` operator answers
|
|
19
|
+
* true, because every value the request carries matches vacuously. AWS
|
|
20
|
+
* documents all three rules, and warns that the `ForAllValues` rule leaves
|
|
21
|
+
* an `Allow` overly permissive without a `Null` guard beside it.
|
|
19
22
|
*/
|
|
20
23
|
readonly matchesAbsentKey: boolean;
|
|
21
24
|
/**
|
|
@@ -2,9 +2,13 @@ import type { SimIamConditionValue } from "../../../../../policy/sim-iam-policy.
|
|
|
2
2
|
import type { SimIamConditionOperator } from "../../sim-iam-condition-operator.js";
|
|
3
3
|
/**
|
|
4
4
|
* Base for `ForAllValues` IAM string operators.
|
|
5
|
+
*
|
|
6
|
+
* An absent context key matches. AWS answers true where the request carries
|
|
7
|
+
* no value for the key. A `ForAllValues` `Allow` therefore needs a `Null`
|
|
8
|
+
* guard beside it to stay tight.
|
|
5
9
|
*/
|
|
6
10
|
export declare abstract class SimIamForAllValuesStringConditionOperator implements SimIamConditionOperator {
|
|
7
|
-
readonly matchesAbsentKey =
|
|
11
|
+
readonly matchesAbsentKey = true;
|
|
8
12
|
/**
|
|
9
13
|
* Check whether a given value matches the expected value.
|
|
10
14
|
*/
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { simIamStringValues } from "../sim-iam-string-values.js";
|
|
2
2
|
/**
|
|
3
3
|
* Base for `ForAllValues` IAM string operators.
|
|
4
|
+
*
|
|
5
|
+
* An absent context key matches. AWS answers true where the request carries
|
|
6
|
+
* no value for the key. A `ForAllValues` `Allow` therefore needs a `Null`
|
|
7
|
+
* guard beside it to stay tight.
|
|
4
8
|
*/
|
|
5
9
|
export class SimIamForAllValuesStringConditionOperator {
|
|
6
|
-
matchesAbsentKey =
|
|
10
|
+
matchesAbsentKey = true;
|
|
7
11
|
/**
|
|
8
12
|
* Check whether a given value matches the expected value.
|
|
9
13
|
*/
|
|
@@ -3637,6 +3637,101 @@ unauthorized caller cannot find out which names are taken.
|
|
|
3637
3637
|
in the same way, each against the `dynamodb:` action of its own name. `ListTables` names no table.
|
|
3638
3638
|
It authorizes against `*`.
|
|
3639
3639
|
|
|
3640
|
+
### Fine-grained access control with dynamodb:LeadingKeys
|
|
3641
|
+
|
|
3642
|
+
`GetItem`, `BatchGetItem`, `Query`, `PutItem`, `UpdateItem`, `DeleteItem` and `BatchWriteItem`
|
|
3643
|
+
supply the partition key values they reach as `dynamodb:LeadingKeys`. A policy conditioned on that
|
|
3644
|
+
key restricts a caller to the items under particular partition keys. AWS scopes one user of a
|
|
3645
|
+
shared table to their own rows this way.
|
|
3646
|
+
|
|
3647
|
+
The condition takes the `ForAllValues:` qualifier, as AWS requires for this key. Every partition key
|
|
3648
|
+
value the request reaches has to match. A batch or a write naming one item outside the allowed set
|
|
3649
|
+
is refused whole, and nothing it asked for is applied.
|
|
3650
|
+
|
|
3651
|
+
```typescript sim-dynamodb-leading-keys
|
|
3652
|
+
import { CreateTableCommand, PutItemCommand } from "@aws-sdk/client-dynamodb";
|
|
3653
|
+
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
|
|
3654
|
+
import { SimAws } from "@kensio/yulin";
|
|
3655
|
+
|
|
3656
|
+
const simAws = new SimAws();
|
|
3657
|
+
const accountId = simAws.defaultAccountId;
|
|
3658
|
+
const region = simAws.defaultRegionName;
|
|
3659
|
+
|
|
3660
|
+
await simAws.dynamoDb().createTable(
|
|
3661
|
+
new CreateTableCommand({
|
|
3662
|
+
TableName: "OrdersTable",
|
|
3663
|
+
KeySchema: [{ AttributeName: "customerId", KeyType: "HASH" }],
|
|
3664
|
+
AttributeDefinitions: [{ AttributeName: "customerId", AttributeType: "S" }],
|
|
3665
|
+
BillingMode: "PAY_PER_REQUEST",
|
|
3666
|
+
}),
|
|
3667
|
+
);
|
|
3668
|
+
|
|
3669
|
+
const roleCreation = await simAws.iam().createRole(
|
|
3670
|
+
new CreateRoleCommand({
|
|
3671
|
+
RoleName: "CustomerRole",
|
|
3672
|
+
AssumeRolePolicyDocument: JSON.stringify({
|
|
3673
|
+
Version: "2012-10-17",
|
|
3674
|
+
Statement: {
|
|
3675
|
+
Effect: "Allow",
|
|
3676
|
+
Principal: { AWS: `arn:aws:iam::${accountId}:root` },
|
|
3677
|
+
Action: "sts:AssumeRole",
|
|
3678
|
+
},
|
|
3679
|
+
}),
|
|
3680
|
+
}),
|
|
3681
|
+
);
|
|
3682
|
+
|
|
3683
|
+
await simAws.iam().putRolePolicy(
|
|
3684
|
+
new PutRolePolicyCommand({
|
|
3685
|
+
RoleName: "CustomerRole",
|
|
3686
|
+
PolicyName: "OwnItemsOnly",
|
|
3687
|
+
PolicyDocument: JSON.stringify({
|
|
3688
|
+
Version: "2012-10-17",
|
|
3689
|
+
Statement: {
|
|
3690
|
+
Effect: "Allow",
|
|
3691
|
+
Action: "dynamodb:PutItem",
|
|
3692
|
+
Resource: `arn:aws:dynamodb:${region}:${accountId}:table/OrdersTable`,
|
|
3693
|
+
Condition: {
|
|
3694
|
+
"ForAllValues:StringEquals": { "dynamodb:LeadingKeys": ["c-1"] },
|
|
3695
|
+
},
|
|
3696
|
+
},
|
|
3697
|
+
}),
|
|
3698
|
+
}),
|
|
3699
|
+
);
|
|
3700
|
+
|
|
3701
|
+
const caller = { kind: "arn", arn: roleCreation.Role.Arn } as const;
|
|
3702
|
+
|
|
3703
|
+
// The customer's own item is written.
|
|
3704
|
+
await simAws.dynamoDb().putItem(
|
|
3705
|
+
new PutItemCommand({
|
|
3706
|
+
TableName: "OrdersTable",
|
|
3707
|
+
Item: { customerId: { S: "c-1" }, total: { N: "25" } },
|
|
3708
|
+
}),
|
|
3709
|
+
{ caller },
|
|
3710
|
+
);
|
|
3711
|
+
|
|
3712
|
+
// Another customer's item is refused.
|
|
3713
|
+
try {
|
|
3714
|
+
await simAws.dynamoDb().putItem(
|
|
3715
|
+
new PutItemCommand({
|
|
3716
|
+
TableName: "OrdersTable",
|
|
3717
|
+
Item: { customerId: { S: "c-2" }, total: { N: "25" } },
|
|
3718
|
+
}),
|
|
3719
|
+
{ caller },
|
|
3720
|
+
);
|
|
3721
|
+
} catch (error) {
|
|
3722
|
+
console.log(error instanceof Error ? error.name : "unknown error");
|
|
3723
|
+
// "AccessDenied"
|
|
3724
|
+
}
|
|
3725
|
+
```
|
|
3726
|
+
|
|
3727
|
+
Reading the partition key values needs the table's key schema. The table is found first for that,
|
|
3728
|
+
before the caller is authorized against it. A caller with no permission still hears `AccessDenied`
|
|
3729
|
+
when the table is missing, and an unauthorized caller cannot find out which table names are taken.
|
|
3730
|
+
|
|
3731
|
+
A request carrying no readable partition key value leaves the key out of the condition context. A
|
|
3732
|
+
`ForAllValues:` condition matches a request carrying no value for its key, as it does on AWS. The
|
|
3733
|
+
condition allows such a request.
|
|
3734
|
+
|
|
3640
3735
|
A transaction is authorized as the operations it is made of rather than as itself. Each action of a
|
|
3641
3736
|
`TransactWriteItems` needs `dynamodb:PutItem`, `dynamodb:UpdateItem`, `dynamodb:DeleteItem` or
|
|
3642
3737
|
`dynamodb:ConditionCheckItem` against the table it names, and each `Get` of a `TransactGetItems`
|
|
@@ -3657,6 +3752,9 @@ is written.
|
|
|
3657
3752
|
answering with the attributes it projects, and paging with a `LastEvaluatedKey` carrying the index
|
|
3658
3753
|
key and the table key together. A local secondary index also answers a strongly consistent read,
|
|
3659
3754
|
and fetches an unprojected attribute from the base table.
|
|
3755
|
+
- `dynamodb:LeadingKeys` on `GetItem`, `BatchGetItem`, `Query`, `PutItem`, `UpdateItem`,
|
|
3756
|
+
`DeleteItem` and `BatchWriteItem`, carrying the partition key values the request reaches so a
|
|
3757
|
+
`ForAllValues:` condition can scope a caller to particular items.
|
|
3660
3758
|
- `DescribeTable`, answering with the full table description, by table name or ARN.
|
|
3661
3759
|
- `ListTables`, ordered by UTF-8 bytes and paged with `Limit` and `ExclusiveStartTableName`.
|
|
3662
3760
|
- `DeleteTable`, following the table status DynamoDB moves a deleted table through, and refusing a
|
|
@@ -3730,6 +3828,16 @@ Arn`, `Fn::GetAtt … StreamArn` and `Fn::GetAtt … TableId` answering. A CDK `
|
|
|
3730
3828
|
|
|
3731
3829
|
## Limitations
|
|
3732
3830
|
|
|
3831
|
+
- `dynamodb:LeadingKeys` is the only DynamoDB condition key supplied. `dynamodb:Attributes`,
|
|
3832
|
+
`dynamodb:Select` and `dynamodb:ReturnValues` are absent from the condition context, and a
|
|
3833
|
+
statement conditioned on one of them matches no request. The transactional operations supply no
|
|
3834
|
+
leading keys either, and are authorized by action and table alone.
|
|
3835
|
+
- A partition key value reaches a policy condition as a string for a `S` key and as its digits for
|
|
3836
|
+
an `N` key. A binary partition key has no documented form for the condition key. A request
|
|
3837
|
+
carrying one leaves the value out.
|
|
3838
|
+
- A request against a table that is not there is authorized carrying no leading keys, since there is
|
|
3839
|
+
no key schema to read them with. A `ForAllValues:` condition matches such a request. A caller
|
|
3840
|
+
barred only by that condition learns the table is missing.
|
|
3733
3841
|
- The document client's PartiQL Commands go unconverted, because PartiQL is an operation this
|
|
3734
3842
|
simulation lacks yet. `ExecuteStatementCommand`, `BatchExecuteStatementCommand` and
|
|
3735
3843
|
`ExecuteTransactionCommand` are refused by name, never half converted.
|
|
@@ -335,6 +335,11 @@ matches instead, as AWS documents. With no value in the request there is none fo
|
|
|
335
335
|
to equal. A `ForAnyValue:` operator answers false for an absent key whatever it wraps, because no
|
|
336
336
|
request value is there to satisfy it.
|
|
337
337
|
|
|
338
|
+
A `ForAllValues:` operator answers true for an absent key, which is also AWS behaviour. Every value
|
|
339
|
+
the request carries matches when it carries none. AWS warns that this leaves a `ForAllValues:`
|
|
340
|
+
`Allow` overly permissive, and asks for a `Null` check with a `false` value beside it. `Null` is
|
|
341
|
+
absent from the operators above. A statement written that way fails closed here.
|
|
342
|
+
|
|
338
343
|
### Statements left unevaluated
|
|
339
344
|
|
|
340
345
|
An operator from outside the list above fails closed. The statement holding it matches nothing, and
|
|
@@ -1684,8 +1689,9 @@ Sim IAM models the policy behaviour that multi-service tests most commonly need.
|
|
|
1684
1689
|
- Only the condition operators listed above are supported. A statement using any other operator
|
|
1685
1690
|
fails closed, matching no request. `decision.unevaluatedStatements` names those statements, and a
|
|
1686
1691
|
test can assert that a decision was reached over policies read in full
|
|
1687
|
-
- A positive `ForAllValues:` condition fails to match
|
|
1688
|
-
|
|
1692
|
+
- A positive `ForAllValues:` condition fails to match an empty value set, where AWS matches one. An
|
|
1693
|
+
absent context key matches, as it does on AWS. A service here leaves a key out where the request
|
|
1694
|
+
carries no value for it, and the empty set is reached only by a caller passing one to `authorize`
|
|
1689
1695
|
- Signature age is deliberately not enforced. `X-Amz-Date` must be present, well formed, and agree
|
|
1690
1696
|
with the credential scope date, but is never compared to a clock. A client stamping real time can
|
|
1691
1697
|
therefore reach a simulation keeping a different one. Session expiry _is_ enforced, against
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kensio/yulin",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.13",
|
|
4
4
|
"description": "AWS system behaviour simulation for isolated unit testing",
|
|
5
5
|
"repository": "https://github.com/KensioSoftware/yulin",
|
|
6
6
|
"homepage": "https://yulinsim.dev/",
|
|
@@ -290,7 +290,7 @@
|
|
|
290
290
|
"conventional-changelog-conventionalcommits": "9.3.1",
|
|
291
291
|
"esbuild": "^0.28.2",
|
|
292
292
|
"eslint": "^10.8.0",
|
|
293
|
-
"eslint-plugin-jsdoc": "^
|
|
293
|
+
"eslint-plugin-jsdoc": "^64.0.1",
|
|
294
294
|
"eslint-plugin-no-secrets": "^2.3.3",
|
|
295
295
|
"eslint-plugin-security": "^4.0.1",
|
|
296
296
|
"eslint-plugin-unicorn": "^73.0.0",
|