local-serverless-stack 1.0.1 → 1.1.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 +125 -3
- package/README.md +75 -26
- package/bin/cli.js +81 -15
- package/bin/i18n.js +30 -3
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/namespaces/services.d.ts +46 -0
- package/dist/client/namespaces/services.d.ts.map +1 -1
- package/dist/client/namespaces/services.js +1 -0
- package/dist/client/namespaces/services.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +17 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/server/engine/backends/self-backend.d.ts +26 -1
- package/dist/server/engine/backends/self-backend.d.ts.map +1 -1
- package/dist/server/engine/backends/self-backend.js +104 -11
- package/dist/server/engine/backends/self-backend.js.map +1 -1
- package/dist/server/engine/emulators/dynamodb/expressions/parser.d.ts.map +1 -1
- package/dist/server/engine/emulators/dynamodb/expressions/parser.js +12 -1
- package/dist/server/engine/emulators/dynamodb/expressions/parser.js.map +1 -1
- package/dist/server/engine/emulators/dynamodb/expressions/reserved-words.d.ts +3 -0
- package/dist/server/engine/emulators/dynamodb/expressions/reserved-words.d.ts.map +1 -0
- package/dist/server/engine/emulators/dynamodb/expressions/reserved-words.js +78 -0
- package/dist/server/engine/emulators/dynamodb/expressions/reserved-words.js.map +1 -0
- package/dist/server/engine/emulators/dynamodb/index.d.ts +11 -0
- package/dist/server/engine/emulators/dynamodb/index.d.ts.map +1 -1
- package/dist/server/engine/emulators/dynamodb/index.js +101 -7
- package/dist/server/engine/emulators/dynamodb/index.js.map +1 -1
- package/dist/server/engine/emulators/dynamodb/query.d.ts +1 -0
- package/dist/server/engine/emulators/dynamodb/query.d.ts.map +1 -1
- package/dist/server/engine/emulators/dynamodb/query.js +34 -6
- package/dist/server/engine/emulators/dynamodb/query.js.map +1 -1
- package/dist/server/engine/emulators/s3/index.d.ts +10 -0
- package/dist/server/engine/emulators/s3/index.d.ts.map +1 -1
- package/dist/server/engine/emulators/s3/index.js +369 -25
- package/dist/server/engine/emulators/s3/index.js.map +1 -1
- package/dist/server/engine/emulators/sqs/index.d.ts +2 -0
- package/dist/server/engine/emulators/sqs/index.d.ts.map +1 -1
- package/dist/server/engine/emulators/sqs/index.js +70 -7
- package/dist/server/engine/emulators/sqs/index.js.map +1 -1
- package/dist/server/engine/emulators/sqs/queue.d.ts +2 -1
- package/dist/server/engine/emulators/sqs/queue.d.ts.map +1 -1
- package/dist/server/engine/emulators/sqs/queue.js +68 -35
- package/dist/server/engine/emulators/sqs/queue.js.map +1 -1
- package/dist/server/engine/emulators/ssm/index.d.ts +31 -0
- package/dist/server/engine/emulators/ssm/index.d.ts.map +1 -0
- package/dist/server/engine/emulators/ssm/index.js +670 -0
- package/dist/server/engine/emulators/ssm/index.js.map +1 -0
- package/dist/server/engine/http/errors.d.ts +1 -0
- package/dist/server/engine/http/errors.d.ts.map +1 -1
- package/dist/server/engine/http/errors.js +13 -5
- package/dist/server/engine/http/errors.js.map +1 -1
- package/dist/server/engine/http/is-aws-request.d.ts.map +1 -1
- package/dist/server/engine/http/is-aws-request.js +13 -3
- package/dist/server/engine/http/is-aws-request.js.map +1 -1
- package/dist/server/engine/http/protocols/aws-json.d.ts +1 -0
- package/dist/server/engine/http/protocols/aws-json.d.ts.map +1 -1
- package/dist/server/engine/http/protocols/aws-json.js +13 -0
- package/dist/server/engine/http/protocols/aws-json.js.map +1 -1
- package/dist/server/engine/http/reset.d.ts +4 -0
- package/dist/server/engine/http/reset.d.ts.map +1 -0
- package/dist/server/engine/http/reset.js +17 -0
- package/dist/server/engine/http/reset.js.map +1 -0
- package/dist/server/engine/http/router.d.ts +1 -0
- package/dist/server/engine/http/router.d.ts.map +1 -1
- package/dist/server/engine/http/router.js +185 -34
- package/dist/server/engine/http/router.js.map +1 -1
- package/dist/server/engine/types.d.ts +2 -1
- package/dist/server/engine/types.d.ts.map +1 -1
- package/dist/server/routes/apis.d.ts.map +1 -1
- package/dist/server/routes/apis.js +6 -0
- package/dist/server/routes/apis.js.map +1 -1
- package/dist/server/routes/config.d.ts.map +1 -1
- package/dist/server/routes/config.js +19 -1
- package/dist/server/routes/config.js.map +1 -1
- package/dist/server/routes/services.d.ts.map +1 -1
- package/dist/server/routes/services.js +49 -1
- package/dist/server/routes/services.js.map +1 -1
- package/dist/server/runtime/runtime-worker.js +33 -3
- package/dist/server/runtime/runtime-worker.js.map +1 -1
- package/dist/server/services/api-gateway-events.d.ts +14 -0
- package/dist/server/services/api-gateway-events.d.ts.map +1 -1
- package/dist/server/services/api-gateway-events.js +114 -8
- package/dist/server/services/api-gateway-events.js.map +1 -1
- package/dist/server/services/artifact-staleness.d.ts +20 -0
- package/dist/server/services/artifact-staleness.d.ts.map +1 -0
- package/dist/server/services/artifact-staleness.js +115 -0
- package/dist/server/services/artifact-staleness.js.map +1 -0
- package/dist/server/services/authorizer-service.d.ts +34 -5
- package/dist/server/services/authorizer-service.d.ts.map +1 -1
- package/dist/server/services/authorizer-service.js +0 -0
- package/dist/server/services/authorizer-service.js.map +1 -1
- package/dist/server/services/cfn-intrinsics.d.ts +63 -0
- package/dist/server/services/cfn-intrinsics.d.ts.map +1 -0
- package/dist/server/services/cfn-intrinsics.js +403 -0
- package/dist/server/services/cfn-intrinsics.js.map +1 -0
- package/dist/server/services/cfn-physical-values.d.ts +51 -0
- package/dist/server/services/cfn-physical-values.d.ts.map +1 -0
- package/dist/server/services/cfn-physical-values.js +186 -0
- package/dist/server/services/cfn-physical-values.js.map +1 -0
- package/dist/server/services/cloudformation-parser.d.ts +26 -3
- package/dist/server/services/cloudformation-parser.d.ts.map +1 -1
- package/dist/server/services/cloudformation-parser.js +76 -4
- package/dist/server/services/cloudformation-parser.js.map +1 -1
- package/dist/server/services/config-manager.d.ts +24 -2
- package/dist/server/services/config-manager.d.ts.map +1 -1
- package/dist/server/services/config-manager.js +60 -7
- package/dist/server/services/config-manager.js.map +1 -1
- package/dist/server/services/gateway-manager.d.ts.map +1 -1
- package/dist/server/services/gateway-manager.js +52 -20
- package/dist/server/services/gateway-manager.js.map +1 -1
- package/dist/server/services/lambda-runtime-manager.d.ts +1 -0
- package/dist/server/services/lambda-runtime-manager.d.ts.map +1 -1
- package/dist/server/services/lambda-runtime-manager.js +36 -14
- package/dist/server/services/lambda-runtime-manager.js.map +1 -1
- package/dist/server/services/raw-api-assembler.d.ts +1 -1
- package/dist/server/services/raw-api-assembler.d.ts.map +1 -1
- package/dist/server/services/raw-api-assembler.js +18 -8
- package/dist/server/services/raw-api-assembler.js.map +1 -1
- package/dist/server/services/resource-provisioner.d.ts +12 -0
- package/dist/server/services/resource-provisioner.d.ts.map +1 -1
- package/dist/server/services/resource-provisioner.js +128 -13
- package/dist/server/services/resource-provisioner.js.map +1 -1
- package/dist/server/services/serverless-state-parser.d.ts +45 -2
- package/dist/server/services/serverless-state-parser.d.ts.map +1 -1
- package/dist/server/services/serverless-state-parser.js +217 -29
- package/dist/server/services/serverless-state-parser.js.map +1 -1
- package/dist/server/services/service-graph.d.ts +76 -0
- package/dist/server/services/service-graph.d.ts.map +1 -0
- package/dist/server/services/service-graph.js +914 -0
- package/dist/server/services/service-graph.js.map +1 -0
- package/dist/server/services/service-registrar.d.ts +4 -0
- package/dist/server/services/service-registrar.d.ts.map +1 -1
- package/dist/server/services/service-registrar.js +129 -55
- package/dist/server/services/service-registrar.js.map +1 -1
- package/dist/server/services/source-watcher.d.ts +1 -0
- package/dist/server/services/source-watcher.d.ts.map +1 -1
- package/dist/server/services/source-watcher.js +19 -1
- package/dist/server/services/source-watcher.js.map +1 -1
- package/dist/server/services/worker-runtime.d.ts +82 -0
- package/dist/server/services/worker-runtime.d.ts.map +1 -0
- package/dist/server/services/worker-runtime.js +303 -0
- package/dist/server/services/worker-runtime.js.map +1 -0
- package/dist/ui/assets/{ApisPage-CTOcNna-.js → ApisPage-BkW-QZ0X.js} +1 -1
- package/dist/ui/assets/BucketDetailPage-CJwTQJh7.js +1 -0
- package/dist/ui/assets/BucketsPage-B1xPVhZC.js +1 -0
- package/dist/ui/assets/{DynamoPage-BnFFKnIU.js → DynamoPage-DvbpwZDc.js} +1 -1
- package/dist/ui/assets/DynamoTablePage-xn3Lq3so.js +3 -0
- package/dist/ui/assets/LambdaDetailPage-BhUyO77H.js +4 -0
- package/dist/ui/assets/{LambdasPage-Bg-MWFBb.js → LambdasPage-CUF_4rOH.js} +1 -1
- package/dist/ui/assets/OnboardingPage-U49THUzu.js +1 -0
- package/dist/ui/assets/OpenSearchCollectionPage-BJTVNCq4.js +1 -0
- package/dist/ui/assets/OpenSearchPage-4s-egkB9.js +1 -0
- package/dist/ui/assets/OverviewPage-DJeJQktG.js +1 -0
- package/dist/ui/assets/{OverviewPage-CSWY71vZ.css → OverviewPage-DjQETmb_.css} +1 -1
- package/dist/ui/assets/QueueDetailPage-DWsy5Yt8.js +1 -0
- package/dist/ui/assets/QueuesPage-BZJSqman.js +1 -0
- package/dist/ui/assets/SecretsPage-YC9ua54G.js +1 -0
- package/dist/ui/assets/ServiceDetailPage-Cum5frP4.js +2 -0
- package/dist/ui/assets/ServicesPage-IpaPDuqx.js +2 -0
- package/dist/ui/assets/SettingsPage-BiCRU8O6.js +1 -0
- package/dist/ui/assets/index-BDlrAJhi.css +1 -0
- package/dist/ui/assets/index-BouKSS43.js +38 -0
- package/dist/ui/assets/resourceIcons-DZs28nbs.js +1 -0
- package/dist/ui/index.html +5 -2
- package/package.json +1 -1
- package/dist/ui/assets/BucketDetailPage-BXW8-FrE.js +0 -1
- package/dist/ui/assets/BucketsPage-DY_ITepb.js +0 -1
- package/dist/ui/assets/DynamoTablePage-BLCuyJHU.js +0 -3
- package/dist/ui/assets/LambdaDetailPage-D4rZNLRT.js +0 -4
- package/dist/ui/assets/OnboardingPage-nnl313Fm.js +0 -1
- package/dist/ui/assets/OpenSearchCollectionPage-Dn15lH_F.js +0 -1
- package/dist/ui/assets/OpenSearchPage-CgD5isok.js +0 -1
- package/dist/ui/assets/OverviewPage-m5sUr8IN.js +0 -1
- package/dist/ui/assets/QueueDetailPage-Ch_FDDoN.js +0 -1
- package/dist/ui/assets/QueuesPage-C_yDuGDu.js +0 -1
- package/dist/ui/assets/SecretsPage-BSGMSoqy.js +0 -1
- package/dist/ui/assets/ServiceDetailPage-6KyohyjI.js +0 -2
- package/dist/ui/assets/ServicesPage-BbSetOMo.js +0 -2
- package/dist/ui/assets/SettingsPage-BgF1jFr6.js +0 -1
- package/dist/ui/assets/index-D627ZyYU.css +0 -1
- package/dist/ui/assets/index-DuSPnI19.js +0 -37
- package/dist/ui/assets/resourceIcons-baecADXA.js +0 -1
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
// SSM Parameter Store emulator (AWS JSON 1.1, X-Amz-Target `AmazonSSM.*`).
|
|
2
|
+
// Covers the Parameter Store surface applications read at boot and tooling
|
|
3
|
+
// writes during setup: PutParameter (String / StringList / SecureString, with
|
|
4
|
+
// Overwrite, AllowedPattern, tiers, data types and create-time tags), the
|
|
5
|
+
// three read paths (GetParameter, GetParameters, GetParametersByPath with
|
|
6
|
+
// Recursive + filters + pagination), DescribeParameters, GetParameterHistory,
|
|
7
|
+
// LabelParameterVersion, DeleteParameter(s) and resource tagging. Parameters
|
|
8
|
+
// persist per region in a catalog, every version kept (up to the AWS limit of
|
|
9
|
+
// 100), so `name:3` and `name:label` selectors resolve exactly like AWS.
|
|
10
|
+
//
|
|
11
|
+
// SecureString and KMS: there is no KMS locally, so values are stored as
|
|
12
|
+
// received and WithDecryption=true returns them verbatim. WithDecryption=false
|
|
13
|
+
// (the AWS default) must NOT hand back the plaintext — AWS returns the
|
|
14
|
+
// ciphertext blob — so the emulator answers a deterministic opaque base64
|
|
15
|
+
// token derived from (name, version, value). It is stable across reads, never
|
|
16
|
+
// decodes to the plaintext, and changes whenever the value does, which is all
|
|
17
|
+
// a caller can legitimately rely on. KeyId is accepted and echoed (default
|
|
18
|
+
// `alias/aws/ssm`), never validated.
|
|
19
|
+
//
|
|
20
|
+
// Not covered (rejected with an explicit AWS-shaped error, or silently
|
|
21
|
+
// accepted where harmless): parameter policies (Expiration/notifications —
|
|
22
|
+
// `Policies` is accepted and ignored, always answered as []), public
|
|
23
|
+
// `/aws/service/...` parameters, cross-account sharing, UnlabelParameterVersion
|
|
24
|
+
// and the asynchronous `aws:ec2:image` value validation.
|
|
25
|
+
import { createHash } from 'crypto';
|
|
26
|
+
import { AwsError, notImplementedOperation } from '../../http/errors.js';
|
|
27
|
+
const PARAMETER_TYPES = ['String', 'StringList', 'SecureString'];
|
|
28
|
+
const TIERS = ['Standard', 'Advanced', 'Intelligent-Tiering'];
|
|
29
|
+
const DATA_TYPES = ['text', 'aws:ec2:image', 'aws:ssm:integration'];
|
|
30
|
+
const DEFAULT_KEY_ID = 'alias/aws/ssm';
|
|
31
|
+
// AWS limits (Systems Manager service quotas, defaults).
|
|
32
|
+
const MAX_NAME_LENGTH = 1011;
|
|
33
|
+
const MAX_HIERARCHY_LEVELS = 15;
|
|
34
|
+
const STANDARD_VALUE_LIMIT = 4096;
|
|
35
|
+
const ADVANCED_VALUE_LIMIT = 8192;
|
|
36
|
+
const MAX_VERSIONS = 100;
|
|
37
|
+
const MAX_STANDARD_PARAMETERS = 10_000;
|
|
38
|
+
const MAX_TAGS = 50;
|
|
39
|
+
const MAX_LABELS_PER_VERSION = 10;
|
|
40
|
+
const MAX_NAMES_PER_BATCH = 10;
|
|
41
|
+
const BY_PATH_MAX_RESULTS = 10;
|
|
42
|
+
const DESCRIBE_MAX_RESULTS = 50;
|
|
43
|
+
const HISTORY_MAX_RESULTS = 50;
|
|
44
|
+
const NAME_CHARS = /^[a-zA-Z0-9_.\-/]+$/;
|
|
45
|
+
const LABEL_CHARS = /^[a-zA-Z0-9_.-]+$/;
|
|
46
|
+
export class SsmEmulator {
|
|
47
|
+
ctx;
|
|
48
|
+
service = 'ssm';
|
|
49
|
+
targetPrefix = 'AmazonSSM';
|
|
50
|
+
jsonVersion = '1.1';
|
|
51
|
+
catalogs = new Map();
|
|
52
|
+
constructor(ctx) {
|
|
53
|
+
this.ctx = ctx;
|
|
54
|
+
}
|
|
55
|
+
async handle(operation, input, req) {
|
|
56
|
+
const region = req.region;
|
|
57
|
+
switch (operation) {
|
|
58
|
+
case 'PutParameter': return this.putParameter(region, input);
|
|
59
|
+
case 'GetParameter': return this.getParameter(region, input);
|
|
60
|
+
case 'GetParameters': return this.getParameters(region, input);
|
|
61
|
+
case 'GetParametersByPath': return this.getParametersByPath(region, input);
|
|
62
|
+
case 'DescribeParameters': return this.describeParameters(region, input);
|
|
63
|
+
case 'GetParameterHistory': return this.getParameterHistory(region, input);
|
|
64
|
+
case 'LabelParameterVersion': return this.labelParameterVersion(region, input);
|
|
65
|
+
case 'DeleteParameter': return this.deleteParameter(region, input);
|
|
66
|
+
case 'DeleteParameters': return this.deleteParameters(region, input);
|
|
67
|
+
case 'AddTagsToResource': return this.addTagsToResource(region, input);
|
|
68
|
+
case 'RemoveTagsFromResource': return this.removeTagsFromResource(region, input);
|
|
69
|
+
case 'ListTagsForResource': return this.listTagsForResource(region, input);
|
|
70
|
+
default: throw notImplementedOperation('ssm', operation);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// -- shared plumbing -------------------------------------------------------
|
|
74
|
+
async catalogFor(region) {
|
|
75
|
+
let catalog = this.catalogs.get(region);
|
|
76
|
+
if (!catalog) {
|
|
77
|
+
catalog = this.ctx.store.catalog(`ssm/${region}/parameters`);
|
|
78
|
+
this.catalogs.set(region, catalog);
|
|
79
|
+
}
|
|
80
|
+
await catalog.load();
|
|
81
|
+
return catalog;
|
|
82
|
+
}
|
|
83
|
+
// ARN shape: the name WITHOUT its leading slash follows `parameter/`, so
|
|
84
|
+
// `/acme/orders/db-password` → `…:parameter/acme/orders/db-password` and a
|
|
85
|
+
// flat `feature-flag` → `…:parameter/feature-flag`.
|
|
86
|
+
arnFor(region, name) {
|
|
87
|
+
const bare = name.startsWith('/') ? name.slice(1) : name;
|
|
88
|
+
return `arn:aws:ssm:${region}:${this.ctx.config.account}:parameter/${bare}`;
|
|
89
|
+
}
|
|
90
|
+
// Every write is attributed to the same principal STS answers for.
|
|
91
|
+
lastModifiedUser() {
|
|
92
|
+
return `arn:aws:iam::${this.ctx.config.account}:root`;
|
|
93
|
+
}
|
|
94
|
+
// Name or full ARN → stored record. An ARN drops the leading slash, so both
|
|
95
|
+
// spellings (`x` and `/x`) are tried — names can't contain `:`, so nothing
|
|
96
|
+
// ambiguous survives the prefix strip.
|
|
97
|
+
findRecord(catalog, reference) {
|
|
98
|
+
const arnMatch = /^arn:[^:]+:ssm:[^:]*:[^:]*:parameter\/(.+)$/.exec(reference);
|
|
99
|
+
if (!arnMatch)
|
|
100
|
+
return catalog.get(reference);
|
|
101
|
+
return catalog.get(arnMatch[1]) ?? catalog.get(`/${arnMatch[1]}`);
|
|
102
|
+
}
|
|
103
|
+
requireRecord(catalog, name) {
|
|
104
|
+
const record = this.findRecord(catalog, name);
|
|
105
|
+
if (!record)
|
|
106
|
+
throw parameterNotFound(name);
|
|
107
|
+
return record;
|
|
108
|
+
}
|
|
109
|
+
// Resolve `name`, `name:<version>` or `name:<label>` to one version. Throws
|
|
110
|
+
// the AWS error GetParameter surfaces; GetParameters maps any throw to an
|
|
111
|
+
// InvalidParameters entry instead.
|
|
112
|
+
resolveSelector(catalog, reference) {
|
|
113
|
+
const { name, selector } = splitSelector(reference);
|
|
114
|
+
const record = this.requireRecord(catalog, name);
|
|
115
|
+
if (selector === undefined)
|
|
116
|
+
return { record, version: currentVersion(record) };
|
|
117
|
+
if (/^\d+$/.test(selector)) {
|
|
118
|
+
const wanted = Number(selector);
|
|
119
|
+
const version = record.versions.find((v) => v.version === wanted);
|
|
120
|
+
if (!version) {
|
|
121
|
+
throw new AwsError('ParameterVersionNotFound', `Systems Manager could not find version ${wanted} of ${record.name}. Verify the version and try again.`);
|
|
122
|
+
}
|
|
123
|
+
return { record, version, selector: `:${selector}` };
|
|
124
|
+
}
|
|
125
|
+
const version = record.versions.find((v) => v.labels.includes(selector));
|
|
126
|
+
if (!version)
|
|
127
|
+
throw parameterNotFound(reference);
|
|
128
|
+
return { record, version, selector: `:${selector}` };
|
|
129
|
+
}
|
|
130
|
+
toParameter(region, record, version, withDecryption, selector) {
|
|
131
|
+
return {
|
|
132
|
+
Name: record.name,
|
|
133
|
+
Type: version.type,
|
|
134
|
+
Value: renderValue(record.name, version, withDecryption),
|
|
135
|
+
Version: version.version,
|
|
136
|
+
Selector: selector,
|
|
137
|
+
LastModifiedDate: epochSeconds(version.lastModifiedMs),
|
|
138
|
+
ARN: this.arnFor(region, record.name),
|
|
139
|
+
DataType: version.dataType,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// -- operations ------------------------------------------------------------
|
|
143
|
+
async putParameter(region, input) {
|
|
144
|
+
const name = requireString(input, 'Name');
|
|
145
|
+
validateName(name);
|
|
146
|
+
const value = requireString(input, 'Value');
|
|
147
|
+
const overwrite = input.Overwrite === true;
|
|
148
|
+
const tags = readTags(input.Tags);
|
|
149
|
+
if (overwrite && tags.length > 0) {
|
|
150
|
+
throw validation('Invalid request: tags and overwrite can\'t be used together. To create a parameter with tags, please ' +
|
|
151
|
+
'remove overwrite flag. To update tags for an existing parameter, please use AddTagsToResource or ' +
|
|
152
|
+
'RemoveTagsFromResource.');
|
|
153
|
+
}
|
|
154
|
+
if (tags.length > MAX_TAGS)
|
|
155
|
+
throw tooManyTags();
|
|
156
|
+
const catalog = await this.catalogFor(region);
|
|
157
|
+
const existing = catalog.get(name);
|
|
158
|
+
if (existing && !overwrite) {
|
|
159
|
+
throw new AwsError('ParameterAlreadyExists', 'The parameter already exists. To overwrite this request value, set the overwrite option in the request to true.');
|
|
160
|
+
}
|
|
161
|
+
const previous = existing ? currentVersion(existing) : undefined;
|
|
162
|
+
if (!existing && catalog.keys().length >= MAX_STANDARD_PARAMETERS) {
|
|
163
|
+
throw new AwsError('ParameterLimitExceeded', 'You have exceeded the number of parameters for this AWS account. Delete one or more parameters and try again.');
|
|
164
|
+
}
|
|
165
|
+
// Type is required on create; on overwrite an omitted Type keeps the
|
|
166
|
+
// current one (the modern AWS API made it optional for that case).
|
|
167
|
+
const rawType = input.Type;
|
|
168
|
+
if (rawType === undefined && !previous)
|
|
169
|
+
throw validation('A parameter type is required when you create a parameter.');
|
|
170
|
+
if (rawType !== undefined && !PARAMETER_TYPES.includes(rawType)) {
|
|
171
|
+
throw new AwsError('UnsupportedParameterType', `The parameter type ${String(rawType)} is not supported.`);
|
|
172
|
+
}
|
|
173
|
+
const type = rawType ?? previous.type;
|
|
174
|
+
const explicitKeyId = optionalString(input, 'KeyId');
|
|
175
|
+
if (explicitKeyId !== undefined && type !== 'SecureString') {
|
|
176
|
+
throw validation('KeyId is required for SecureString type parameter only.');
|
|
177
|
+
}
|
|
178
|
+
const keyId = type === 'SecureString'
|
|
179
|
+
? explicitKeyId ?? (previous?.type === 'SecureString' ? previous.keyId : undefined) ?? DEFAULT_KEY_ID
|
|
180
|
+
: undefined;
|
|
181
|
+
const dataType = optionalString(input, 'DataType') ?? previous?.dataType ?? 'text';
|
|
182
|
+
if (!DATA_TYPES.includes(dataType)) {
|
|
183
|
+
throw validation(`The following data type is not supported: ${dataType} (Data type names are all lowercase.)`);
|
|
184
|
+
}
|
|
185
|
+
// An omitted Description / AllowedPattern on overwrite keeps the current
|
|
186
|
+
// one, so a value-only rotation doesn't silently drop the constraint.
|
|
187
|
+
const description = input.Description !== undefined ? optionalString(input, 'Description') : previous?.description;
|
|
188
|
+
const allowedPattern = input.AllowedPattern !== undefined ? optionalString(input, 'AllowedPattern') : previous?.allowedPattern;
|
|
189
|
+
if (allowedPattern !== undefined)
|
|
190
|
+
enforcePattern(allowedPattern, value);
|
|
191
|
+
const tier = resolveTier(input.Tier, value, previous?.tier);
|
|
192
|
+
const nowMs = Date.now();
|
|
193
|
+
const record = existing ?? { name, tags, versions: [] };
|
|
194
|
+
const version = {
|
|
195
|
+
version: previous ? previous.version + 1 : 1,
|
|
196
|
+
value,
|
|
197
|
+
type,
|
|
198
|
+
description,
|
|
199
|
+
keyId,
|
|
200
|
+
allowedPattern,
|
|
201
|
+
dataType,
|
|
202
|
+
tier,
|
|
203
|
+
labels: [],
|
|
204
|
+
lastModifiedMs: nowMs,
|
|
205
|
+
};
|
|
206
|
+
record.versions.push(version);
|
|
207
|
+
// AWS keeps the 100 most recent versions; older ones age out on write.
|
|
208
|
+
if (record.versions.length > MAX_VERSIONS)
|
|
209
|
+
record.versions = record.versions.slice(-MAX_VERSIONS);
|
|
210
|
+
catalog.set(name, record);
|
|
211
|
+
return { Version: version.version, Tier: version.tier };
|
|
212
|
+
}
|
|
213
|
+
async getParameter(region, input) {
|
|
214
|
+
const reference = requireString(input, 'Name');
|
|
215
|
+
const catalog = await this.catalogFor(region);
|
|
216
|
+
const { record, version, selector } = this.resolveSelector(catalog, reference);
|
|
217
|
+
return { Parameter: this.toParameter(region, record, version, input.WithDecryption === true, selector) };
|
|
218
|
+
}
|
|
219
|
+
async getParameters(region, input) {
|
|
220
|
+
const names = requireNameList(input, 'Names');
|
|
221
|
+
const catalog = await this.catalogFor(region);
|
|
222
|
+
const parameters = [];
|
|
223
|
+
const invalid = [];
|
|
224
|
+
for (const reference of names) {
|
|
225
|
+
try {
|
|
226
|
+
const { record, version, selector } = this.resolveSelector(catalog, reference);
|
|
227
|
+
parameters.push(this.toParameter(region, record, version, input.WithDecryption === true, selector));
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
invalid.push(reference);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return { Parameters: parameters, InvalidParameters: invalid };
|
|
234
|
+
}
|
|
235
|
+
async getParametersByPath(region, input) {
|
|
236
|
+
const path = requireString(input, 'Path');
|
|
237
|
+
validatePath(path);
|
|
238
|
+
const recursive = input.Recursive === true;
|
|
239
|
+
const filters = readFilters(input.ParameterFilters, 'by-path');
|
|
240
|
+
const maxResults = readMaxResults(input, BY_PATH_MAX_RESULTS);
|
|
241
|
+
const withDecryption = input.WithDecryption === true;
|
|
242
|
+
const prefix = path.endsWith('/') ? path : `${path}/`;
|
|
243
|
+
const catalog = await this.catalogFor(region);
|
|
244
|
+
const matches = [];
|
|
245
|
+
for (const record of sortedRecords(catalog)) {
|
|
246
|
+
if (!record.name.startsWith(prefix))
|
|
247
|
+
continue;
|
|
248
|
+
if (!recursive && record.name.slice(prefix.length).includes('/'))
|
|
249
|
+
continue;
|
|
250
|
+
// A Label filter answers the labeled version, not the current one.
|
|
251
|
+
const labelFilter = filters.find((f) => f.Key === 'Label');
|
|
252
|
+
const version = labelFilter
|
|
253
|
+
? record.versions.find((v) => v.labels.some((l) => labelFilter.Values.includes(l)))
|
|
254
|
+
: currentVersion(record);
|
|
255
|
+
if (!version)
|
|
256
|
+
continue;
|
|
257
|
+
if (!filters.every((f) => f.Key === 'Label' || matchesFilter(f, record, version)))
|
|
258
|
+
continue;
|
|
259
|
+
matches.push({ record, version });
|
|
260
|
+
}
|
|
261
|
+
const { page, nextToken } = paginate(matches, input.NextToken, maxResults);
|
|
262
|
+
return {
|
|
263
|
+
Parameters: page.map(({ record, version }) => this.toParameter(region, record, version, withDecryption)),
|
|
264
|
+
NextToken: nextToken,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async describeParameters(region, input) {
|
|
268
|
+
const filters = readFilters(input.ParameterFilters, 'describe');
|
|
269
|
+
const maxResults = readMaxResults(input, DESCRIBE_MAX_RESULTS);
|
|
270
|
+
const catalog = await this.catalogFor(region);
|
|
271
|
+
const matches = sortedRecords(catalog).filter((record) => {
|
|
272
|
+
const version = currentVersion(record);
|
|
273
|
+
return filters.every((f) => matchesFilter(f, record, version));
|
|
274
|
+
});
|
|
275
|
+
const { page, nextToken } = paginate(matches, input.NextToken, maxResults);
|
|
276
|
+
return {
|
|
277
|
+
Parameters: page.map((record) => {
|
|
278
|
+
const version = currentVersion(record);
|
|
279
|
+
return {
|
|
280
|
+
Name: record.name,
|
|
281
|
+
ARN: this.arnFor(region, record.name),
|
|
282
|
+
Type: version.type,
|
|
283
|
+
KeyId: version.keyId,
|
|
284
|
+
LastModifiedDate: epochSeconds(version.lastModifiedMs),
|
|
285
|
+
LastModifiedUser: this.lastModifiedUser(),
|
|
286
|
+
Description: version.description,
|
|
287
|
+
AllowedPattern: version.allowedPattern,
|
|
288
|
+
Version: version.version,
|
|
289
|
+
Tier: version.tier,
|
|
290
|
+
Policies: [],
|
|
291
|
+
DataType: version.dataType,
|
|
292
|
+
};
|
|
293
|
+
}),
|
|
294
|
+
NextToken: nextToken,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
async getParameterHistory(region, input) {
|
|
298
|
+
const name = requireString(input, 'Name');
|
|
299
|
+
const maxResults = readMaxResults(input, HISTORY_MAX_RESULTS);
|
|
300
|
+
const withDecryption = input.WithDecryption === true;
|
|
301
|
+
const catalog = await this.catalogFor(region);
|
|
302
|
+
const record = this.requireRecord(catalog, name);
|
|
303
|
+
const { page, nextToken } = paginate(record.versions, input.NextToken, maxResults);
|
|
304
|
+
return {
|
|
305
|
+
Parameters: page.map((version) => ({
|
|
306
|
+
Name: record.name,
|
|
307
|
+
Type: version.type,
|
|
308
|
+
KeyId: version.keyId,
|
|
309
|
+
LastModifiedDate: epochSeconds(version.lastModifiedMs),
|
|
310
|
+
LastModifiedUser: this.lastModifiedUser(),
|
|
311
|
+
Description: version.description,
|
|
312
|
+
Value: renderValue(record.name, version, withDecryption),
|
|
313
|
+
AllowedPattern: version.allowedPattern,
|
|
314
|
+
Version: version.version,
|
|
315
|
+
Labels: [...version.labels],
|
|
316
|
+
Tier: version.tier,
|
|
317
|
+
Policies: [],
|
|
318
|
+
DataType: version.dataType,
|
|
319
|
+
})),
|
|
320
|
+
NextToken: nextToken,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
// A label belongs to one version at a time: attaching it moves it off any
|
|
324
|
+
// other version of the same parameter. Malformed labels are not an error —
|
|
325
|
+
// AWS reports them in InvalidLabels and applies the rest.
|
|
326
|
+
async labelParameterVersion(region, input) {
|
|
327
|
+
const name = requireString(input, 'Name');
|
|
328
|
+
const labels = input.Labels;
|
|
329
|
+
if (!Array.isArray(labels) || labels.length === 0 || labels.length > MAX_LABELS_PER_VERSION) {
|
|
330
|
+
throw validation(`Labels must contain between 1 and ${MAX_LABELS_PER_VERSION} labels.`);
|
|
331
|
+
}
|
|
332
|
+
const catalog = await this.catalogFor(region);
|
|
333
|
+
const record = this.requireRecord(catalog, name);
|
|
334
|
+
let version = currentVersion(record);
|
|
335
|
+
if (input.ParameterVersion !== undefined) {
|
|
336
|
+
const wanted = Number(input.ParameterVersion);
|
|
337
|
+
const found = record.versions.find((v) => v.version === wanted);
|
|
338
|
+
if (!found) {
|
|
339
|
+
throw new AwsError('ParameterVersionNotFound', `Systems Manager could not find version ${wanted} of ${record.name}. Verify the version and try again.`);
|
|
340
|
+
}
|
|
341
|
+
version = found;
|
|
342
|
+
}
|
|
343
|
+
const valid = labels.map(String).filter(isValidLabel);
|
|
344
|
+
const invalid = labels.map(String).filter((label) => !isValidLabel(label));
|
|
345
|
+
const merged = [...new Set([...version.labels, ...valid])];
|
|
346
|
+
if (merged.length > MAX_LABELS_PER_VERSION) {
|
|
347
|
+
throw new AwsError('ParameterVersionLabelLimitExceeded', `A parameter version can have maximum ${MAX_LABELS_PER_VERSION} labels. Move one or more labels to another version and try again.`);
|
|
348
|
+
}
|
|
349
|
+
for (const other of record.versions) {
|
|
350
|
+
if (other !== version)
|
|
351
|
+
other.labels = other.labels.filter((label) => !valid.includes(label));
|
|
352
|
+
}
|
|
353
|
+
version.labels = merged;
|
|
354
|
+
catalog.set(record.name, record);
|
|
355
|
+
return { InvalidLabels: invalid, ParameterVersion: version.version };
|
|
356
|
+
}
|
|
357
|
+
async deleteParameter(region, input) {
|
|
358
|
+
const name = requireString(input, 'Name');
|
|
359
|
+
const catalog = await this.catalogFor(region);
|
|
360
|
+
const record = this.requireRecord(catalog, name);
|
|
361
|
+
catalog.delete(record.name);
|
|
362
|
+
return {};
|
|
363
|
+
}
|
|
364
|
+
async deleteParameters(region, input) {
|
|
365
|
+
const names = requireNameList(input, 'Names');
|
|
366
|
+
const catalog = await this.catalogFor(region);
|
|
367
|
+
const deleted = [];
|
|
368
|
+
const invalid = [];
|
|
369
|
+
for (const name of names) {
|
|
370
|
+
const record = this.findRecord(catalog, name);
|
|
371
|
+
if (record) {
|
|
372
|
+
catalog.delete(record.name);
|
|
373
|
+
deleted.push(name);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
invalid.push(name);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return { DeletedParameters: deleted, InvalidParameters: invalid };
|
|
380
|
+
}
|
|
381
|
+
async taggedRecord(region, input) {
|
|
382
|
+
const resourceType = input.ResourceType;
|
|
383
|
+
if (resourceType !== 'Parameter') {
|
|
384
|
+
throw new AwsError('InvalidResourceType', `The resource type ${String(resourceType)} is not supported by the LSS self engine — only Parameter is.`);
|
|
385
|
+
}
|
|
386
|
+
const resourceId = requireString(input, 'ResourceId');
|
|
387
|
+
const catalog = await this.catalogFor(region);
|
|
388
|
+
const record = this.findRecord(catalog, resourceId);
|
|
389
|
+
if (!record)
|
|
390
|
+
throw new AwsError('InvalidResourceId', 'The resource ID is not valid. Verify the ID and try again.');
|
|
391
|
+
return { catalog, record };
|
|
392
|
+
}
|
|
393
|
+
async addTagsToResource(region, input) {
|
|
394
|
+
const { catalog, record } = await this.taggedRecord(region, input);
|
|
395
|
+
const tags = readTags(input.Tags);
|
|
396
|
+
if (tags.length === 0)
|
|
397
|
+
throw validation('Tags must contain at least one tag.');
|
|
398
|
+
// Merge into a copy so a rejected call leaves the stored tags untouched.
|
|
399
|
+
const merged = record.tags.map((tag) => ({ ...tag }));
|
|
400
|
+
for (const tag of tags) {
|
|
401
|
+
const existing = merged.find((t) => t.Key === tag.Key);
|
|
402
|
+
if (existing)
|
|
403
|
+
existing.Value = tag.Value;
|
|
404
|
+
else
|
|
405
|
+
merged.push(tag);
|
|
406
|
+
}
|
|
407
|
+
if (merged.length > MAX_TAGS)
|
|
408
|
+
throw tooManyTags();
|
|
409
|
+
record.tags = merged;
|
|
410
|
+
catalog.set(record.name, record);
|
|
411
|
+
return {};
|
|
412
|
+
}
|
|
413
|
+
async removeTagsFromResource(region, input) {
|
|
414
|
+
const { catalog, record } = await this.taggedRecord(region, input);
|
|
415
|
+
const keys = Array.isArray(input.TagKeys) ? input.TagKeys.map(String) : [];
|
|
416
|
+
record.tags = record.tags.filter((tag) => !keys.includes(tag.Key));
|
|
417
|
+
catalog.set(record.name, record);
|
|
418
|
+
return {};
|
|
419
|
+
}
|
|
420
|
+
async listTagsForResource(region, input) {
|
|
421
|
+
const { record } = await this.taggedRecord(region, input);
|
|
422
|
+
return { TagList: record.tags.map((tag) => ({ Key: tag.Key, Value: tag.Value })) };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
// Pure helpers
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
function currentVersion(record) {
|
|
429
|
+
return record.versions[record.versions.length - 1];
|
|
430
|
+
}
|
|
431
|
+
function sortedRecords(catalog) {
|
|
432
|
+
// Names are unique catalog keys, so the comparator never sees a tie.
|
|
433
|
+
return catalog.values().sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
434
|
+
}
|
|
435
|
+
// `name:selector` — names can't contain `:`, but ARNs do, so the selector is
|
|
436
|
+
// whatever follows the LAST colon only when it carries no `/` (an ARN's own
|
|
437
|
+
// last colon is followed by `parameter/…`).
|
|
438
|
+
function splitSelector(reference) {
|
|
439
|
+
const colon = reference.lastIndexOf(':');
|
|
440
|
+
if (colon === -1)
|
|
441
|
+
return { name: reference };
|
|
442
|
+
const tail = reference.slice(colon + 1);
|
|
443
|
+
if (tail.includes('/'))
|
|
444
|
+
return { name: reference };
|
|
445
|
+
return { name: reference.slice(0, colon), selector: tail };
|
|
446
|
+
}
|
|
447
|
+
// AWS name rules: [a-zA-Z0-9_.-/] only, at most 1011 characters, a `/` anywhere
|
|
448
|
+
// means a hierarchy (which must then be fully qualified: leading `/`, no empty
|
|
449
|
+
// or trailing segment, at most 15 levels), and neither the name nor its first
|
|
450
|
+
// hierarchy segment may start with `aws` or `ssm` (case-insensitive) — those
|
|
451
|
+
// prefixes are reserved for AWS-owned parameters.
|
|
452
|
+
function validateName(name) {
|
|
453
|
+
if (name.length > MAX_NAME_LENGTH) {
|
|
454
|
+
throw validation(`Parameter name must be less than or equal to ${MAX_NAME_LENGTH} characters.`);
|
|
455
|
+
}
|
|
456
|
+
if (!NAME_CHARS.test(name)) {
|
|
457
|
+
throw validation('Parameter name: can\'t contain spaces or special characters. Only letters, numbers, and the symbols ' +
|
|
458
|
+
'"_", ".", "-" and "/" are allowed.');
|
|
459
|
+
}
|
|
460
|
+
if (name.includes('/')) {
|
|
461
|
+
if (!name.startsWith('/') || name.endsWith('/') || name.slice(1).split('/').some((segment) => segment === '')) {
|
|
462
|
+
throw validation('Parameter name must be a fully qualified name.');
|
|
463
|
+
}
|
|
464
|
+
if (name.slice(1).split('/').length > MAX_HIERARCHY_LEVELS) {
|
|
465
|
+
throw new AwsError('HierarchyLevelLimitExceededException', `A hierarchy can have a maximum of ${MAX_HIERARCHY_LEVELS} levels. You can't create additional levels.`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const head = (name.startsWith('/') ? name.slice(1) : name).toLowerCase();
|
|
469
|
+
if (head.startsWith('aws') || head.startsWith('ssm')) {
|
|
470
|
+
throw validation('Parameter name: can\'t be prefixed with "aws" or "ssm" (case-insensitive).');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// GetParametersByPath: `/` alone is valid; otherwise the same hierarchy shape
|
|
474
|
+
// as a name (a trailing slash is tolerated, AWS normalizes it away).
|
|
475
|
+
function validatePath(path) {
|
|
476
|
+
if (path === '/')
|
|
477
|
+
return;
|
|
478
|
+
const trimmed = path.endsWith('/') ? path.slice(0, -1) : path;
|
|
479
|
+
if (!path.startsWith('/') || !NAME_CHARS.test(path) || trimmed.slice(1).split('/').some((segment) => segment === '')) {
|
|
480
|
+
throw validation(`The parameter doesn't meet the parameter name requirements. The parameter name must begin with a forward slash "/". Path: ${path}`);
|
|
481
|
+
}
|
|
482
|
+
if (trimmed.slice(1).split('/').length > MAX_HIERARCHY_LEVELS) {
|
|
483
|
+
throw new AwsError('HierarchyLevelLimitExceededException', `A hierarchy can have a maximum of ${MAX_HIERARCHY_LEVELS} levels. You can't create additional levels.`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// AllowedPattern is a regular expression the value must satisfy (search
|
|
487
|
+
// semantics, so patterns carry their own ^…$ anchors as AWS examples do).
|
|
488
|
+
function enforcePattern(pattern, value) {
|
|
489
|
+
let regex;
|
|
490
|
+
try {
|
|
491
|
+
regex = new RegExp(pattern);
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
throw new AwsError('InvalidAllowedPatternException', `The request doesn't meet the regular expression requirement: ${pattern}`);
|
|
495
|
+
}
|
|
496
|
+
if (!regex.test(value)) {
|
|
497
|
+
throw new AwsError('ParameterPatternMismatchException', `Parameter value, cannot be validated against allowedPattern: ${pattern}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// Standard holds 4 KB, Advanced 8 KB. Intelligent-Tiering picks the cheapest
|
|
501
|
+
// tier that fits. A parameter never moves back down: AWS refuses an explicit
|
|
502
|
+
// Advanced → Standard downgrade, and an omitted Tier keeps the current one.
|
|
503
|
+
function resolveTier(raw, value, previous) {
|
|
504
|
+
if (raw !== undefined && !TIERS.includes(raw)) {
|
|
505
|
+
throw validation(`Tier ${String(raw)} is not valid. Valid values: ${TIERS.join(', ')}.`);
|
|
506
|
+
}
|
|
507
|
+
if (value.length > ADVANCED_VALUE_LIMIT) {
|
|
508
|
+
throw validation(`Parameter value must be less than or equal to ${ADVANCED_VALUE_LIMIT} characters.`);
|
|
509
|
+
}
|
|
510
|
+
const needsAdvanced = value.length > STANDARD_VALUE_LIMIT || previous === 'Advanced';
|
|
511
|
+
if (raw === 'Standard') {
|
|
512
|
+
if (previous === 'Advanced') {
|
|
513
|
+
throw validation('This parameter uses the advanced-parameter tier. You can\'t downgrade a parameter from the advanced-parameter tier to the standard-parameter tier.');
|
|
514
|
+
}
|
|
515
|
+
if (needsAdvanced) {
|
|
516
|
+
throw validation(`Standard tier parameters support a maximum parameter value of ${STANDARD_VALUE_LIMIT} characters. ` +
|
|
517
|
+
'To create a larger parameter value, upgrade the parameter to use the advanced-parameter tier.');
|
|
518
|
+
}
|
|
519
|
+
return 'Standard';
|
|
520
|
+
}
|
|
521
|
+
if (raw === 'Advanced')
|
|
522
|
+
return 'Advanced';
|
|
523
|
+
return needsAdvanced ? 'Advanced' : 'Standard';
|
|
524
|
+
}
|
|
525
|
+
// See the header: an encrypted read without decryption gets an opaque,
|
|
526
|
+
// deterministic stand-in for the KMS ciphertext, never the plaintext.
|
|
527
|
+
function renderValue(name, version, withDecryption) {
|
|
528
|
+
if (version.type !== 'SecureString' || withDecryption)
|
|
529
|
+
return version.value;
|
|
530
|
+
const digest = createHash('sha256').update(`${name}\0${version.version}\0${version.value}`).digest();
|
|
531
|
+
return Buffer.concat([Buffer.from('lss-ssm-securestring:'), digest]).toString('base64');
|
|
532
|
+
}
|
|
533
|
+
function isValidLabel(label) {
|
|
534
|
+
if (label.length === 0 || label.length > 100)
|
|
535
|
+
return false;
|
|
536
|
+
if (!LABEL_CHARS.test(label) || /^\d/.test(label))
|
|
537
|
+
return false;
|
|
538
|
+
const lower = label.toLowerCase();
|
|
539
|
+
return !lower.startsWith('aws') && !lower.startsWith('ssm');
|
|
540
|
+
}
|
|
541
|
+
// ParameterFilters. GetParametersByPath accepts Type / KeyId / Label /
|
|
542
|
+
// DataType; DescribeParameters adds Name, Path, Tier and `tag:<key>` but not
|
|
543
|
+
// Label. Options: Equals everywhere, BeginsWith / Contains for Name, and
|
|
544
|
+
// OneLevel / Recursive for Path.
|
|
545
|
+
function readFilters(raw, mode) {
|
|
546
|
+
if (raw === undefined)
|
|
547
|
+
return [];
|
|
548
|
+
if (!Array.isArray(raw))
|
|
549
|
+
throw validation('ParameterFilters must be a list.');
|
|
550
|
+
const allowed = mode === 'by-path'
|
|
551
|
+
? ['Type', 'KeyId', 'Label', 'DataType']
|
|
552
|
+
: ['Name', 'Type', 'KeyId', 'Path', 'Tier', 'DataType'];
|
|
553
|
+
return raw.map((entry) => {
|
|
554
|
+
const filter = entry;
|
|
555
|
+
const key = String(filter.Key ?? '');
|
|
556
|
+
const isTag = mode === 'describe' && key.startsWith('tag:') && key.length > 4;
|
|
557
|
+
if (!allowed.includes(key) && !isTag) {
|
|
558
|
+
throw new AwsError('InvalidFilterKey', `The following filter key is not valid: ${key}`);
|
|
559
|
+
}
|
|
560
|
+
const defaultOption = key === 'Path' ? 'OneLevel' : 'Equals';
|
|
561
|
+
const option = filter.Option === undefined ? defaultOption : String(filter.Option);
|
|
562
|
+
const options = key === 'Name' ? ['Equals', 'BeginsWith', 'Contains'] : key === 'Path' ? ['OneLevel', 'Recursive'] : ['Equals'];
|
|
563
|
+
if (!options.includes(option)) {
|
|
564
|
+
throw new AwsError('InvalidFilterOption', `The filter option ${option} is not valid for key ${key}. Valid options: ${options.join(', ')}.`);
|
|
565
|
+
}
|
|
566
|
+
const values = Array.isArray(filter.Values) ? filter.Values.map(String) : [];
|
|
567
|
+
if (values.length === 0 && !isTag) {
|
|
568
|
+
throw new AwsError('InvalidFilterValue', `The filter key ${key} requires at least one value.`);
|
|
569
|
+
}
|
|
570
|
+
return { Key: key, Option: option, Values: values };
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
function matchesFilter(filter, record, version) {
|
|
574
|
+
const { Key: key, Option: option, Values: values } = filter;
|
|
575
|
+
switch (key) {
|
|
576
|
+
case 'Name':
|
|
577
|
+
return values.some((value) => {
|
|
578
|
+
// AWS matches Name filters with or without the leading slash.
|
|
579
|
+
const wanted = value.startsWith('/') || !record.name.startsWith('/') ? value : `/${value}`;
|
|
580
|
+
if (option === 'BeginsWith')
|
|
581
|
+
return record.name.startsWith(wanted);
|
|
582
|
+
if (option === 'Contains')
|
|
583
|
+
return record.name.includes(value);
|
|
584
|
+
return record.name === wanted;
|
|
585
|
+
});
|
|
586
|
+
case 'Type': return values.includes(version.type);
|
|
587
|
+
case 'KeyId': return version.keyId !== undefined && values.includes(version.keyId);
|
|
588
|
+
case 'Tier': return values.includes(version.tier);
|
|
589
|
+
case 'DataType': return values.includes(version.dataType);
|
|
590
|
+
case 'Path':
|
|
591
|
+
return values.some((value) => {
|
|
592
|
+
const prefix = value.endsWith('/') ? value : `${value}/`;
|
|
593
|
+
if (!record.name.startsWith(prefix))
|
|
594
|
+
return false;
|
|
595
|
+
return option === 'Recursive' || !record.name.slice(prefix.length).includes('/');
|
|
596
|
+
});
|
|
597
|
+
default: {
|
|
598
|
+
// tag:<key> — presence when no values are given, else value match.
|
|
599
|
+
const tag = record.tags.find((t) => t.Key === key.slice(4));
|
|
600
|
+
return tag !== undefined && (values.length === 0 || values.includes(tag.Value));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function readMaxResults(input, max) {
|
|
605
|
+
const raw = input.MaxResults;
|
|
606
|
+
if (raw === undefined)
|
|
607
|
+
return max;
|
|
608
|
+
if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 1 || raw > max) {
|
|
609
|
+
throw validation(`1 validation error detected: Value '${String(raw)}' at 'maxResults' failed to satisfy constraint: Member must have value less than or equal to ${max}`);
|
|
610
|
+
}
|
|
611
|
+
return raw;
|
|
612
|
+
}
|
|
613
|
+
// NextToken is an opaque base64 offset into the (stably sorted) result list.
|
|
614
|
+
function paginate(items, rawToken, maxResults) {
|
|
615
|
+
let offset = 0;
|
|
616
|
+
if (rawToken !== undefined) {
|
|
617
|
+
const decoded = typeof rawToken === 'string' ? Buffer.from(rawToken, 'base64').toString('utf8') : '';
|
|
618
|
+
const match = /^lss-ssm:(\d+)$/.exec(decoded);
|
|
619
|
+
if (!match)
|
|
620
|
+
throw new AwsError('InvalidNextToken', 'The specified token isn\'t valid.');
|
|
621
|
+
offset = Number(match[1]);
|
|
622
|
+
}
|
|
623
|
+
const page = items.slice(offset, offset + maxResults);
|
|
624
|
+
const next = offset + maxResults;
|
|
625
|
+
return { page, nextToken: next < items.length ? Buffer.from(`lss-ssm:${next}`).toString('base64') : undefined };
|
|
626
|
+
}
|
|
627
|
+
function readTags(raw) {
|
|
628
|
+
if (!Array.isArray(raw))
|
|
629
|
+
return [];
|
|
630
|
+
return raw.map((tag) => {
|
|
631
|
+
const entry = tag;
|
|
632
|
+
// Key is required by the API model; Value may be empty.
|
|
633
|
+
if (typeof entry.Key !== 'string' || entry.Key.length === 0)
|
|
634
|
+
throw validation('Tag keys must be non-empty strings.');
|
|
635
|
+
return { Key: entry.Key, Value: String(entry.Value ?? '') };
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
function requireNameList(input, field) {
|
|
639
|
+
const raw = input[field];
|
|
640
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.length > MAX_NAMES_PER_BATCH || !raw.every((n) => typeof n === 'string')) {
|
|
641
|
+
throw validation(`${field} must contain between 1 and ${MAX_NAMES_PER_BATCH} parameter names.`);
|
|
642
|
+
}
|
|
643
|
+
return raw;
|
|
644
|
+
}
|
|
645
|
+
function requireString(input, field) {
|
|
646
|
+
const value = input[field];
|
|
647
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
648
|
+
throw validation(`1 validation error detected: Value null at '${field.charAt(0).toLowerCase()}${field.slice(1)}' failed to satisfy constraint: Member must not be null`);
|
|
649
|
+
}
|
|
650
|
+
return value;
|
|
651
|
+
}
|
|
652
|
+
function optionalString(input, field) {
|
|
653
|
+
const value = input[field];
|
|
654
|
+
return typeof value === 'string' ? value : undefined;
|
|
655
|
+
}
|
|
656
|
+
// SSM timestamps are epoch seconds with millisecond precision (a float on the
|
|
657
|
+
// wire), unlike Secrets Manager's whole seconds.
|
|
658
|
+
function epochSeconds(ms) {
|
|
659
|
+
return ms / 1000;
|
|
660
|
+
}
|
|
661
|
+
function parameterNotFound(name) {
|
|
662
|
+
return new AwsError('ParameterNotFound', `Parameter ${name} not found.`);
|
|
663
|
+
}
|
|
664
|
+
function tooManyTags() {
|
|
665
|
+
return new AwsError('TooManyTagsError', `The resource has too many tags. A parameter can have a maximum of ${MAX_TAGS} tags; remove one or more and try again.`);
|
|
666
|
+
}
|
|
667
|
+
function validation(message) {
|
|
668
|
+
return new AwsError('ValidationException', message);
|
|
669
|
+
}
|
|
670
|
+
//# sourceMappingURL=index.js.map
|