@geekmidas/cloud 10.0.0-alpha.11 → 10.0.0-alpha.111
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/README.md +14 -6
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +2 -3
- package/dist/utils/index.cjs +106 -5
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +62 -2
- package/dist/utils/index.d.cts.map +1 -0
- package/dist/utils/index.d.mts +62 -2
- package/dist/utils/index.d.mts.map +1 -0
- package/dist/utils/index.mjs +78 -2
- package/dist/utils/index.mjs.map +1 -0
- package/package.json +9 -8
- package/src/dokploy/Application.ts +52 -5
- package/src/dokploy/__tests__/Application.provider.spec.ts +206 -0
- package/src/sst/__tests__/api.spec.ts +185 -0
- package/src/sst/__tests__/encryption.spec.ts +90 -0
- package/src/sst/__tests__/fromManifest.integration.spec.ts +452 -0
- package/src/sst/__tests__/fromManifest.secret.spec.ts +79 -0
- package/src/sst/__tests__/fromManifest.spec.ts +306 -5
- package/src/sst/__tests__/function.spec.ts +121 -0
- package/src/sst/__tests__/links.spec.ts +209 -0
- package/src/sst/__tests__/surfaces.spec.ts +17 -0
- package/src/sst/__type-tests__/overrides.type-test.ts +152 -0
- package/src/sst/aws/Api.ts +16 -44
- package/src/sst/aws/Cache.ts +4 -4
- package/src/sst/aws/Credential.ts +42 -9
- package/src/sst/aws/Cron.ts +5 -45
- package/src/sst/aws/Database.ts +15 -1
- package/src/sst/aws/DatabaseBootstrap.ts +3 -0
- package/src/sst/aws/Encryption.ts +91 -0
- package/src/sst/aws/ExternalApi.ts +58 -0
- package/src/sst/aws/Function.ts +9 -38
- package/src/sst/aws/Queue.ts +34 -1
- package/src/sst/aws/RestApiSurface.ts +36 -14
- package/src/sst/aws/Secret.ts +5 -3
- package/src/sst/aws/bootstrap/handler.ts +10 -1
- package/src/sst/fromManifest.ts +299 -46
- package/src/sst/index.ts +6 -1
- package/src/sst/overrides.ts +133 -0
- package/dist/index-B5CZ1xVf.d.cts +0 -64
- package/dist/index-B5CZ1xVf.d.cts.map +0 -1
- package/dist/index-DhHRjduZ.d.mts +0 -64
- package/dist/index-DhHRjduZ.d.mts.map +0 -1
- package/dist/utils-B1a2UuEO.cjs +0 -129
- package/dist/utils-B1a2UuEO.cjs.map +0 -1
- package/dist/utils-DMOXJ27j.mjs +0 -83
- package/dist/utils-DMOXJ27j.mjs.map +0 -1
- package/src/sst/__type-tests__/manifest.type-test.ts +0 -77
package/src/sst/fromManifest.ts
CHANGED
|
@@ -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 {
|
|
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
|
/**
|
|
@@ -98,8 +110,8 @@ export interface ProvisionContext {
|
|
|
98
110
|
/**
|
|
99
111
|
* Where a declared cache lives, and who delivers mail.
|
|
100
112
|
*
|
|
101
|
-
* Deployment choices rather than declarations, for the same reason
|
|
102
|
-
*
|
|
113
|
+
* Deployment choices rather than declarations, for the same reason the
|
|
114
|
+
* event broker is: the same application code caches into any of them and
|
|
103
115
|
* sends through any of them. They reach the provisioner here because it is
|
|
104
116
|
* the only place that knows both the choice and the manifest.
|
|
105
117
|
*/
|
|
@@ -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
|
-
|
|
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,21 @@ 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,
|
|
240
242
|
...(hasReader(d.id, context) ? { reader: readerRole(runtime) } : {}),
|
|
241
243
|
});
|
|
244
|
+
|
|
245
|
+
// Registering the role was half of it: its handlers have to connect as
|
|
246
|
+
// it too, or the DDL creates a role nothing uses and every handler
|
|
247
|
+
// linked to the database still holds the master.
|
|
248
|
+
if (credentials) {
|
|
249
|
+
cluster.connectAs({ user: runtime, password: credentials.runtime });
|
|
250
|
+
}
|
|
242
251
|
}
|
|
243
252
|
|
|
244
253
|
return cluster;
|
|
@@ -363,17 +372,12 @@ const PROVISIONERS: Partial<Record<DeclarationKind, Provisioner>> = {
|
|
|
363
372
|
},
|
|
364
373
|
|
|
365
374
|
/**
|
|
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.
|
|
375
|
+
* The surface, without its routes yet.
|
|
372
376
|
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
+
* Its address is what a site inlines as `VITE_API_URL`, what an auth server
|
|
378
|
+
* puts on its trusted-origin list, and what the cookie domain derives from —
|
|
379
|
+
* so it is provisioned in order, and its endpoints are mounted by
|
|
380
|
+
* {@link mountRoutes} once everything they link to exists.
|
|
377
381
|
*/
|
|
378
382
|
'rest-api': (stack, d, props, context) =>
|
|
379
383
|
new RestApiSurface(stack, d.id, {
|
|
@@ -381,10 +385,41 @@ const PROVISIONERS: Partial<Record<DeclarationKind, Provisioner>> = {
|
|
|
381
385
|
callers: context.callers?.get(d.id)?.promise,
|
|
382
386
|
}),
|
|
383
387
|
|
|
384
|
-
|
|
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
|
+
}),
|
|
385
396
|
|
|
386
397
|
// The same storage as a secret, under a different role — see the component.
|
|
387
398
|
credential: (stack, d, props) => new Credential(stack, d.id, props),
|
|
399
|
+
|
|
400
|
+
// In the stage's region, as mail is: a key in another region is a key
|
|
401
|
+
// every call crosses the world to reach.
|
|
402
|
+
encryption: (stack, d, props) =>
|
|
403
|
+
new Encryption(stack, d.id, {
|
|
404
|
+
...(props as Partial<EncryptionProps>),
|
|
405
|
+
region:
|
|
406
|
+
(props as Partial<EncryptionProps>).region ??
|
|
407
|
+
$app.providers?.aws?.region ??
|
|
408
|
+
'us-east-1',
|
|
409
|
+
}),
|
|
410
|
+
|
|
411
|
+
// Nothing to provision: the URL this stage calls, and the credentials kept
|
|
412
|
+
// as a secret.
|
|
413
|
+
'external-api': (stack, d, props) => {
|
|
414
|
+
if (d.kind !== 'external-api') {
|
|
415
|
+
throw new UnknownDeclarationKind(d.kind, []);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return new ExternalApi(stack, d.id, {
|
|
419
|
+
...(props as Partial<ExternalApiProps>),
|
|
420
|
+
url: d.url,
|
|
421
|
+
});
|
|
422
|
+
},
|
|
388
423
|
};
|
|
389
424
|
|
|
390
425
|
/**
|
|
@@ -565,9 +600,7 @@ function rootCluster(component: Provisioned): Database {
|
|
|
565
600
|
* printed: one line per route, naming what it can reach.
|
|
566
601
|
*
|
|
567
602
|
* A surface with no routes says so rather than printing nothing, because "no
|
|
568
|
-
* routes
|
|
569
|
-
* application's own API that is currently the true state, pending the endpoint
|
|
570
|
-
* merge.
|
|
603
|
+
* routes" and "this printed nothing" look identical otherwise.
|
|
571
604
|
*
|
|
572
605
|
* Pure, so what gets printed can be asserted without a deploy.
|
|
573
606
|
*/
|
|
@@ -636,6 +669,8 @@ async function callersOf(
|
|
|
636
669
|
id: string,
|
|
637
670
|
manifest: ConstructManifest,
|
|
638
671
|
provisioned: ProvisionedManifest,
|
|
672
|
+
/** The app's name — a mobile caller's scheme base when it gives none. */
|
|
673
|
+
project: string,
|
|
639
674
|
): Promise<{ trustedOrigins: string; cookieDomain?: string }> {
|
|
640
675
|
// Every address here is a Pulumi output, not a string — a CloudFront domain
|
|
641
676
|
// and an API Gateway endpoint are both known only after their resource
|
|
@@ -660,8 +695,17 @@ async function callersOf(
|
|
|
660
695
|
const own = await resolved(provisioned[id]?.provides().url);
|
|
661
696
|
const domain = cookieDomain([...(own ? [own] : []), ...origins]);
|
|
662
697
|
|
|
698
|
+
// A mobile caller is reached by its scheme — the bare one, deployed: what
|
|
699
|
+
// the store build registers.
|
|
700
|
+
const schemes = dependentsOf(manifest, id).flatMap((caller) => {
|
|
701
|
+
const app = manifest[caller];
|
|
702
|
+
return app?.kind === 'mobile-app'
|
|
703
|
+
? mobileOrigins(schemeBase(project, app.scheme))
|
|
704
|
+
: [];
|
|
705
|
+
});
|
|
706
|
+
|
|
663
707
|
return {
|
|
664
|
-
trustedOrigins: origins.join(','),
|
|
708
|
+
trustedOrigins: [...origins, ...schemes].join(','),
|
|
665
709
|
...(domain ? { cookieDomain: domain } : {}),
|
|
666
710
|
};
|
|
667
711
|
}
|
|
@@ -711,6 +755,170 @@ export function isServed(id: string, manifest: ConstructManifest): boolean {
|
|
|
711
755
|
);
|
|
712
756
|
}
|
|
713
757
|
|
|
758
|
+
/**
|
|
759
|
+
* Kinds the in-order pass skips.
|
|
760
|
+
*
|
|
761
|
+
* - `mobile-app` ships through EAS and the stores. It is in the manifest for
|
|
762
|
+
* what it makes the surfaces trust, which `callersOf` reads off the graph.
|
|
763
|
+
* - `worker` is a process, not a resource: what it runs is provisioned as
|
|
764
|
+
* what it is — a queue's consumer by {@link subscribeConsumers}.
|
|
765
|
+
* - `cron` and `function` are Lambdas linked to what they depend on, so they
|
|
766
|
+
* are provisioned once all of it exists, by {@link provisionCompute}.
|
|
767
|
+
* - `telemetry` provisions nothing: where it goes is the stage's
|
|
768
|
+
* `deploy.telemetry`, a provider somebody else runs.
|
|
769
|
+
*/
|
|
770
|
+
export const PROVISIONED_ELSEWHERE: ReadonlySet<DeclarationKind> =
|
|
771
|
+
new Set<DeclarationKind>([
|
|
772
|
+
'mobile-app',
|
|
773
|
+
'worker',
|
|
774
|
+
'cron',
|
|
775
|
+
'function',
|
|
776
|
+
'telemetry',
|
|
777
|
+
]);
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Every function and cron: a Lambda running the handler the build wrote,
|
|
781
|
+
* linked to exactly what it depends on, in the database's network when it
|
|
782
|
+
* reaches one. A cron is that Lambda on its schedule; a function is reached
|
|
783
|
+
* at its URL, which only callers granted IAM access can invoke.
|
|
784
|
+
*
|
|
785
|
+
* After everything else is provisioned, because each links to things
|
|
786
|
+
* `provisionOrder` does not order before it.
|
|
787
|
+
*/
|
|
788
|
+
export function provisionCompute(
|
|
789
|
+
stack: StackType,
|
|
790
|
+
manifest: ConstructManifest,
|
|
791
|
+
provisioned: ProvisionedManifest,
|
|
792
|
+
): Cron[] {
|
|
793
|
+
const crons: Cron[] = [];
|
|
794
|
+
|
|
795
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
796
|
+
if (declaration?.kind !== 'function' && declaration?.kind !== 'cron') {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const { link } = resolveEdges(declaration.dependencies, provisioned);
|
|
801
|
+
const vpc = vpcFor(link);
|
|
802
|
+
const lambda = new Function(
|
|
803
|
+
stack,
|
|
804
|
+
declaration.kind === 'cron' ? `${id}Function` : id,
|
|
805
|
+
{
|
|
806
|
+
name: stack.logicalPrefixedName(id),
|
|
807
|
+
handler: declaration.handler,
|
|
808
|
+
link,
|
|
809
|
+
...(vpc ? { vpc } : {}),
|
|
810
|
+
...(declaration.kind === 'function'
|
|
811
|
+
? { url: { authorization: 'iam' } }
|
|
812
|
+
: {}),
|
|
813
|
+
},
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
if (declaration.kind === 'cron') {
|
|
817
|
+
crons.push(
|
|
818
|
+
new Cron(stack, id, {
|
|
819
|
+
processor: lambda,
|
|
820
|
+
schedule: declaration.schedule as CronSchedule,
|
|
821
|
+
}),
|
|
822
|
+
);
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
assertProvides(
|
|
827
|
+
id,
|
|
828
|
+
declaration.provides,
|
|
829
|
+
Object.keys(lambda.provides()).map((role) =>
|
|
830
|
+
providedKeyFor(id, declaration.kind, role),
|
|
831
|
+
),
|
|
832
|
+
);
|
|
833
|
+
provisioned[id] = lambda;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return crons;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/** A provisioned queue: what {@link subscribeConsumers} subscribes through. */
|
|
840
|
+
interface Consumable {
|
|
841
|
+
consume(consumer: {
|
|
842
|
+
handler: string;
|
|
843
|
+
link?: unknown[];
|
|
844
|
+
vpc?: sst.aws.Vpc;
|
|
845
|
+
}): unknown;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* The network a function has to run in to reach what it links to.
|
|
850
|
+
*
|
|
851
|
+
* A database is private to its VPC, so a handler linked to one — or to a
|
|
852
|
+
* schema tenant or reader of one — runs inside it, or its connection times out.
|
|
853
|
+
*/
|
|
854
|
+
export function vpcFor(link: readonly Provisioned[]): sst.aws.Vpc | undefined {
|
|
855
|
+
for (const component of link) {
|
|
856
|
+
const cluster = rootCluster(component);
|
|
857
|
+
if (cluster instanceof Database) return cluster.vpc;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return undefined;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Mount every surface's endpoints — the handlers the build wrote, each linked
|
|
865
|
+
* to exactly what that endpoint depends on.
|
|
866
|
+
*
|
|
867
|
+
* After every construct is provisioned, for the reason consumers are: an
|
|
868
|
+
* endpoint links to things `provisionOrder` does not order before its surface.
|
|
869
|
+
*/
|
|
870
|
+
export function mountRoutes(
|
|
871
|
+
manifest: ConstructManifest,
|
|
872
|
+
provisioned: ProvisionedManifest,
|
|
873
|
+
): void {
|
|
874
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
875
|
+
if (declaration?.kind !== 'rest-api') continue;
|
|
876
|
+
|
|
877
|
+
const surface = provisioned[id];
|
|
878
|
+
if (!(surface instanceof RestApiSurface)) continue;
|
|
879
|
+
|
|
880
|
+
for (const endpoint of declaration.endpoints) {
|
|
881
|
+
const { link } = resolveEdges(endpoint.dependencies, provisioned);
|
|
882
|
+
const vpc = vpcFor(link);
|
|
883
|
+
surface.mount({
|
|
884
|
+
method: endpoint.method,
|
|
885
|
+
path: endpoint.path,
|
|
886
|
+
handler: endpoint.handler,
|
|
887
|
+
link,
|
|
888
|
+
...(vpc ? { vpc } : {}),
|
|
889
|
+
...(endpoint.authorizer ? { authorizer: endpoint.authorizer } : {}),
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Subscribe every queue's one consumer — the handler the build wrote for it,
|
|
897
|
+
* linked to what that consumer depends on.
|
|
898
|
+
*
|
|
899
|
+
* After every construct is provisioned, because a consumer links to things
|
|
900
|
+
* that `provisionOrder` does not order before the queue.
|
|
901
|
+
*/
|
|
902
|
+
export function subscribeConsumers(
|
|
903
|
+
manifest: ConstructManifest,
|
|
904
|
+
provisioned: ProvisionedManifest,
|
|
905
|
+
): void {
|
|
906
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
907
|
+
if (declaration?.kind !== 'queue') continue;
|
|
908
|
+
|
|
909
|
+
const queue = provisioned[id] as unknown as Consumable | undefined;
|
|
910
|
+
if (!queue) continue;
|
|
911
|
+
|
|
912
|
+
const { link } = resolveEdges(declaration.worker.dependencies, provisioned);
|
|
913
|
+
const vpc = vpcFor(link);
|
|
914
|
+
queue.consume({
|
|
915
|
+
handler: declaration.worker.handler,
|
|
916
|
+
link,
|
|
917
|
+
...(vpc ? { vpc } : {}),
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
714
922
|
/** The provisioner for a kind. Pure — the lookup is testable without Pulumi. */
|
|
715
923
|
export function provisionerFor(kind: DeclarationKind): Provisioner {
|
|
716
924
|
const provisioner = PROVISIONERS[kind];
|
|
@@ -749,20 +957,34 @@ export function assertProvides(
|
|
|
749
957
|
* tenant, a read replica, and the surface over a bucket all resolve an address
|
|
750
958
|
* off something else, and a pass in map order would find it half the time.
|
|
751
959
|
*/
|
|
752
|
-
export function fromManifest
|
|
960
|
+
export function fromManifest<
|
|
961
|
+
const M extends ConstructManifest,
|
|
962
|
+
const B extends ManifestBackends = {},
|
|
963
|
+
>(
|
|
753
964
|
stack: StackType,
|
|
754
|
-
manifest:
|
|
755
|
-
|
|
965
|
+
manifest: M,
|
|
966
|
+
/**
|
|
967
|
+
* Provider-specific props, per construct id — typed from the manifest, so
|
|
968
|
+
* only its ids are keys, each takes what its kind's component reads, and
|
|
969
|
+
* what the synth cannot guess (a database's `vpc`, mail's `from`) is
|
|
970
|
+
* required. See {@link ManifestOverrides}.
|
|
971
|
+
*
|
|
972
|
+
* Neutral options — `versioned`, `fifo` — travel in the declaration,
|
|
973
|
+
* because the app legitimately has an opinion about them. Anything with S3
|
|
974
|
+
* in its name does not belong in application code, so it is supplied here.
|
|
975
|
+
* A third escape hatch needs no API: the components are returned, so
|
|
976
|
+
* `provisioned.Uploads.nodes.bucket` is reachable for anything else.
|
|
977
|
+
*/
|
|
978
|
+
overrides: ManifestOverrides<M, B>,
|
|
756
979
|
/**
|
|
757
980
|
* The backend choices that are config rather than declaration.
|
|
758
981
|
*
|
|
759
982
|
* Defaulted the same way the local target defaults them, so a stage deployed
|
|
760
|
-
* without saying gets the same backend a developer ran against.
|
|
983
|
+
* without saying gets the same backend a developer ran against. Typed
|
|
984
|
+
* literally, because the backend decides what is required above: an
|
|
985
|
+
* ElastiCache cache needs a `vpc`, Resend or SMTP mail a `url`.
|
|
761
986
|
*/
|
|
762
|
-
backends: {
|
|
763
|
-
cache?: 'upstash' | 'elasticache' | 'db';
|
|
764
|
-
email?: 'resend' | 'ses' | 'smtp';
|
|
765
|
-
} = {},
|
|
987
|
+
backends: B = {} as B,
|
|
766
988
|
): ProvisionedManifest {
|
|
767
989
|
const provisioned: ProvisionedManifest = {};
|
|
768
990
|
|
|
@@ -799,11 +1021,12 @@ export function fromManifest(
|
|
|
799
1021
|
for (const id of [...rest, ...sites]) {
|
|
800
1022
|
const declaration = manifest[id];
|
|
801
1023
|
if (!declaration) continue;
|
|
1024
|
+
if (PROVISIONED_ELSEWHERE.has(declaration.kind)) continue;
|
|
802
1025
|
|
|
803
1026
|
const component = provisionerFor(declaration.kind)(
|
|
804
1027
|
stack,
|
|
805
1028
|
declaration,
|
|
806
|
-
overrides[id] ?? {},
|
|
1029
|
+
(overrides as Record<string, Record<string, unknown>>)[id] ?? {},
|
|
807
1030
|
context,
|
|
808
1031
|
);
|
|
809
1032
|
|
|
@@ -822,6 +1045,12 @@ export function fromManifest(
|
|
|
822
1045
|
provisioned[id] = component;
|
|
823
1046
|
}
|
|
824
1047
|
|
|
1048
|
+
// The functions and crons, each queue's one consumer and each surface's
|
|
1049
|
+
// endpoints, once everything they link to exists.
|
|
1050
|
+
provisionCompute(stack, manifest, provisioned);
|
|
1051
|
+
subscribeConsumers(manifest, provisioned);
|
|
1052
|
+
mountRoutes(manifest, provisioned);
|
|
1053
|
+
|
|
825
1054
|
for (const line of describeRoutes(manifest)) console.log(line);
|
|
826
1055
|
|
|
827
1056
|
// Now everything exists, so every surface's callers can be resolved — the
|
|
@@ -829,7 +1058,7 @@ export function fromManifest(
|
|
|
829
1058
|
for (const [id, deferred] of callers) {
|
|
830
1059
|
// Resolved with the promise itself: the addresses it needs are Pulumi
|
|
831
1060
|
// outputs, so the answer arrives when they do.
|
|
832
|
-
deferred.resolve(callersOf(id, manifest, provisioned));
|
|
1061
|
+
deferred.resolve(callersOf(id, manifest, provisioned, $app.name));
|
|
833
1062
|
}
|
|
834
1063
|
|
|
835
1064
|
// Last, and only now: the roles exist as passwords and secrets from the
|
|
@@ -844,6 +1073,30 @@ export function fromManifest(
|
|
|
844
1073
|
return provisioned;
|
|
845
1074
|
}
|
|
846
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
|
+
|
|
847
1100
|
/** What one function receives from its edges. */
|
|
848
1101
|
export interface ResolvedEdges {
|
|
849
1102
|
/** The components it may reach. SST turns these into IAM *and* injects their
|
package/src/sst/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ export {
|
|
|
34
34
|
} from './aws/DatabaseBootstrap';
|
|
35
35
|
export { DatabaseReader, DatabaseSchema } from './aws/DerivedDatabase';
|
|
36
36
|
export { Email, EmailNeedsUrl, type EmailProps } from './aws/Email';
|
|
37
|
+
export { Encryption, type EncryptionProps } from './aws/Encryption';
|
|
37
38
|
export { FileServer, type FileServerProps } from './aws/FileServer';
|
|
38
39
|
export { Function, type FunctionProps } from './aws/Function';
|
|
39
40
|
export {
|
|
@@ -61,7 +62,6 @@ export {
|
|
|
61
62
|
} from './errors';
|
|
62
63
|
export {
|
|
63
64
|
assertProvides,
|
|
64
|
-
type ComponentOverrides,
|
|
65
65
|
describeRoutes,
|
|
66
66
|
fromManifest,
|
|
67
67
|
isServed,
|
|
@@ -75,4 +75,9 @@ export {
|
|
|
75
75
|
} from './fromManifest';
|
|
76
76
|
export { type GkmLinkable, ResourceType } from './Linkable';
|
|
77
77
|
export { kebab, prefixedName, regionOfArn } from './naming';
|
|
78
|
+
export type {
|
|
79
|
+
ManifestBackends,
|
|
80
|
+
ManifestOverrides,
|
|
81
|
+
OverrideFor,
|
|
82
|
+
} from './overrides';
|
|
78
83
|
export { Stack, type StackType } from './Stack';
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a deploy can say about each construct, inferred from the manifest.
|
|
3
|
+
*
|
|
4
|
+
* `fromManifest` used to take `Record<string, Record<string, unknown>>`, so a
|
|
5
|
+
* misspelt id, a prop the component never reads, and a missing VPC all passed
|
|
6
|
+
* the type checker — the first two silently, the last at synth, mid-deploy.
|
|
7
|
+
* The manifest is emitted `as const`, so every id's kind is known statically,
|
|
8
|
+
* and the kind decides what can be overridden:
|
|
9
|
+
*
|
|
10
|
+
* - only the manifest's own ids are keys;
|
|
11
|
+
* - each takes the props its kind's provisioner reads, minus those the
|
|
12
|
+
* declaration already decides (a site's `path`, a file server's `origin`);
|
|
13
|
+
* - what the synth refuses to guess is required — a database's `vpc`, mail's
|
|
14
|
+
* `from`, and, by backend, an ElastiCache `vpc` or a non-SES mail `url`;
|
|
15
|
+
* - a kind with nothing to override takes no key at all.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ConstructManifest, DeclarationKind } from '@geekmidas/manifest';
|
|
19
|
+
import type { CacheProps } from './aws/Cache';
|
|
20
|
+
import type { CredentialProps } from './aws/Credential';
|
|
21
|
+
import type { DatabaseProps } from './aws/Database';
|
|
22
|
+
import type { EncryptionProps } from './aws/Encryption';
|
|
23
|
+
import type { ExternalApiProps } from './aws/ExternalApi';
|
|
24
|
+
import type { FileServerProps } from './aws/FileServer';
|
|
25
|
+
import type { ObjectStorageProps } from './aws/ObjectStorage';
|
|
26
|
+
import type { QueueProps } from './aws/Queue';
|
|
27
|
+
import type { SecretProps } from './aws/Secret';
|
|
28
|
+
import type { StaticSiteProps } from './aws/StaticSite';
|
|
29
|
+
import type { TopicProps } from './aws/Topic';
|
|
30
|
+
|
|
31
|
+
/** The backend choices that are config rather than declaration. */
|
|
32
|
+
export interface ManifestBackends {
|
|
33
|
+
cache?: 'upstash' | 'elasticache' | 'db';
|
|
34
|
+
email?: 'resend' | 'ses' | 'smtp';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The backend a choice resolves to — the provisioner's default when unset. */
|
|
38
|
+
type CacheBackendOf<B extends ManifestBackends> =
|
|
39
|
+
B['cache'] extends NonNullable<ManifestBackends['cache']>
|
|
40
|
+
? B['cache']
|
|
41
|
+
: 'upstash';
|
|
42
|
+
type EmailBackendOf<B extends ManifestBackends> =
|
|
43
|
+
B['email'] extends NonNullable<ManifestBackends['email']>
|
|
44
|
+
? B['email']
|
|
45
|
+
: 'ses';
|
|
46
|
+
|
|
47
|
+
type CacheOverride<Backend> = Backend extends 'elasticache'
|
|
48
|
+
? { vpc: NonNullable<CacheProps['vpc']> }
|
|
49
|
+
: Backend extends 'upstash'
|
|
50
|
+
? { url?: CacheProps['url']; region?: string }
|
|
51
|
+
: never;
|
|
52
|
+
|
|
53
|
+
type EmailOverride<Backend> = {
|
|
54
|
+
/** Every provider rejects an unverified sender, so there is no default. */
|
|
55
|
+
from: $util.Input<string>;
|
|
56
|
+
region?: $util.Input<string>;
|
|
57
|
+
} & (Backend extends 'ses'
|
|
58
|
+
? { url?: $util.Input<string> }
|
|
59
|
+
: // Resend and a relay are accounts somebody created: their URL is theirs.
|
|
60
|
+
{ url: $util.Input<string> });
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* What one declaration takes, by its kind and the backends; `never` for a kind
|
|
64
|
+
* with nothing to override, which drops its id from the keys.
|
|
65
|
+
*/
|
|
66
|
+
export type OverrideFor<D, B extends ManifestBackends = {}> = D extends {
|
|
67
|
+
kind: infer K extends DeclarationKind;
|
|
68
|
+
}
|
|
69
|
+
? K extends 'objects'
|
|
70
|
+
? Partial<ObjectStorageProps>
|
|
71
|
+
: K extends 'file-server'
|
|
72
|
+
? Partial<Omit<FileServerProps, 'origin'>>
|
|
73
|
+
: K extends 'site'
|
|
74
|
+
? Partial<Omit<StaticSiteProps, 'path' | 'variant' | 'environment'>>
|
|
75
|
+
: K extends 'queue'
|
|
76
|
+
? Partial<Omit<QueueProps, 'fifo'>>
|
|
77
|
+
: K extends 'topic'
|
|
78
|
+
? Partial<TopicProps>
|
|
79
|
+
: K extends 'database'
|
|
80
|
+
? Omit<DatabaseProps, 'schema' | 'version'>
|
|
81
|
+
: K extends 'cache'
|
|
82
|
+
? // In a database: same address, one more table, nothing to say.
|
|
83
|
+
D extends { of: string }
|
|
84
|
+
? never
|
|
85
|
+
: CacheOverride<CacheBackendOf<B>>
|
|
86
|
+
: K extends 'email'
|
|
87
|
+
? EmailOverride<EmailBackendOf<B>>
|
|
88
|
+
: K extends 'rest-api'
|
|
89
|
+
? Partial<sst.aws.ApiGatewayV2Args>
|
|
90
|
+
: K extends 'secret'
|
|
91
|
+
? SecretProps
|
|
92
|
+
: K extends 'credential'
|
|
93
|
+
? CredentialProps
|
|
94
|
+
: K extends 'encryption'
|
|
95
|
+
? Partial<EncryptionProps>
|
|
96
|
+
: K extends 'external-api'
|
|
97
|
+
? Omit<ExternalApiProps, 'url'>
|
|
98
|
+
: // Derived databases, compute, mobile apps, OIDC.
|
|
99
|
+
never
|
|
100
|
+
: never;
|
|
101
|
+
|
|
102
|
+
/** Whether `{}` satisfies an override — whether the id can be left out. */
|
|
103
|
+
type IsOptional<T> = {} extends T ? true : false;
|
|
104
|
+
|
|
105
|
+
type Overridable<M, B extends ManifestBackends> = {
|
|
106
|
+
[Id in keyof M]: [OverrideFor<M[Id], B>] extends [never] ? never : Id;
|
|
107
|
+
}[keyof M];
|
|
108
|
+
|
|
109
|
+
type RequiredIds<M, B extends ManifestBackends> = {
|
|
110
|
+
[Id in Overridable<M, B>]: IsOptional<OverrideFor<M[Id], B>> extends true
|
|
111
|
+
? never
|
|
112
|
+
: Id;
|
|
113
|
+
}[Overridable<M, B>];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The overrides a manifest accepts, keyed by its own construct ids.
|
|
117
|
+
*
|
|
118
|
+
* A manifest typed only as `ConstructManifest` — ids unknown — falls back to
|
|
119
|
+
* the untyped record, and the provisioners' synth-time checks still apply.
|
|
120
|
+
*/
|
|
121
|
+
export type ManifestOverrides<
|
|
122
|
+
M extends ConstructManifest,
|
|
123
|
+
B extends ManifestBackends = {},
|
|
124
|
+
> = string extends keyof M
|
|
125
|
+
? Record<string, Record<string, unknown>>
|
|
126
|
+
: {
|
|
127
|
+
[Id in RequiredIds<M, B>]: OverrideFor<M[Id], B>;
|
|
128
|
+
} & {
|
|
129
|
+
[Id in Exclude<Overridable<M, B>, RequiredIds<M, B>>]?: OverrideFor<
|
|
130
|
+
M[Id],
|
|
131
|
+
B
|
|
132
|
+
>;
|
|
133
|
+
};
|