@geekmidas/cloud 10.0.0-alpha.103 → 10.0.0-alpha.105

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cloud",
3
- "version": "10.0.0-alpha.103",
3
+ "version": "10.0.0-alpha.105",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -41,8 +41,8 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "lodash.snakecase": "~4.1.1",
44
- "@geekmidas/envkit": "10.0.0-alpha.103",
45
- "@geekmidas/manifest": "10.0.0-alpha.103"
44
+ "@geekmidas/envkit": "10.0.0-alpha.105",
45
+ "@geekmidas/manifest": "10.0.0-alpha.105"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/lodash.get": "~4.4.9",
@@ -58,10 +58,10 @@
58
58
  "@pulumi/aws": ">=7.47.0",
59
59
  "@pulumi/random": ">=4.21.2",
60
60
  "@pulumi/pulumi": ">=3.263.0",
61
- "@geekmidas/manifest": "^10.0.0-alpha.103",
62
- "@geekmidas/storage": "^10.0.0-alpha.103",
63
- "@geekmidas/events": "^10.0.0-alpha.103",
64
- "@geekmidas/db": "^10.0.0-alpha.103"
61
+ "@geekmidas/manifest": "^10.0.0-alpha.105",
62
+ "@geekmidas/storage": "^10.0.0-alpha.105",
63
+ "@geekmidas/events": "^10.0.0-alpha.105",
64
+ "@geekmidas/db": "^10.0.0-alpha.105"
65
65
  },
66
66
  "peerDependenciesMeta": {
67
67
  "@geekmidas/manifest": {
@@ -355,6 +355,31 @@ describe('fromManifest', () => {
355
355
  });
356
356
  });
357
357
 
358
+ it('generates a secret rather than waiting for one to be set', () => {
359
+ vi.spyOn(console, 'log').mockImplementation(() => {});
360
+ const provisioned = fromManifest(stack, manifest(), overrides, {
361
+ cache: 'db',
362
+ email: 'smtp',
363
+ });
364
+
365
+ // Without a placeholder, the stub stands in for `SecretMissingError`.
366
+ expect(provisioned.AuthSecret!.provides().value).not.toBe(
367
+ 'stub-secret-AuthSecret',
368
+ );
369
+ });
370
+
371
+ it('keeps a secret’s placeholder over the generated one', () => {
372
+ vi.spyOn(console, 'log').mockImplementation(() => {});
373
+ const provisioned = fromManifest(
374
+ stack,
375
+ manifest(),
376
+ { ...overrides, AuthSecret: { placeholder: 'chosen' } },
377
+ { cache: 'db', email: 'smtp' },
378
+ );
379
+
380
+ expect(provisioned.AuthSecret!.provides().value).toBe('chosen');
381
+ });
382
+
358
383
  it('takes an existing cache and mailer rather than creating them', async () => {
359
384
  vi.spyOn(console, 'log').mockImplementation(() => {});
360
385
  const provisioned = fromManifest(
@@ -0,0 +1,79 @@
1
+ import type { ConstructManifest } from '@geekmidas/manifest';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { fromManifest } from '../fromManifest';
4
+
5
+ /** What each `RandomPassword` was asked for, by name. */
6
+ const drawn = vi.hoisted(() => new Map<string, unknown[]>());
7
+
8
+ vi.mock('@pulumi/random', () => ({
9
+ RandomPassword: class {
10
+ readonly result: string;
11
+ constructor(name: string, ...rest: unknown[]) {
12
+ drawn.set(name, rest);
13
+ this.result = `generated-${name}`;
14
+ }
15
+ },
16
+ }));
17
+
18
+ const provision = (manifest: ConstructManifest) => {
19
+ vi.spyOn(console, 'log').mockImplementation(() => {});
20
+ return fromManifest({} as never, manifest, {}, {});
21
+ };
22
+
23
+ describe('a secret on AWS', () => {
24
+ it('is generated in the format it declares, kept when the format changes', () => {
25
+ const provisioned = provision({
26
+ RouterPassword: {
27
+ kind: 'secret',
28
+ id: 'RouterPassword',
29
+ provides: ['ROUTER_PASSWORD'],
30
+ length: 24,
31
+ symbols: '!@#',
32
+ },
33
+ } as unknown as ConstructManifest);
34
+
35
+ expect(provisioned.RouterPassword!.provides().value).toBe(
36
+ 'generated-RouterPasswordGenerated',
37
+ );
38
+ expect(drawn.get('RouterPasswordGenerated')).toEqual([
39
+ { length: 24, special: true, overrideSpecial: '!@#', minSpecial: 1 },
40
+ {
41
+ ignoreChanges: ['length', 'special', 'overrideSpecial', 'minSpecial'],
42
+ },
43
+ ]);
44
+ });
45
+
46
+ it('is 43 letters, digits, - and _ by default', () => {
47
+ provision({
48
+ AuthSecret: {
49
+ kind: 'secret',
50
+ id: 'AuthSecret',
51
+ provides: ['AUTH_SECRET'],
52
+ },
53
+ } as unknown as ConstructManifest);
54
+
55
+ expect(drawn.get('AuthSecretGenerated')?.[0]).toEqual({
56
+ length: 43,
57
+ special: true,
58
+ overrideSpecial: '-_',
59
+ minSpecial: 1,
60
+ });
61
+ });
62
+
63
+ it('draws no symbols when told none', () => {
64
+ provision({
65
+ Token: {
66
+ kind: 'secret',
67
+ id: 'Token',
68
+ provides: ['TOKEN'],
69
+ length: 6,
70
+ symbols: '',
71
+ },
72
+ } as unknown as ConstructManifest);
73
+
74
+ expect(drawn.get('TokenGenerated')?.[0]).toEqual({
75
+ length: 6,
76
+ special: false,
77
+ });
78
+ });
79
+ });
@@ -15,8 +15,10 @@ import type { StackType } from '../Stack';
15
15
  * signing key, the thing that stores it, and the thing that reads it are three
16
16
  * different systems deployed, and one derived string locally.
17
17
  *
18
- * Usable on its own. `Stack.fromManifest` translates a manifest into these, but
19
- * the component takes an ordinary placeholder so it works in a hand-written
18
+ * Usable on its own. `Stack.fromManifest` translates a manifest into these, and
19
+ * hands each a generated placeholder, so a manifest's secret is generated on
20
+ * its first deploy rather than set by hand; the component on its own takes
21
+ * whatever placeholder it is given, so it works in a hand-written
20
22
  * `sst.config.ts` with no manifest anywhere.
21
23
  */
22
24
  export class Secret<
@@ -72,5 +74,5 @@ export interface SecretProps {
72
74
  * letting the deploy fail — is the correct handling for a real secret. It
73
75
  * exists for the values that are secrets by habit rather than by need.
74
76
  */
75
- placeholder?: string;
77
+ placeholder?: $util.Input<string>;
76
78
  }
@@ -19,6 +19,8 @@ import type {
19
19
  Declaration,
20
20
  DeclarationKind,
21
21
  Dependency,
22
+ SecretDeclaration,
23
+ SecretFormat,
22
24
  SiteDeclaration,
23
25
  } from '@geekmidas/manifest';
24
26
  import {
@@ -31,8 +33,11 @@ import {
31
33
  PUBLIC_PREFIX,
32
34
  providedKeyFor,
33
35
  provisionOrder,
36
+ SECRET_LENGTH,
37
+ SECRET_SYMBOLS,
34
38
  schemeBase,
35
39
  } from '@geekmidas/manifest';
40
+ import { RandomPassword } from '@pulumi/random';
36
41
  import {
37
42
  Cache,
38
43
  CacheIsAmbiguous,
@@ -53,7 +58,7 @@ import { Function } from './aws/Function';
53
58
  import { ObjectStorage } from './aws/ObjectStorage';
54
59
  import { Queue } from './aws/Queue';
55
60
  import { RestApiSurface } from './aws/RestApiSurface';
56
- import { Secret } from './aws/Secret';
61
+ import { Secret, type SecretProps } from './aws/Secret';
57
62
  import { StaticSite } from './aws/StaticSite';
58
63
  import { Topic } from './aws/Topic';
59
64
  import {
@@ -380,7 +385,14 @@ const PROVISIONERS: Partial<Record<DeclarationKind, Provisioner>> = {
380
385
  callers: context.callers?.get(d.id)?.promise,
381
386
  }),
382
387
 
383
- secret: (stack, d, props) => new Secret(stack, d.id, props),
388
+ // Generated on the first deploy and kept in the stage's state, as a server
389
+ // stage keeps it in its secrets: every later deploy reads the same value
390
+ // back. One set with `sst secret set` wins over it.
391
+ secret: (stack, d, props) =>
392
+ new Secret(stack, d.id, {
393
+ placeholder: generatedSecret(d.id, d as SecretDeclaration),
394
+ ...(props as SecretProps),
395
+ }),
384
396
 
385
397
  // The same storage as a secret, under a different role — see the component.
386
398
  credential: (stack, d, props) => new Credential(stack, d.id, props),
@@ -1061,6 +1073,30 @@ export function fromManifest<
1061
1073
  return provisioned;
1062
1074
  }
1063
1075
 
1076
+ /**
1077
+ * A secret in its declared format — the alphabet `drawSecret` draws from on
1078
+ * every other stage, with one symbol guaranteed as it guarantees one.
1079
+ *
1080
+ * The format is fixed at the first deploy, as a server stage's is: changing it
1081
+ * would replace the password, and every later deploy is meant to read back the
1082
+ * one it had.
1083
+ */
1084
+ function generatedSecret(id: string, format: SecretFormat) {
1085
+ const symbols = [...new Set(format.symbols ?? SECRET_SYMBOLS)].join('');
1086
+
1087
+ return new RandomPassword(
1088
+ `${id}Generated`,
1089
+ {
1090
+ length: format.length ?? SECRET_LENGTH,
1091
+ special: symbols.length > 0,
1092
+ ...(symbols ? { overrideSpecial: symbols, minSpecial: 1 } : {}),
1093
+ },
1094
+ {
1095
+ ignoreChanges: ['length', 'special', 'overrideSpecial', 'minSpecial'],
1096
+ },
1097
+ ).result;
1098
+ }
1099
+
1064
1100
  /** What one function receives from its edges. */
1065
1101
  export interface ResolvedEdges {
1066
1102
  /** The components it may reach. SST turns these into IAM *and* injects their