@aws-blocks/bb-app-setting 0.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.
Files changed (43) hide show
  1. package/LICENSE +174 -0
  2. package/README.md +156 -0
  3. package/dist/errors.d.ts +25 -0
  4. package/dist/errors.d.ts.map +1 -0
  5. package/dist/errors.js +26 -0
  6. package/dist/index.aws.d.ts +76 -0
  7. package/dist/index.aws.d.ts.map +1 -0
  8. package/dist/index.aws.js +135 -0
  9. package/dist/index.browser.d.ts +5 -0
  10. package/dist/index.browser.d.ts.map +1 -0
  11. package/dist/index.browser.js +9 -0
  12. package/dist/index.cdk.d.ts +35 -0
  13. package/dist/index.cdk.d.ts.map +1 -0
  14. package/dist/index.cdk.js +242 -0
  15. package/dist/index.cdk.test.d.ts +2 -0
  16. package/dist/index.cdk.test.d.ts.map +1 -0
  17. package/dist/index.cdk.test.js +187 -0
  18. package/dist/index.hooks.d.ts +2 -0
  19. package/dist/index.hooks.d.ts.map +1 -0
  20. package/dist/index.hooks.js +3 -0
  21. package/dist/index.mock.d.ts +77 -0
  22. package/dist/index.mock.d.ts.map +1 -0
  23. package/dist/index.mock.js +153 -0
  24. package/dist/index.test.d.ts +2 -0
  25. package/dist/index.test.d.ts.map +1 -0
  26. package/dist/index.test.js +542 -0
  27. package/dist/types.d.ts +52 -0
  28. package/dist/types.d.ts.map +1 -0
  29. package/dist/types.js +3 -0
  30. package/dist/version.d.ts +3 -0
  31. package/dist/version.d.ts.map +1 -0
  32. package/dist/version.js +3 -0
  33. package/package.json +46 -0
  34. package/src/errors.ts +27 -0
  35. package/src/index.aws.ts +165 -0
  36. package/src/index.browser.ts +10 -0
  37. package/src/index.cdk.test.ts +227 -0
  38. package/src/index.cdk.ts +291 -0
  39. package/src/index.hooks.ts +5 -0
  40. package/src/index.mock.ts +181 -0
  41. package/src/index.test.ts +627 -0
  42. package/src/types.ts +56 -0
  43. package/src/version.ts +3 -0
@@ -0,0 +1,242 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import * as cdk from 'aws-cdk-lib';
4
+ import * as ssm from 'aws-cdk-lib/aws-ssm';
5
+ import * as iam from 'aws-cdk-lib/aws-iam';
6
+ import * as lambda from 'aws-cdk-lib/aws-lambda';
7
+ import * as cr from 'aws-cdk-lib/custom-resources';
8
+ import { Scope, registerConfig, DEFAULT_NODE_RUNTIME } from '@aws-blocks/core/cdk';
9
+ import { AppSettingErrors } from './errors.js';
10
+ export { AppSettingErrors } from './errors.js';
11
+ /**
12
+ * CDK construct for AppSetting. Creates a single SSM parameter (String or
13
+ * SecureString) and grants the shared Lambda handler read/write permissions.
14
+ *
15
+ * - String parameters use `aws-cdk-lib/aws-ssm.StringParameter` directly.
16
+ * - SecureString parameters use a Custom Resource Lambda because
17
+ * CloudFormation cannot natively create SecureString parameters.
18
+ * - SecureString parameters are encrypted with the default `aws/ssm` KMS key.
19
+ */
20
+ export class AppSetting extends Scope {
21
+ /**
22
+ * Reference an SSM parameter that is created and owned **outside this stack**
23
+ * (e.g. a connection string seeded by `ensureSecrets` before deploy). The
24
+ * construct does not create, seed, tag, or delete it — it only grants the app
25
+ * **read-only** access (`ssm:GetParameter`, plus `kms:Decrypt` when `secret`)
26
+ * and registers the name for config resolution.
27
+ *
28
+ * The parameter MUST already exist at deploy time, otherwise the app fails at
29
+ * runtime with `ParameterNotFound`.
30
+ *
31
+ * @example
32
+ * const dbUrl = AppSetting.fromExisting(scope, 'db-url', { name: dbParameterName, secret: true });
33
+ */
34
+ static fromExisting(scope, id, options) {
35
+ const opts = { ...options, external: true };
36
+ return new AppSetting(scope, id, opts);
37
+ }
38
+ constructor(scope, id, options) {
39
+ super(id, { parent: scope });
40
+ // `external` is package-internal (set only by fromExisting), not on the
41
+ // public AppSettingOptions — read it via the internal options type.
42
+ const external = options.external ?? false;
43
+ // ── Validation ──────────────────────────────────────────────────────
44
+ if (options.secret && options.schema) {
45
+ const err = new Error(`AppSetting '${id}': 'secret' and 'schema' cannot be used together. ` +
46
+ `Secrets are always plain strings. Remove the schema or the secret flag.`);
47
+ err.name = AppSettingErrors.ValidationFailed;
48
+ throw err;
49
+ }
50
+ if (options.schema && options.value === undefined) {
51
+ const err = new Error(`AppSetting '${id}': a schema is provided but no value. ` +
52
+ `Provide a value that conforms to the schema so the SSM parameter is valid on first deploy.`);
53
+ err.name = AppSettingErrors.ValidationFailed;
54
+ throw err;
55
+ }
56
+ if (options.secret && options.value !== undefined) {
57
+ const err = new Error(`AppSetting '${id}': secrets should not have a value in source code. ` +
58
+ `Remove the value — a random secret will be generated on first deploy. ` +
59
+ `Set the real value at runtime via AppSetting.put().`);
60
+ err.name = AppSettingErrors.ValidationFailed;
61
+ throw err;
62
+ }
63
+ if (external && options.value !== undefined) {
64
+ const err = new Error(`AppSetting '${id}': 'external' settings are owned elsewhere and must not have a value. ` +
65
+ `Remove the value — the parameter is created and seeded outside this stack.`);
66
+ err.name = AppSettingErrors.ValidationFailed;
67
+ throw err;
68
+ }
69
+ if (external && !options.name) {
70
+ const err = new Error(`AppSetting '${id}': 'external' requires an explicit 'name' referencing the existing parameter.`);
71
+ err.name = AppSettingErrors.ValidationFailed;
72
+ throw err;
73
+ }
74
+ if (!options.secret && !external && options.value === undefined) {
75
+ const err = new Error(`AppSetting '${id}': non-secret settings require a value. ` +
76
+ `Provide an initial value for the SSM parameter.`);
77
+ err.name = AppSettingErrors.ValidationFailed;
78
+ throw err;
79
+ }
80
+ const parameterName = options.name ?? `/${this.fullId}`;
81
+ // Always JSON.stringify
82
+ // For secrets without a value, the Custom Resource Lambda generates a random string
83
+ const initialValue = options.value !== undefined
84
+ ? JSON.stringify(options.value)
85
+ : undefined;
86
+ const parameterArn = cdk.Stack.of(this).formatArn({
87
+ service: 'ssm',
88
+ resource: 'parameter',
89
+ resourceName: parameterName.replace(/^\//, ''),
90
+ });
91
+ if (options.secret) {
92
+ // ── SecureString ────────────────────────────────────────────────
93
+ // Externally-owned secrets (e.g. the connection string seeded by
94
+ // ensureSecrets) are NOT enrolled in the bulk-init: it would
95
+ // PutParameter a random placeholder over a parameter we don't own and
96
+ // then fail tagging it (AddTagsToResource needs ssm:GetParameters).
97
+ // We only need runtime read access, granted below.
98
+ if (!external) {
99
+ registerSecret(cdk.Stack.of(this), parameterName);
100
+ }
101
+ // Grant handler KMS access for the default aws/ssm key. External secrets
102
+ // are read-only (Decrypt only); stack-managed secrets also need Encrypt
103
+ // so the app can write the value via put().
104
+ this.handler.addToRolePolicy(new iam.PolicyStatement({
105
+ actions: external ? ['kms:Decrypt'] : ['kms:Decrypt', 'kms:Encrypt'],
106
+ resources: ['*'],
107
+ conditions: {
108
+ StringEquals: {
109
+ 'kms:ViaService': `ssm.${cdk.Stack.of(this).region}.amazonaws.com`,
110
+ },
111
+ },
112
+ }));
113
+ }
114
+ else if (!external) {
115
+ // ── String parameter via CDK construct ──────────────────────────
116
+ const param = new ssm.StringParameter(this, 'Param', {
117
+ parameterName,
118
+ stringValue: initialValue ?? '',
119
+ });
120
+ let tagStack = cdk.Stack.of(this);
121
+ while (tagStack.nestedStackParent)
122
+ tagStack = tagStack.nestedStackParent;
123
+ cdk.Tags.of(param).add('aws-blocks-stack', tagStack.stackName);
124
+ }
125
+ // (external non-secret: parameter exists already; nothing to create.)
126
+ // Grant handler SSM access on this parameter. External parameters are owned
127
+ // elsewhere, so the app only reads them (no ssm:PutParameter).
128
+ this.handler.addToRolePolicy(new iam.PolicyStatement({
129
+ actions: external ? ['ssm:GetParameter'] : ['ssm:GetParameter', 'ssm:PutParameter'],
130
+ resources: [parameterArn],
131
+ }));
132
+ // Pass the parameter name to the runtime via config registry
133
+ const envKey = `BLOCKS_SSM_PARAM_${id.toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;
134
+ registerConfig(this, envKey, parameterName);
135
+ }
136
+ }
137
+ // ── Bulk Secret Initialization (one CustomResource per stack) ───────────────
138
+ const SECRET_BULK_KEY = Symbol.for('BLOCKS_SECRET_BULK_INIT');
139
+ /**
140
+ * Register a secret parameter name. On first call, creates the shared Lambda,
141
+ * Provider, and a single CustomResource. All subsequent calls just append to
142
+ * the parameter list (resolved lazily at synth time).
143
+ */
144
+ function registerSecret(stack, parameterName) {
145
+ let state = stack[SECRET_BULK_KEY];
146
+ if (state) {
147
+ state.parameterNames.push(parameterName);
148
+ return;
149
+ }
150
+ // First secret in this stack — create all shared infrastructure
151
+ state = { parameterNames: [parameterName] };
152
+ stack[SECRET_BULK_KEY] = state;
153
+ const secretInitFn = new lambda.Function(stack, 'KitSecretInitFn', {
154
+ runtime: DEFAULT_NODE_RUNTIME,
155
+ handler: 'index.handler',
156
+ code: lambda.Code.fromInline(`
157
+ const { SSMClient, PutParameterCommand, DeleteParameterCommand, AddTagsToResourceCommand } = require('@aws-sdk/client-ssm');
158
+ const crypto = require('crypto');
159
+ const client = new SSMClient({});
160
+ exports.handler = async (event) => {
161
+ const names = event.ResourceProperties.ParameterNames || [];
162
+ const stackName = event.ResourceProperties.StackName || '';
163
+ const tags = stackName ? [{ Key: 'aws-blocks-stack', Value: stackName }] : [];
164
+ const oldNames = (event.OldResourceProperties || {}).ParameterNames || [];
165
+ if (event.RequestType === 'Delete') {
166
+ for (const name of names) {
167
+ try { await client.send(new DeleteParameterCommand({ Name: name })); } catch {}
168
+ }
169
+ return { PhysicalResourceId: 'bb-secrets-bulk' };
170
+ }
171
+ if (event.RequestType === 'Create') {
172
+ for (const name of names) {
173
+ const secret = crypto.randomBytes(32).toString('base64url');
174
+ try {
175
+ await client.send(new PutParameterCommand({
176
+ Name: name, Value: secret, Type: 'SecureString', Overwrite: false, Tags: tags,
177
+ }));
178
+ } catch (e) {
179
+ if (e.name !== 'ParameterAlreadyExists') throw e;
180
+ if (tags.length) {
181
+ await client.send(new AddTagsToResourceCommand({ ResourceType: 'Parameter', ResourceId: name, Tags: tags }));
182
+ }
183
+ }
184
+ }
185
+ return { PhysicalResourceId: 'bb-secrets-bulk' };
186
+ }
187
+ if (event.RequestType === 'Update') {
188
+ const added = names.filter(n => !oldNames.includes(n));
189
+ const removed = oldNames.filter(n => !names.includes(n));
190
+ for (const name of added) {
191
+ const secret = crypto.randomBytes(32).toString('base64url');
192
+ try {
193
+ await client.send(new PutParameterCommand({
194
+ Name: name, Value: secret, Type: 'SecureString', Overwrite: false, Tags: tags,
195
+ }));
196
+ } catch (e) {
197
+ if (e.name !== 'ParameterAlreadyExists') throw e;
198
+ if (tags.length) {
199
+ await client.send(new AddTagsToResourceCommand({ ResourceType: 'Parameter', ResourceId: name, Tags: tags }));
200
+ }
201
+ }
202
+ }
203
+ for (const name of removed) {
204
+ try { await client.send(new DeleteParameterCommand({ Name: name })); } catch {}
205
+ }
206
+ return { PhysicalResourceId: 'bb-secrets-bulk' };
207
+ }
208
+ return { PhysicalResourceId: 'bb-secrets-bulk' };
209
+ };
210
+ `),
211
+ });
212
+ secretInitFn.addToRolePolicy(new iam.PolicyStatement({
213
+ actions: ['ssm:PutParameter', 'ssm:DeleteParameter', 'ssm:AddTagsToResource'],
214
+ resources: cdk.Lazy.list({
215
+ produce: () => state.parameterNames.map(name => stack.formatArn({
216
+ service: 'ssm',
217
+ resource: 'parameter',
218
+ resourceName: name.replace(/^\//, ''),
219
+ })),
220
+ }),
221
+ }));
222
+ secretInitFn.addToRolePolicy(new iam.PolicyStatement({
223
+ actions: ['kms:Encrypt'],
224
+ resources: ['*'],
225
+ conditions: {
226
+ StringEquals: {
227
+ 'kms:ViaService': `ssm.${stack.region}.amazonaws.com`,
228
+ },
229
+ },
230
+ }));
231
+ const provider = new cr.Provider(stack, 'KitSecretProvider', {
232
+ onEventHandler: secretInitFn,
233
+ });
234
+ new cdk.CustomResource(stack, 'KitSecretsBulk', {
235
+ serviceToken: provider.serviceToken,
236
+ properties: {
237
+ ParameterNames: cdk.Lazy.list({ produce: () => state.parameterNames }),
238
+ StackName: (() => { let s = stack; while (s.nestedStackParent)
239
+ s = s.nestedStackParent; return s.stackName; })(),
240
+ },
241
+ });
242
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.cdk.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cdk.test.d.ts","sourceRoot":"","sources":["../src/index.cdk.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,187 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * CDK-side tests for AppSetting.
5
+ *
6
+ * Verifies that the Custom Resource Lambda's IAM policy is scoped to specific
7
+ * parameter ARNs (not a wildcard) — regression test for #598.
8
+ */
9
+ import { test } from 'node:test';
10
+ import assert from 'node:assert';
11
+ import * as cdk from 'aws-cdk-lib';
12
+ import { Template, Match } from 'aws-cdk-lib/assertions';
13
+ import { Scope, DEFAULT_NODE_RUNTIME } from '@aws-blocks/core/cdk';
14
+ import { AppSetting } from './index.cdk.js';
15
+ class StubBlocksStack extends cdk.Stack {
16
+ handler;
17
+ id;
18
+ constructor(scope, id) {
19
+ super(scope, id);
20
+ this.id = id;
21
+ globalThis.CURRENT_BLOCKS_STACK = this;
22
+ this.handler = new cdk.aws_lambda.Function(this, 'StubHandler', {
23
+ runtime: DEFAULT_NODE_RUNTIME,
24
+ handler: 'index.handler',
25
+ code: cdk.aws_lambda.Code.fromInline('exports.handler = async () => {};'),
26
+ });
27
+ }
28
+ }
29
+ function setup() {
30
+ const app = new cdk.App();
31
+ const stack = new StubBlocksStack(app, 'TestStack');
32
+ const parent = new Scope('app');
33
+ return { stack, parent };
34
+ }
35
+ test('CDK: secret AppSetting SSM policy is scoped to specific parameter ARN (not wildcard)', () => {
36
+ const { stack, parent } = setup();
37
+ new AppSetting(parent, 'my-secret', { secret: true, name: '/myapp/secret-key' });
38
+ const template = Template.fromStack(stack);
39
+ // The KitSecretInitFn should have an IAM policy for ssm:PutParameter/DeleteParameter
40
+ // scoped to the specific parameter, NOT a wildcard
41
+ const policies = template.findResources('AWS::IAM::Policy');
42
+ const policyLogicalIds = Object.keys(policies);
43
+ let foundSsmPolicy = false;
44
+ for (const logicalId of policyLogicalIds) {
45
+ const statements = policies[logicalId]?.Properties?.PolicyDocument?.Statement;
46
+ if (!Array.isArray(statements))
47
+ continue;
48
+ for (const stmt of statements) {
49
+ const actions = stmt.Action;
50
+ if (!Array.isArray(actions))
51
+ continue;
52
+ if (!actions.includes('ssm:PutParameter') || !actions.includes('ssm:DeleteParameter'))
53
+ continue;
54
+ foundSsmPolicy = true;
55
+ // Resource must NOT be a wildcard — it should be a specific ARN
56
+ const resources = stmt.Resource;
57
+ if (Array.isArray(resources)) {
58
+ for (const res of resources) {
59
+ const arnStr = typeof res === 'string' ? res : JSON.stringify(res);
60
+ assert.ok(!arnStr.includes('"*"') && arnStr !== '*', `SSM policy resource must not be a wildcard, got: ${arnStr}`);
61
+ }
62
+ }
63
+ else {
64
+ const arnStr = typeof resources === 'string' ? resources : JSON.stringify(resources);
65
+ assert.notStrictEqual(arnStr, '*', 'SSM policy resource must not be a wildcard');
66
+ }
67
+ }
68
+ }
69
+ assert.ok(foundSsmPolicy, 'Expected to find an IAM policy with ssm:PutParameter/DeleteParameter');
70
+ });
71
+ test('CDK: secret AppSetting SSM policy contains the correct parameter name', () => {
72
+ const { stack, parent } = setup();
73
+ new AppSetting(parent, 'db-password', { secret: true, name: '/myapp/db-password' });
74
+ const template = Template.fromStack(stack);
75
+ // Verify the policy resource ARN references the parameter name
76
+ const templateJson = JSON.stringify(template.toJSON());
77
+ assert.ok(templateJson.includes('myapp/db-password'), 'Expected the synthesized template to contain the specific parameter name "myapp/db-password"');
78
+ });
79
+ test('CDK: multiple secret AppSettings produce scoped policy with all parameter ARNs', () => {
80
+ const { stack, parent } = setup();
81
+ new AppSetting(parent, 'secret-a', { secret: true, name: '/app/secret-a' });
82
+ new AppSetting(parent, 'secret-b', { secret: true, name: '/app/secret-b' });
83
+ const template = Template.fromStack(stack);
84
+ const templateJson = JSON.stringify(template.toJSON());
85
+ assert.ok(templateJson.includes('app/secret-a'), 'Expected template to reference parameter "app/secret-a"');
86
+ assert.ok(templateJson.includes('app/secret-b'), 'Expected template to reference parameter "app/secret-b"');
87
+ });
88
+ test('CDK: non-secret AppSetting creates SSM StringParameter', () => {
89
+ const { stack, parent } = setup();
90
+ new AppSetting(parent, 'config', { value: 'hello', name: '/app/config' });
91
+ const template = Template.fromStack(stack);
92
+ template.resourceCountIs('AWS::SSM::Parameter', 1);
93
+ });
94
+ test('CDK: non-secret AppSetting grants handler scoped SSM access', () => {
95
+ const { stack, parent } = setup();
96
+ new AppSetting(parent, 'config', { value: 'hello', name: '/app/config' });
97
+ const template = Template.fromStack(stack);
98
+ // Should have a policy statement for ssm:GetParameter, ssm:PutParameter
99
+ // scoped to the specific parameter ARN
100
+ template.hasResourceProperties('AWS::IAM::Policy', {
101
+ PolicyDocument: {
102
+ Statement: Match.arrayWith([
103
+ Match.objectLike({
104
+ Action: ['ssm:GetParameter', 'ssm:PutParameter'],
105
+ Resource: Match.objectLike({
106
+ 'Fn::Join': Match.anyValue(),
107
+ }),
108
+ }),
109
+ ]),
110
+ },
111
+ });
112
+ });
113
+ test('CDK: external secret is NOT enrolled in bulk-init (no KitSecretsBulk / KitSecretInitFn)', () => {
114
+ const { stack, parent } = setup();
115
+ AppSetting.fromExisting(parent, 'db-url', { name: '/blocks/sandbox/db-abc-connection-string', secret: true });
116
+ const template = Template.fromStack(stack);
117
+ // No secret bulk-init custom resource and no init Lambda should be synthesized:
118
+ // an externally-owned parameter must not be created, tagged, or deleted by us.
119
+ template.resourceCountIs('AWS::CloudFormation::CustomResource', 0);
120
+ const lambdas = template.findResources('AWS::Lambda::Function');
121
+ for (const id of Object.keys(lambdas)) {
122
+ const code = JSON.stringify(lambdas[id]?.Properties?.Code ?? {});
123
+ assert.ok(!code.includes('AddTagsToResourceCommand'), `Lambda ${id} should not be the secret-init function`);
124
+ }
125
+ });
126
+ test('CDK: external secret grants READ-ONLY runtime access (GetParameter + Decrypt, scoped, no write)', () => {
127
+ const { stack, parent } = setup();
128
+ AppSetting.fromExisting(parent, 'db-url', { name: '/blocks/sandbox/db-abc-connection-string', secret: true });
129
+ const template = Template.fromStack(stack);
130
+ // ssm:GetParameter, scoped to the specific parameter ARN (not a wildcard).
131
+ template.hasResourceProperties('AWS::IAM::Policy', {
132
+ PolicyDocument: {
133
+ Statement: Match.arrayWith([
134
+ Match.objectLike({
135
+ Action: 'ssm:GetParameter',
136
+ Resource: Match.objectLike({ 'Fn::Join': Match.anyValue() }),
137
+ }),
138
+ ]),
139
+ },
140
+ });
141
+ // kms:Decrypt for reading the SecureString.
142
+ template.hasResourceProperties('AWS::IAM::Policy', {
143
+ PolicyDocument: { Statement: Match.arrayWith([Match.objectLike({ Action: 'kms:Decrypt' })]) },
144
+ });
145
+ // Must NOT grant write to an externally-owned secret.
146
+ const policiesJson = JSON.stringify(template.findResources('AWS::IAM::Policy'));
147
+ assert.ok(!policiesJson.includes('ssm:PutParameter'), 'external secret must not grant ssm:PutParameter');
148
+ assert.ok(!policiesJson.includes('kms:Encrypt'), 'external secret must not grant kms:Encrypt');
149
+ });
150
+ test('CDK: external non-secret creates no SSM parameter and grants read-only access', () => {
151
+ const { stack, parent } = setup();
152
+ AppSetting.fromExisting(parent, 'shared-config', { name: '/some/external/config' });
153
+ const template = Template.fromStack(stack);
154
+ // The construct does not create the parameter — it's owned externally.
155
+ template.resourceCountIs('AWS::SSM::Parameter', 0);
156
+ template.hasResourceProperties('AWS::IAM::Policy', {
157
+ PolicyDocument: {
158
+ Statement: Match.arrayWith([
159
+ Match.objectLike({
160
+ Action: 'ssm:GetParameter',
161
+ Resource: Match.objectLike({ 'Fn::Join': Match.anyValue() }),
162
+ }),
163
+ ]),
164
+ },
165
+ });
166
+ assert.ok(!JSON.stringify(template.findResources('AWS::IAM::Policy')).includes('ssm:PutParameter'), 'external non-secret must not grant write');
167
+ });
168
+ test('CDK: the internal external guard requires a name and forbids a value', () => {
169
+ // fromExisting() is the public API and makes `name` required at the type level;
170
+ // these assertions cover the runtime guard on the underlying `external` option
171
+ // (defense for JS callers / direct construction).
172
+ const { parent } = setup();
173
+ assert.throws(() => new AppSetting(parent, 'ext-no-name', { secret: true, external: true }), /requires an explicit 'name'/);
174
+ assert.throws(() => new AppSetting(parent, 'ext-with-value', { external: true, name: '/x', value: 'v' }), /must not have a value/);
175
+ });
176
+ test('CDK: fromExisting still registers the runtime config key (BLOCKS_SSM_PARAM_*)', () => {
177
+ // BLOCKS_SSM_PARAM_DB_URL is the ONLY link between db-pull's runtime resolveConnString()
178
+ // and the deployed parameter name. If config registration ever moved inside the
179
+ // non-external branch, every external setting would fail at runtime with
180
+ // ParameterNotFound — and nothing else would catch it. This pins the contract.
181
+ const { stack, parent } = setup();
182
+ AppSetting.fromExisting(parent, 'db-url', { name: '/blocks/sandbox/db-abc-connection-string', secret: true });
183
+ const registry = stack[Symbol.for('BLOCKS_CONFIG_REGISTRY')];
184
+ assert.ok(registry, 'config registry exists on the stack');
185
+ assert.ok(registry.entries.has('BLOCKS_SSM_PARAM_DB_URL'), 'external setting must register BLOCKS_SSM_PARAM_DB_URL so the runtime can resolve the parameter');
186
+ assert.equal(registry.entries.get('BLOCKS_SSM_PARAM_DB_URL'), '/blocks/sandbox/db-abc-connection-string');
187
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.hooks.d.ts","sourceRoot":"","sources":["../src/index.hooks.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC"}
@@ -0,0 +1,3 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ export {};
@@ -0,0 +1,77 @@
1
+ import { Scope } from '@aws-blocks/core';
2
+ import type { ScopeParent } from '@aws-blocks/core';
3
+ import type { AppSettingOptions } from './types.js';
4
+ import type { ChildLogger } from '@aws-blocks/bb-logger';
5
+ export { AppSettingErrors } from './errors.js';
6
+ export type { AppSettingOptions } from './types.js';
7
+ /**
8
+ * A single application configuration value backed by SSM Parameter Store.
9
+ *
10
+ * **When to use:** You need to store and retrieve a non-secret configuration
11
+ * value at runtime — a feature flag, API URL, threshold, or structured config
12
+ * object. For sensitive values, set `secret: true` to use SSM SecureString.
13
+ *
14
+ * **When NOT to use:** If you need structured key-value data with conditional
15
+ * writes and queries, use `KVStore` or `DistributedTable`.
16
+ *
17
+ * **Best practices:**
18
+ * - One AppSetting per logical configuration value
19
+ * - Use a schema for structured objects to get type safety and runtime validation
20
+ * - Use `secret: true` for API keys, tokens, and passwords
21
+ *
22
+ * **Scaling:** Standard-tier SSM parameters. 40 TPS default for GetParameter
23
+ * (can be increased). No cost for standard parameters.
24
+ */
25
+ export declare class AppSetting<T = string> extends Scope {
26
+ /**
27
+ * Reference an SSM parameter created and owned outside this stack — the local
28
+ * dev mirror of the CDK `fromExisting`. In the mock there is no IAM or
29
+ * bulk-init, so it behaves like a normal setting keyed by its `fullId`; the
30
+ * factory exists so app code uses the same API in dev and deploy.
31
+ *
32
+ * Note: like any mock secret with no value, `get()` returns a random placeholder
33
+ * unless `.bb-data/settings.json` was already seeded for this `fullId` (e.g. by
34
+ * `db pull`). Local dev still depends on whatever seeds that value.
35
+ */
36
+ static fromExisting<T = string>(scope: ScopeParent, id: string, options: {
37
+ name: string;
38
+ secret?: boolean;
39
+ }): AppSetting<T>;
40
+ private parameterName;
41
+ private initialValue;
42
+ private schema?;
43
+ private isSecret;
44
+ /** @internal Logger for internal operations. Defaults to error-level when not provided. */
45
+ protected log: ChildLogger;
46
+ constructor(scope: ScopeParent, id: string, options: AppSettingOptions<T>);
47
+ /**
48
+ * Retrieve the current value.
49
+ *
50
+ * Returns the stored value from `.bb-data/settings.json`.
51
+ *
52
+ * @returns The current value.
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * const retries = await maxRetries.get();
57
+ * ```
58
+ */
59
+ get(): Promise<T>;
60
+ /**
61
+ * Update the value at runtime.
62
+ *
63
+ * Writes the new value to `.bb-data/settings.json`. When a schema is
64
+ * configured, the value is validated before writing.
65
+ *
66
+ * @param value - The new value to store.
67
+ * @throws {AppSettingErrors.ValidationFailed} If schema validation fails or value exceeds 4 KB.
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * await maxRetries.put('5');
72
+ * await config.put({ maxRetries: 5, timeout: 10000 });
73
+ * ```
74
+ */
75
+ put(value: T): Promise<void>;
76
+ }
77
+ //# sourceMappingURL=index.mock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mock.d.ts","sourceRoot":"","sources":["../src/index.mock.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAA0B,MAAM,kBAAkB,CAAC;AAEjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAMpD,OAAO,KAAK,EAAE,iBAAiB,EAA6B,MAAM,YAAY,CAAC;AAE/E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAIzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAsCpD;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,UAAU,CAAC,CAAC,GAAG,MAAM,CAAE,SAAQ,KAAK;IAChD;;;;;;;;;OASG;IACH,MAAM,CAAC,YAAY,CAAC,CAAC,GAAG,MAAM,EAC7B,KAAK,EAAE,WAAW,EAClB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GACzC,UAAU,CAAC,CAAC,CAAC;IAKhB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,YAAY,CAAI;IACxB,OAAO,CAAC,MAAM,CAAC,CAAsB;IACrC,OAAO,CAAC,QAAQ,CAAU;IAE1B,2FAA2F;IAC3F,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC;gBAEf,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAyBzE;;;;;;;;;;;OAWG;IACG,GAAG,IAAI,OAAO,CAAC,CAAC,CAAC;IAWvB;;;;;;;;;;;;;;OAcG;IACG,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAelC"}