@geekmidas/cloud 10.0.0-alpha.12 → 10.0.0-alpha.121

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 (50) hide show
  1. package/README.md +14 -6
  2. package/dist/index.cjs +3 -3
  3. package/dist/index.d.cts +1 -1
  4. package/dist/index.d.mts +1 -1
  5. package/dist/index.mjs +2 -3
  6. package/dist/utils/index.cjs +106 -5
  7. package/dist/utils/index.cjs.map +1 -0
  8. package/dist/utils/index.d.cts +62 -2
  9. package/dist/utils/index.d.cts.map +1 -0
  10. package/dist/utils/index.d.mts +62 -2
  11. package/dist/utils/index.d.mts.map +1 -0
  12. package/dist/utils/index.mjs +78 -2
  13. package/dist/utils/index.mjs.map +1 -0
  14. package/package.json +9 -8
  15. package/src/dokploy/Application.ts +52 -5
  16. package/src/dokploy/__tests__/Application.provider.spec.ts +206 -0
  17. package/src/sst/__tests__/api.spec.ts +185 -0
  18. package/src/sst/__tests__/bootstrap.spec.ts +15 -0
  19. package/src/sst/__tests__/encryption.spec.ts +90 -0
  20. package/src/sst/__tests__/fromManifest.integration.spec.ts +452 -0
  21. package/src/sst/__tests__/fromManifest.secret.spec.ts +79 -0
  22. package/src/sst/__tests__/fromManifest.spec.ts +306 -5
  23. package/src/sst/__tests__/function.spec.ts +121 -0
  24. package/src/sst/__tests__/links.spec.ts +209 -0
  25. package/src/sst/__tests__/surfaces.spec.ts +17 -0
  26. package/src/sst/__type-tests__/overrides.type-test.ts +152 -0
  27. package/src/sst/aws/Api.ts +16 -44
  28. package/src/sst/aws/Credential.ts +42 -9
  29. package/src/sst/aws/Cron.ts +5 -45
  30. package/src/sst/aws/Database.ts +15 -1
  31. package/src/sst/aws/DatabaseBootstrap.ts +6 -0
  32. package/src/sst/aws/Encryption.ts +91 -0
  33. package/src/sst/aws/ExternalApi.ts +58 -0
  34. package/src/sst/aws/Function.ts +9 -38
  35. package/src/sst/aws/Queue.ts +34 -1
  36. package/src/sst/aws/RestApiSurface.ts +36 -14
  37. package/src/sst/aws/Secret.ts +5 -3
  38. package/src/sst/aws/bootstrap/handler.ts +12 -1
  39. package/src/sst/fromManifest.ts +300 -44
  40. package/src/sst/index.ts +6 -1
  41. package/src/sst/overrides.ts +133 -0
  42. package/dist/index-B5CZ1xVf.d.cts +0 -64
  43. package/dist/index-B5CZ1xVf.d.cts.map +0 -1
  44. package/dist/index-DhHRjduZ.d.mts +0 -64
  45. package/dist/index-DhHRjduZ.d.mts.map +0 -1
  46. package/dist/utils-B1a2UuEO.cjs +0 -129
  47. package/dist/utils-B1a2UuEO.cjs.map +0 -1
  48. package/dist/utils-DMOXJ27j.mjs +0 -83
  49. package/dist/utils-DMOXJ27j.mjs.map +0 -1
  50. package/src/sst/__type-tests__/manifest.type-test.ts +0 -77
@@ -11,7 +11,8 @@ import type { StackType } from '../Stack';
11
11
  * work queue. Link it to a producer and the runtime resolves `<NAME>_URL`,
12
12
  * `<NAME>_ARN`, and a `<NAME>_PUBLISHER_CONNECTION_STRING` (`sqs://?queueUrl=…`)
13
13
  * that `@geekmidas/events`'s `Publisher.fromConnectionString` consumes. Its
14
- * single consumer is wired by `QueueSubscriber`.
14
+ * one consumer — the handler the queue was declared with — is subscribed by
15
+ * {@link Queue.consume}.
15
16
  *
16
17
  * SST's native `Queue` link exposes only `url`, so `getSSTLink` is overridden to
17
18
  * also expose `arn` (what the resolver needs). `QueueProps` extends
@@ -73,6 +74,26 @@ export class Queue<
73
74
  };
74
75
  }
75
76
 
77
+ /**
78
+ * Subscribe the queue's one consumer: a Lambda running `handler`, linked to
79
+ * what the consumer depends on and to this queue itself.
80
+ *
81
+ * A queue has exactly one consumer, so this is called once per queue — by
82
+ * `fromManifest`, from the handler the build wrote for it.
83
+ */
84
+ consume(consumer: QueueConsumer) {
85
+ return this.subscribe(
86
+ {
87
+ handler: consumer.handler,
88
+ link: [this, ...(consumer.link ?? [])],
89
+ runtime: 'nodejs24.x',
90
+ ...(consumer.vpc ? { vpc: consumer.vpc } : {}),
91
+ ...(consumer.timeout ? { timeout: consumer.timeout } : {}),
92
+ },
93
+ consumer.batchSize ? { batch: { size: consumer.batchSize } } : {},
94
+ );
95
+ }
96
+
76
97
  override getSSTLink() {
77
98
  const link = super.getSSTLink();
78
99
  return {
@@ -82,6 +103,18 @@ export class Queue<
82
103
  }
83
104
  }
84
105
 
106
+ /** The queue's one consumer, as {@link Queue.consume} subscribes it. */
107
+ export interface QueueConsumer {
108
+ /** The built handler — `apps/api/.gkm/aws/queues/emails.handler`. */
109
+ handler: string;
110
+ /** What the consumer depends on; the queue itself is always linked. */
111
+ link?: unknown[];
112
+ /** The database's network, when the consumer reaches one. */
113
+ vpc?: sst.aws.Vpc;
114
+ timeout?: `${number} seconds` | `${number} minutes`;
115
+ batchSize?: number;
116
+ }
117
+
85
118
  export interface QueueProps extends sst.aws.QueueArgs {
86
119
  /**
87
120
  * The physical queue name, where the auto-generated one will not do.
@@ -4,20 +4,10 @@ import type { StackType } from '../Stack';
4
4
  /**
5
5
  * `RestApiSurface` — an HTTP API, and the infra half of the `rest-api` kind.
6
6
  *
7
- * Deliberately *not* `Api`, which takes a route table and validates each
8
- * route's environment at synth. That component is the endpoint pipeline's, and
9
- * it needs routes; this one is the manifest's, and the manifest's `rest-api`
10
- * node currently carries `endpoints: []` for an application's own API.
11
- *
12
- * So what this provisions is the surface and nothing on it. An API Gateway with
13
- * no routes 404s everything, which is the honest state of things — and it is
14
- * still worth provisioning, because the *address* is what everything downstream
15
- * needs: a site inlines it as `VITE_API_URL`, an auth server puts it on its
16
- * trusted-origin list, and the cookie domain derives from it. Those are blocked
17
- * on the API existing, not on it answering.
18
- *
19
- * The two collapse into one component when the endpoint merge lands and the
20
- * surface node carries its routes.
7
+ * Provisioned before its routes, because its *address* is what everything
8
+ * downstream needs: a site inlines it as `VITE_API_URL`, an auth server puts it
9
+ * on its trusted-origin list, and the cookie domain derives from it. The routes
10
+ * are mounted once everything they link to exists — see {@link mount}.
21
11
  */
22
12
 
23
13
  export interface RestApiSurfaceProps extends sst.aws.ApiGatewayV2Args {
@@ -82,6 +72,25 @@ export class RestApiSurface<
82
72
  };
83
73
  }
84
74
 
75
+ /**
76
+ * Mount one endpoint: a Lambda running the handler the build wrote for it,
77
+ * linked to exactly what that endpoint depends on.
78
+ */
79
+ mount(endpoint: SurfaceRoute) {
80
+ return this.route(
81
+ `${endpoint.method} ${endpoint.path}`,
82
+ {
83
+ handler: endpoint.handler,
84
+ link: endpoint.link ?? [],
85
+ runtime: 'nodejs24.x',
86
+ ...(endpoint.vpc ? { vpc: endpoint.vpc } : {}),
87
+ },
88
+ // `iam` is the one authorizer the gateway enforces itself; every other
89
+ // is the endpoint's own, checked in the handler.
90
+ endpoint.authorizer === 'iam' ? { auth: { iam: true } } : {},
91
+ );
92
+ }
93
+
85
94
  override getSSTLink() {
86
95
  const link = super.getSSTLink();
87
96
  return {
@@ -90,3 +99,16 @@ export class RestApiSurface<
90
99
  };
91
100
  }
92
101
  }
102
+
103
+ /** One endpoint, as {@link RestApiSurface.mount} mounts it. */
104
+ export interface SurfaceRoute {
105
+ method: string;
106
+ path: string;
107
+ /** The built handler — `apps/api/.gkm/aws/routes/getUser.handler`. */
108
+ handler: string;
109
+ /** What the endpoint depends on, and nothing else. */
110
+ link?: unknown[];
111
+ /** The database's network, when the endpoint reaches one. */
112
+ vpc?: sst.aws.Vpc;
113
+ authorizer?: string;
114
+ }
@@ -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
  }
@@ -28,6 +28,13 @@ export interface BootstrapTenant {
28
28
  runtime: string;
29
29
  owner: string;
30
30
  reader?: string;
31
+ /**
32
+ * The database's own roles rather than a tenant's: its owner may create in
33
+ * the database, for trusted extensions.
34
+ */
35
+ ownsDatabase?: boolean;
36
+ /** Extensions the database declared, created as the master. */
37
+ extensions?: readonly string[];
31
38
  passwords: { runtime: string; owner: string; reader?: string };
32
39
  }
33
40
 
@@ -75,7 +82,11 @@ export async function handler(event: BootstrapEvent): Promise<BootstrapResult> {
75
82
 
76
83
  try {
77
84
  for (const tenant of event.tenants) {
78
- for (const statement of roleStatements(tenant)) {
85
+ const spec = {
86
+ ...tenant,
87
+ ...(tenant.ownsDatabase ? { database: event.master.database } : {}),
88
+ };
89
+ for (const statement of roleStatements(spec)) {
79
90
  if (statement.exists) {
80
91
  const { rows } = await client.query(
81
92
  statement.exists.sql,
@@ -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 {
@@ -26,11 +28,16 @@ import {
26
28
  cookieDomain,
27
29
  DEFAULT_POSTGRES_VERSION,
28
30
  dependentsOf,
31
+ mobileOrigins,
29
32
  PUBLIC,
30
33
  PUBLIC_PREFIX,
31
34
  providedKeyFor,
32
35
  provisionOrder,
36
+ SECRET_LENGTH,
37
+ SECRET_SYMBOLS,
38
+ schemeBase,
33
39
  } from '@geekmidas/manifest';
40
+ import { RandomPassword } from '@pulumi/random';
34
41
  import {
35
42
  Cache,
36
43
  CacheIsAmbiguous,
@@ -39,15 +46,19 @@ import {
39
46
  withCacheTable,
40
47
  } from './aws/Cache';
41
48
  import { Credential } from './aws/Credential';
42
- import { Database, DatabaseNeedsVpc } from './aws/Database';
49
+ import { Cron, type CronSchedule } from './aws/Cron';
50
+ import { Database, DatabaseNeedsVpc, type DatabaseProps } from './aws/Database';
43
51
  import { DatabaseBootstrap } from './aws/DatabaseBootstrap';
44
52
  import { DatabaseReader, DatabaseSchema } from './aws/DerivedDatabase';
45
53
  import { Email, EmailNeedsSender } from './aws/Email';
54
+ import { Encryption, type EncryptionProps } from './aws/Encryption';
55
+ import { ExternalApi, type ExternalApiProps } from './aws/ExternalApi';
46
56
  import { FileServer } from './aws/FileServer';
57
+ import { Function } from './aws/Function';
47
58
  import { ObjectStorage } from './aws/ObjectStorage';
48
59
  import { Queue } from './aws/Queue';
49
60
  import { RestApiSurface } from './aws/RestApiSurface';
50
- import { Secret } from './aws/Secret';
61
+ import { Secret, type SecretProps } from './aws/Secret';
51
62
  import { StaticSite } from './aws/StaticSite';
52
63
  import { Topic } from './aws/Topic';
53
64
  import {
@@ -56,6 +67,7 @@ import {
56
67
  UnresolvedDependency,
57
68
  } from './errors';
58
69
  import type { GkmLinkable } from './Linkable';
70
+ import type { ManifestBackends, ManifestOverrides } from './overrides';
59
71
  import type { StackType } from './Stack';
60
72
 
61
73
  /**
@@ -133,21 +145,6 @@ type Provisioner = (
133
145
  context: ProvisionContext,
134
146
  ) => Provisioned;
135
147
 
136
- /**
137
- * Provider-specific props, per construct id.
138
- *
139
- * Neutral options — `versioned`, and later `cdn` — travel in the declaration,
140
- * because the app legitimately has an opinion about them. Anything with S3 in
141
- * its name does not belong in application code, so lifecycle rules, CORS, and
142
- * canned ACLs are supplied here, in the deploy layer, keyed by the id they
143
- * apply to and typed against the component that receives them.
144
- *
145
- * A third escape hatch needs no API at all: `fromManifest` returns the
146
- * components, so `provisioned.Uploads.nodes.bucket` is reachable for anything
147
- * neither route covers.
148
- */
149
- export type ComponentOverrides = Record<string, Record<string, unknown>>;
150
-
151
148
  /**
152
149
  * Which component provisions which kind.
153
150
  *
@@ -220,7 +217,11 @@ const PROVISIONERS: Partial<Record<DeclarationKind, Provisioner>> = {
220
217
  // rather than whatever Aurora defaults to that month. Local ran 18
221
218
  // while this ran 17.7, and neither was written down anywhere.
222
219
  version: String(d.version ?? DEFAULT_POSTGRES_VERSION),
223
- ...(props as unknown as sst.aws.PostgresArgs),
220
+ // `DatabaseProps`, not `sst.aws.PostgresArgs`: the database needs the
221
+ // `Vpc` component itself (its bootstrap function runs in the same VPC
222
+ // and needs the security groups too), where RDS would accept bare
223
+ // subnet ids. TS 7 checks the spread; 6 let the wider type through.
224
+ ...(props as unknown as Omit<DatabaseProps, 'schema'>),
224
225
  });
225
226
 
226
227
  // A database needs its own roles, not just its tenants'. Registering only
@@ -232,13 +233,24 @@ const PROVISIONERS: Partial<Record<DeclarationKind, Provisioner>> = {
232
233
  const bootstrap = bootstrapFor(cluster, d.id, context);
233
234
  const runtime = roleNameFor(d, context);
234
235
 
235
- bootstrap?.add({
236
+ const credentials = bootstrap?.add({
236
237
  id: d.id,
237
238
  schema: d.schema,
238
239
  runtime,
239
240
  owner: ownerRole(runtime),
241
+ ownsDatabase: true,
242
+ // RDS ships PostGIS, so the image is not a question here — only the
243
+ // `CREATE EXTENSION`, which the master can run and the owner cannot.
244
+ ...(d.extensions?.length ? { extensions: d.extensions } : {}),
240
245
  ...(hasReader(d.id, context) ? { reader: readerRole(runtime) } : {}),
241
246
  });
247
+
248
+ // Registering the role was half of it: its handlers have to connect as
249
+ // it too, or the DDL creates a role nothing uses and every handler
250
+ // linked to the database still holds the master.
251
+ if (credentials) {
252
+ cluster.connectAs({ user: runtime, password: credentials.runtime });
253
+ }
242
254
  }
243
255
 
244
256
  return cluster;
@@ -363,17 +375,12 @@ const PROVISIONERS: Partial<Record<DeclarationKind, Provisioner>> = {
363
375
  },
364
376
 
365
377
  /**
366
- * The surface, without its routes.
367
- *
368
- * An API Gateway with no routes 404s everything, and that is the honest
369
- * state: the *surface* is what this kind declares, and mounting handlers on
370
- * it is the endpoint merge that has not landed — routes still reach the
371
- * deploy target through the separate `RouteInfo[]` pipeline.
378
+ * The surface, without its routes yet.
372
379
  *
373
- * Provisioning it anyway is not ceremony. Its address is what a site inlines
374
- * as `VITE_API_URL`, what an auth server puts on its trusted-origin list, and
375
- * what the cookie domain derives from — so everything downstream of the API
376
- * is blocked on the API *existing*, not on it answering.
380
+ * Its address is what a site inlines as `VITE_API_URL`, what an auth server
381
+ * puts on its trusted-origin list, and what the cookie domain derives from —
382
+ * so it is provisioned in order, and its endpoints are mounted by
383
+ * {@link mountRoutes} once everything they link to exists.
377
384
  */
378
385
  'rest-api': (stack, d, props, context) =>
379
386
  new RestApiSurface(stack, d.id, {
@@ -381,10 +388,41 @@ const PROVISIONERS: Partial<Record<DeclarationKind, Provisioner>> = {
381
388
  callers: context.callers?.get(d.id)?.promise,
382
389
  }),
383
390
 
384
- secret: (stack, d, props) => new Secret(stack, d.id, props),
391
+ // Generated on the first deploy and kept in the stage's state, as a server
392
+ // stage keeps it in its secrets: every later deploy reads the same value
393
+ // back. One set with `sst secret set` wins over it.
394
+ secret: (stack, d, props) =>
395
+ new Secret(stack, d.id, {
396
+ placeholder: generatedSecret(d.id, d as SecretDeclaration),
397
+ ...(props as SecretProps),
398
+ }),
385
399
 
386
400
  // The same storage as a secret, under a different role — see the component.
387
401
  credential: (stack, d, props) => new Credential(stack, d.id, props),
402
+
403
+ // In the stage's region, as mail is: a key in another region is a key
404
+ // every call crosses the world to reach.
405
+ encryption: (stack, d, props) =>
406
+ new Encryption(stack, d.id, {
407
+ ...(props as Partial<EncryptionProps>),
408
+ region:
409
+ (props as Partial<EncryptionProps>).region ??
410
+ $app.providers?.aws?.region ??
411
+ 'us-east-1',
412
+ }),
413
+
414
+ // Nothing to provision: the URL this stage calls, and the credentials kept
415
+ // as a secret.
416
+ 'external-api': (stack, d, props) => {
417
+ if (d.kind !== 'external-api') {
418
+ throw new UnknownDeclarationKind(d.kind, []);
419
+ }
420
+
421
+ return new ExternalApi(stack, d.id, {
422
+ ...(props as Partial<ExternalApiProps>),
423
+ url: d.url,
424
+ });
425
+ },
388
426
  };
389
427
 
390
428
  /**
@@ -565,9 +603,7 @@ function rootCluster(component: Provisioned): Database {
565
603
  * printed: one line per route, naming what it can reach.
566
604
  *
567
605
  * A surface with no routes says so rather than printing nothing, because "no
568
- * routes yet" and "this printed nothing" look identical otherwise — and for an
569
- * application's own API that is currently the true state, pending the endpoint
570
- * merge.
606
+ * routes" and "this printed nothing" look identical otherwise.
571
607
  *
572
608
  * Pure, so what gets printed can be asserted without a deploy.
573
609
  */
@@ -636,6 +672,8 @@ async function callersOf(
636
672
  id: string,
637
673
  manifest: ConstructManifest,
638
674
  provisioned: ProvisionedManifest,
675
+ /** The app's name — a mobile caller's scheme base when it gives none. */
676
+ project: string,
639
677
  ): Promise<{ trustedOrigins: string; cookieDomain?: string }> {
640
678
  // Every address here is a Pulumi output, not a string — a CloudFront domain
641
679
  // and an API Gateway endpoint are both known only after their resource
@@ -660,8 +698,17 @@ async function callersOf(
660
698
  const own = await resolved(provisioned[id]?.provides().url);
661
699
  const domain = cookieDomain([...(own ? [own] : []), ...origins]);
662
700
 
701
+ // A mobile caller is reached by its scheme — the bare one, deployed: what
702
+ // the store build registers.
703
+ const schemes = dependentsOf(manifest, id).flatMap((caller) => {
704
+ const app = manifest[caller];
705
+ return app?.kind === 'mobile-app'
706
+ ? mobileOrigins(schemeBase(project, app.scheme))
707
+ : [];
708
+ });
709
+
663
710
  return {
664
- trustedOrigins: origins.join(','),
711
+ trustedOrigins: [...origins, ...schemes].join(','),
665
712
  ...(domain ? { cookieDomain: domain } : {}),
666
713
  };
667
714
  }
@@ -711,6 +758,170 @@ export function isServed(id: string, manifest: ConstructManifest): boolean {
711
758
  );
712
759
  }
713
760
 
761
+ /**
762
+ * Kinds the in-order pass skips.
763
+ *
764
+ * - `mobile-app` ships through EAS and the stores. It is in the manifest for
765
+ * what it makes the surfaces trust, which `callersOf` reads off the graph.
766
+ * - `worker` is a process, not a resource: what it runs is provisioned as
767
+ * what it is — a queue's consumer by {@link subscribeConsumers}.
768
+ * - `cron` and `function` are Lambdas linked to what they depend on, so they
769
+ * are provisioned once all of it exists, by {@link provisionCompute}.
770
+ * - `telemetry` provisions nothing: where it goes is the stage's
771
+ * `deploy.telemetry`, a provider somebody else runs.
772
+ */
773
+ export const PROVISIONED_ELSEWHERE: ReadonlySet<DeclarationKind> =
774
+ new Set<DeclarationKind>([
775
+ 'mobile-app',
776
+ 'worker',
777
+ 'cron',
778
+ 'function',
779
+ 'telemetry',
780
+ ]);
781
+
782
+ /**
783
+ * Every function and cron: a Lambda running the handler the build wrote,
784
+ * linked to exactly what it depends on, in the database's network when it
785
+ * reaches one. A cron is that Lambda on its schedule; a function is reached
786
+ * at its URL, which only callers granted IAM access can invoke.
787
+ *
788
+ * After everything else is provisioned, because each links to things
789
+ * `provisionOrder` does not order before it.
790
+ */
791
+ export function provisionCompute(
792
+ stack: StackType,
793
+ manifest: ConstructManifest,
794
+ provisioned: ProvisionedManifest,
795
+ ): Cron[] {
796
+ const crons: Cron[] = [];
797
+
798
+ for (const [id, declaration] of Object.entries(manifest)) {
799
+ if (declaration?.kind !== 'function' && declaration?.kind !== 'cron') {
800
+ continue;
801
+ }
802
+
803
+ const { link } = resolveEdges(declaration.dependencies, provisioned);
804
+ const vpc = vpcFor(link);
805
+ const lambda = new Function(
806
+ stack,
807
+ declaration.kind === 'cron' ? `${id}Function` : id,
808
+ {
809
+ name: stack.logicalPrefixedName(id),
810
+ handler: declaration.handler,
811
+ link,
812
+ ...(vpc ? { vpc } : {}),
813
+ ...(declaration.kind === 'function'
814
+ ? { url: { authorization: 'iam' } }
815
+ : {}),
816
+ },
817
+ );
818
+
819
+ if (declaration.kind === 'cron') {
820
+ crons.push(
821
+ new Cron(stack, id, {
822
+ processor: lambda,
823
+ schedule: declaration.schedule as CronSchedule,
824
+ }),
825
+ );
826
+ continue;
827
+ }
828
+
829
+ assertProvides(
830
+ id,
831
+ declaration.provides,
832
+ Object.keys(lambda.provides()).map((role) =>
833
+ providedKeyFor(id, declaration.kind, role),
834
+ ),
835
+ );
836
+ provisioned[id] = lambda;
837
+ }
838
+
839
+ return crons;
840
+ }
841
+
842
+ /** A provisioned queue: what {@link subscribeConsumers} subscribes through. */
843
+ interface Consumable {
844
+ consume(consumer: {
845
+ handler: string;
846
+ link?: unknown[];
847
+ vpc?: sst.aws.Vpc;
848
+ }): unknown;
849
+ }
850
+
851
+ /**
852
+ * The network a function has to run in to reach what it links to.
853
+ *
854
+ * A database is private to its VPC, so a handler linked to one — or to a
855
+ * schema tenant or reader of one — runs inside it, or its connection times out.
856
+ */
857
+ export function vpcFor(link: readonly Provisioned[]): sst.aws.Vpc | undefined {
858
+ for (const component of link) {
859
+ const cluster = rootCluster(component);
860
+ if (cluster instanceof Database) return cluster.vpc;
861
+ }
862
+
863
+ return undefined;
864
+ }
865
+
866
+ /**
867
+ * Mount every surface's endpoints — the handlers the build wrote, each linked
868
+ * to exactly what that endpoint depends on.
869
+ *
870
+ * After every construct is provisioned, for the reason consumers are: an
871
+ * endpoint links to things `provisionOrder` does not order before its surface.
872
+ */
873
+ export function mountRoutes(
874
+ manifest: ConstructManifest,
875
+ provisioned: ProvisionedManifest,
876
+ ): void {
877
+ for (const [id, declaration] of Object.entries(manifest)) {
878
+ if (declaration?.kind !== 'rest-api') continue;
879
+
880
+ const surface = provisioned[id];
881
+ if (!(surface instanceof RestApiSurface)) continue;
882
+
883
+ for (const endpoint of declaration.endpoints) {
884
+ const { link } = resolveEdges(endpoint.dependencies, provisioned);
885
+ const vpc = vpcFor(link);
886
+ surface.mount({
887
+ method: endpoint.method,
888
+ path: endpoint.path,
889
+ handler: endpoint.handler,
890
+ link,
891
+ ...(vpc ? { vpc } : {}),
892
+ ...(endpoint.authorizer ? { authorizer: endpoint.authorizer } : {}),
893
+ });
894
+ }
895
+ }
896
+ }
897
+
898
+ /**
899
+ * Subscribe every queue's one consumer — the handler the build wrote for it,
900
+ * linked to what that consumer depends on.
901
+ *
902
+ * After every construct is provisioned, because a consumer links to things
903
+ * that `provisionOrder` does not order before the queue.
904
+ */
905
+ export function subscribeConsumers(
906
+ manifest: ConstructManifest,
907
+ provisioned: ProvisionedManifest,
908
+ ): void {
909
+ for (const [id, declaration] of Object.entries(manifest)) {
910
+ if (declaration?.kind !== 'queue') continue;
911
+
912
+ const queue = provisioned[id] as unknown as Consumable | undefined;
913
+ if (!queue) continue;
914
+
915
+ const { link } = resolveEdges(declaration.worker.dependencies, provisioned);
916
+ const vpc = vpcFor(link);
917
+ queue.consume({
918
+ handler: declaration.worker.handler,
919
+ link,
920
+ ...(vpc ? { vpc } : {}),
921
+ });
922
+ }
923
+ }
924
+
714
925
  /** The provisioner for a kind. Pure — the lookup is testable without Pulumi. */
715
926
  export function provisionerFor(kind: DeclarationKind): Provisioner {
716
927
  const provisioner = PROVISIONERS[kind];
@@ -749,20 +960,34 @@ export function assertProvides(
749
960
  * tenant, a read replica, and the surface over a bucket all resolve an address
750
961
  * off something else, and a pass in map order would find it half the time.
751
962
  */
752
- export function fromManifest(
963
+ export function fromManifest<
964
+ const M extends ConstructManifest,
965
+ const B extends ManifestBackends = {},
966
+ >(
753
967
  stack: StackType,
754
- manifest: ConstructManifest,
755
- overrides: ComponentOverrides = {},
968
+ manifest: M,
969
+ /**
970
+ * Provider-specific props, per construct id — typed from the manifest, so
971
+ * only its ids are keys, each takes what its kind's component reads, and
972
+ * what the synth cannot guess (a database's `vpc`, mail's `from`) is
973
+ * required. See {@link ManifestOverrides}.
974
+ *
975
+ * Neutral options — `versioned`, `fifo` — travel in the declaration,
976
+ * because the app legitimately has an opinion about them. Anything with S3
977
+ * in its name does not belong in application code, so it is supplied here.
978
+ * A third escape hatch needs no API: the components are returned, so
979
+ * `provisioned.Uploads.nodes.bucket` is reachable for anything else.
980
+ */
981
+ overrides: ManifestOverrides<M, B>,
756
982
  /**
757
983
  * The backend choices that are config rather than declaration.
758
984
  *
759
985
  * Defaulted the same way the local target defaults them, so a stage deployed
760
- * without saying gets the same backend a developer ran against.
986
+ * without saying gets the same backend a developer ran against. Typed
987
+ * literally, because the backend decides what is required above: an
988
+ * ElastiCache cache needs a `vpc`, Resend or SMTP mail a `url`.
761
989
  */
762
- backends: {
763
- cache?: 'upstash' | 'elasticache' | 'db';
764
- email?: 'resend' | 'ses' | 'smtp';
765
- } = {},
990
+ backends: B = {} as B,
766
991
  ): ProvisionedManifest {
767
992
  const provisioned: ProvisionedManifest = {};
768
993
 
@@ -799,11 +1024,12 @@ export function fromManifest(
799
1024
  for (const id of [...rest, ...sites]) {
800
1025
  const declaration = manifest[id];
801
1026
  if (!declaration) continue;
1027
+ if (PROVISIONED_ELSEWHERE.has(declaration.kind)) continue;
802
1028
 
803
1029
  const component = provisionerFor(declaration.kind)(
804
1030
  stack,
805
1031
  declaration,
806
- overrides[id] ?? {},
1032
+ (overrides as Record<string, Record<string, unknown>>)[id] ?? {},
807
1033
  context,
808
1034
  );
809
1035
 
@@ -822,6 +1048,12 @@ export function fromManifest(
822
1048
  provisioned[id] = component;
823
1049
  }
824
1050
 
1051
+ // The functions and crons, each queue's one consumer and each surface's
1052
+ // endpoints, once everything they link to exists.
1053
+ provisionCompute(stack, manifest, provisioned);
1054
+ subscribeConsumers(manifest, provisioned);
1055
+ mountRoutes(manifest, provisioned);
1056
+
825
1057
  for (const line of describeRoutes(manifest)) console.log(line);
826
1058
 
827
1059
  // Now everything exists, so every surface's callers can be resolved — the
@@ -829,7 +1061,7 @@ export function fromManifest(
829
1061
  for (const [id, deferred] of callers) {
830
1062
  // Resolved with the promise itself: the addresses it needs are Pulumi
831
1063
  // outputs, so the answer arrives when they do.
832
- deferred.resolve(callersOf(id, manifest, provisioned));
1064
+ deferred.resolve(callersOf(id, manifest, provisioned, $app.name));
833
1065
  }
834
1066
 
835
1067
  // Last, and only now: the roles exist as passwords and secrets from the
@@ -844,6 +1076,30 @@ export function fromManifest(
844
1076
  return provisioned;
845
1077
  }
846
1078
 
1079
+ /**
1080
+ * A secret in its declared format — the alphabet `drawSecret` draws from on
1081
+ * every other stage, with one symbol guaranteed as it guarantees one.
1082
+ *
1083
+ * The format is fixed at the first deploy, as a server stage's is: changing it
1084
+ * would replace the password, and every later deploy is meant to read back the
1085
+ * one it had.
1086
+ */
1087
+ function generatedSecret(id: string, format: SecretFormat) {
1088
+ const symbols = [...new Set(format.symbols ?? SECRET_SYMBOLS)].join('');
1089
+
1090
+ return new RandomPassword(
1091
+ `${id}Generated`,
1092
+ {
1093
+ length: format.length ?? SECRET_LENGTH,
1094
+ special: symbols.length > 0,
1095
+ ...(symbols ? { overrideSpecial: symbols, minSpecial: 1 } : {}),
1096
+ },
1097
+ {
1098
+ ignoreChanges: ['length', 'special', 'overrideSpecial', 'minSpecial'],
1099
+ },
1100
+ ).result;
1101
+ }
1102
+
847
1103
  /** What one function receives from its edges. */
848
1104
  export interface ResolvedEdges {
849
1105
  /** The components it may reach. SST turns these into IAM *and* injects their