@geekmidas/cloud 10.0.0-alpha.9 → 10.0.0-alpha.91
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 +427 -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/bootstrap/handler.ts +10 -1
- package/src/sst/fromManifest.ts +261 -44
- 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/aws/Function.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import type { EnvValidator, ValidationResult } from '@geekmidas/envkit/sst';
|
|
2
|
-
import {
|
|
3
|
-
type FunctionInfo,
|
|
4
|
-
flattenManifestField,
|
|
5
|
-
type ManifestField,
|
|
6
|
-
} from '@geekmidas/manifest';
|
|
7
2
|
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
8
3
|
import { LinkedEnvironment } from '../LinkedEnvironment';
|
|
9
4
|
import type { StackType } from '../Stack';
|
|
@@ -66,9 +61,10 @@ export class Function<
|
|
|
66
61
|
super(id, {
|
|
67
62
|
...fnArgs,
|
|
68
63
|
environment: mergedEnvironment,
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
link:
|
|
64
|
+
// Resolved from `links` when `envVars` names what it needs; otherwise
|
|
65
|
+
// the `link` it was given — the edges a manifest declared.
|
|
66
|
+
link:
|
|
67
|
+
envVars.length > 0 ? linked.resolveLink(envVars) : (fnArgs.link ?? []),
|
|
72
68
|
runtime: runtime ?? 'nodejs24.x',
|
|
73
69
|
logging: logging ?? { format: 'json' },
|
|
74
70
|
});
|
|
@@ -78,40 +74,15 @@ export class Function<
|
|
|
78
74
|
this.envVars = envVars;
|
|
79
75
|
}
|
|
80
76
|
|
|
77
|
+
/** Its address, for whatever declared an edge to it. */
|
|
78
|
+
provides(): Record<string, $util.Input<string>> {
|
|
79
|
+
return { url: this.url };
|
|
80
|
+
}
|
|
81
|
+
|
|
81
82
|
/** Re-runs validation and returns the result (does not throw). */
|
|
82
83
|
validate(): ValidationResult {
|
|
83
84
|
return this.validator.validate(this.envVars);
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Build one `Function` per entry in a `gkm build` manifest's `functions`
|
|
88
|
-
* field (flat or partitioned). Shared `props` (e.g. `links`) apply to every
|
|
89
|
-
* function.
|
|
90
|
-
*
|
|
91
|
-
* ```ts
|
|
92
|
-
* Function.fromManifest(stack, manifest.functions, { links: [db] });
|
|
93
|
-
* ```
|
|
94
|
-
*/
|
|
95
|
-
static fromManifest<
|
|
96
|
-
TStage extends string = string,
|
|
97
|
-
TDomain extends string = string,
|
|
98
|
-
>(
|
|
99
|
-
stack: StackType<TStage, TDomain>,
|
|
100
|
-
functions: ManifestField<FunctionInfo>,
|
|
101
|
-
props: Omit<FunctionProps, 'handler'> = {},
|
|
102
|
-
): Function<TStage, TDomain>[] {
|
|
103
|
-
return flattenManifestField(functions).map(
|
|
104
|
-
(fn) =>
|
|
105
|
-
new Function(stack, fn.name, {
|
|
106
|
-
...props,
|
|
107
|
-
name: stack.logicalPrefixedName(fn.name),
|
|
108
|
-
handler: fn.handler,
|
|
109
|
-
envVars: fn.environment,
|
|
110
|
-
timeout: fn.timeout ? `${fn.timeout} seconds` : undefined,
|
|
111
|
-
memory: fn.memorySize ? `${fn.memorySize} MB` : undefined,
|
|
112
|
-
}),
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
86
|
}
|
|
116
87
|
|
|
117
88
|
export interface FunctionProps extends sst.aws.FunctionArgs {
|
package/src/sst/aws/Queue.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
+
}
|
|
@@ -28,6 +28,11 @@ 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;
|
|
31
36
|
passwords: { runtime: string; owner: string; reader?: string };
|
|
32
37
|
}
|
|
33
38
|
|
|
@@ -75,7 +80,11 @@ export async function handler(event: BootstrapEvent): Promise<BootstrapResult> {
|
|
|
75
80
|
|
|
76
81
|
try {
|
|
77
82
|
for (const tenant of event.tenants) {
|
|
78
|
-
|
|
83
|
+
const spec = {
|
|
84
|
+
...tenant,
|
|
85
|
+
...(tenant.ownsDatabase ? { database: event.master.database } : {}),
|
|
86
|
+
};
|
|
87
|
+
for (const statement of roleStatements(spec)) {
|
|
79
88
|
if (statement.exists) {
|
|
80
89
|
const { rows } = await client.query(
|
|
81
90
|
statement.exists.sql,
|
package/src/sst/fromManifest.ts
CHANGED
|
@@ -26,10 +26,12 @@ import {
|
|
|
26
26
|
cookieDomain,
|
|
27
27
|
DEFAULT_POSTGRES_VERSION,
|
|
28
28
|
dependentsOf,
|
|
29
|
+
mobileOrigins,
|
|
29
30
|
PUBLIC,
|
|
30
31
|
PUBLIC_PREFIX,
|
|
31
32
|
providedKeyFor,
|
|
32
33
|
provisionOrder,
|
|
34
|
+
schemeBase,
|
|
33
35
|
} from '@geekmidas/manifest';
|
|
34
36
|
import {
|
|
35
37
|
Cache,
|
|
@@ -39,11 +41,15 @@ import {
|
|
|
39
41
|
withCacheTable,
|
|
40
42
|
} from './aws/Cache';
|
|
41
43
|
import { Credential } from './aws/Credential';
|
|
42
|
-
import {
|
|
44
|
+
import { Cron, type CronSchedule } from './aws/Cron';
|
|
45
|
+
import { Database, DatabaseNeedsVpc, type DatabaseProps } from './aws/Database';
|
|
43
46
|
import { DatabaseBootstrap } from './aws/DatabaseBootstrap';
|
|
44
47
|
import { DatabaseReader, DatabaseSchema } from './aws/DerivedDatabase';
|
|
45
48
|
import { Email, EmailNeedsSender } from './aws/Email';
|
|
49
|
+
import { Encryption, type EncryptionProps } from './aws/Encryption';
|
|
50
|
+
import { ExternalApi, type ExternalApiProps } from './aws/ExternalApi';
|
|
46
51
|
import { FileServer } from './aws/FileServer';
|
|
52
|
+
import { Function } from './aws/Function';
|
|
47
53
|
import { ObjectStorage } from './aws/ObjectStorage';
|
|
48
54
|
import { Queue } from './aws/Queue';
|
|
49
55
|
import { RestApiSurface } from './aws/RestApiSurface';
|
|
@@ -56,6 +62,7 @@ import {
|
|
|
56
62
|
UnresolvedDependency,
|
|
57
63
|
} from './errors';
|
|
58
64
|
import type { GkmLinkable } from './Linkable';
|
|
65
|
+
import type { ManifestBackends, ManifestOverrides } from './overrides';
|
|
59
66
|
import type { StackType } from './Stack';
|
|
60
67
|
|
|
61
68
|
/**
|
|
@@ -98,8 +105,8 @@ export interface ProvisionContext {
|
|
|
98
105
|
/**
|
|
99
106
|
* Where a declared cache lives, and who delivers mail.
|
|
100
107
|
*
|
|
101
|
-
* Deployment choices rather than declarations, for the same reason
|
|
102
|
-
*
|
|
108
|
+
* Deployment choices rather than declarations, for the same reason the
|
|
109
|
+
* event broker is: the same application code caches into any of them and
|
|
103
110
|
* sends through any of them. They reach the provisioner here because it is
|
|
104
111
|
* the only place that knows both the choice and the manifest.
|
|
105
112
|
*/
|
|
@@ -133,21 +140,6 @@ type Provisioner = (
|
|
|
133
140
|
context: ProvisionContext,
|
|
134
141
|
) => Provisioned;
|
|
135
142
|
|
|
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
143
|
/**
|
|
152
144
|
* Which component provisions which kind.
|
|
153
145
|
*
|
|
@@ -220,7 +212,11 @@ const PROVISIONERS: Partial<Record<DeclarationKind, Provisioner>> = {
|
|
|
220
212
|
// rather than whatever Aurora defaults to that month. Local ran 18
|
|
221
213
|
// while this ran 17.7, and neither was written down anywhere.
|
|
222
214
|
version: String(d.version ?? DEFAULT_POSTGRES_VERSION),
|
|
223
|
-
|
|
215
|
+
// `DatabaseProps`, not `sst.aws.PostgresArgs`: the database needs the
|
|
216
|
+
// `Vpc` component itself (its bootstrap function runs in the same VPC
|
|
217
|
+
// and needs the security groups too), where RDS would accept bare
|
|
218
|
+
// subnet ids. TS 7 checks the spread; 6 let the wider type through.
|
|
219
|
+
...(props as unknown as Omit<DatabaseProps, 'schema'>),
|
|
224
220
|
});
|
|
225
221
|
|
|
226
222
|
// A database needs its own roles, not just its tenants'. Registering only
|
|
@@ -232,13 +228,21 @@ const PROVISIONERS: Partial<Record<DeclarationKind, Provisioner>> = {
|
|
|
232
228
|
const bootstrap = bootstrapFor(cluster, d.id, context);
|
|
233
229
|
const runtime = roleNameFor(d, context);
|
|
234
230
|
|
|
235
|
-
bootstrap?.add({
|
|
231
|
+
const credentials = bootstrap?.add({
|
|
236
232
|
id: d.id,
|
|
237
233
|
schema: d.schema,
|
|
238
234
|
runtime,
|
|
239
235
|
owner: ownerRole(runtime),
|
|
236
|
+
ownsDatabase: true,
|
|
240
237
|
...(hasReader(d.id, context) ? { reader: readerRole(runtime) } : {}),
|
|
241
238
|
});
|
|
239
|
+
|
|
240
|
+
// Registering the role was half of it: its handlers have to connect as
|
|
241
|
+
// it too, or the DDL creates a role nothing uses and every handler
|
|
242
|
+
// linked to the database still holds the master.
|
|
243
|
+
if (credentials) {
|
|
244
|
+
cluster.connectAs({ user: runtime, password: credentials.runtime });
|
|
245
|
+
}
|
|
242
246
|
}
|
|
243
247
|
|
|
244
248
|
return cluster;
|
|
@@ -363,17 +367,12 @@ const PROVISIONERS: Partial<Record<DeclarationKind, Provisioner>> = {
|
|
|
363
367
|
},
|
|
364
368
|
|
|
365
369
|
/**
|
|
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.
|
|
370
|
+
* The surface, without its routes yet.
|
|
372
371
|
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
372
|
+
* Its address is what a site inlines as `VITE_API_URL`, what an auth server
|
|
373
|
+
* puts on its trusted-origin list, and what the cookie domain derives from —
|
|
374
|
+
* so it is provisioned in order, and its endpoints are mounted by
|
|
375
|
+
* {@link mountRoutes} once everything they link to exists.
|
|
377
376
|
*/
|
|
378
377
|
'rest-api': (stack, d, props, context) =>
|
|
379
378
|
new RestApiSurface(stack, d.id, {
|
|
@@ -385,6 +384,30 @@ const PROVISIONERS: Partial<Record<DeclarationKind, Provisioner>> = {
|
|
|
385
384
|
|
|
386
385
|
// The same storage as a secret, under a different role — see the component.
|
|
387
386
|
credential: (stack, d, props) => new Credential(stack, d.id, props),
|
|
387
|
+
|
|
388
|
+
// In the stage's region, as mail is: a key in another region is a key
|
|
389
|
+
// every call crosses the world to reach.
|
|
390
|
+
encryption: (stack, d, props) =>
|
|
391
|
+
new Encryption(stack, d.id, {
|
|
392
|
+
...(props as Partial<EncryptionProps>),
|
|
393
|
+
region:
|
|
394
|
+
(props as Partial<EncryptionProps>).region ??
|
|
395
|
+
$app.providers?.aws?.region ??
|
|
396
|
+
'us-east-1',
|
|
397
|
+
}),
|
|
398
|
+
|
|
399
|
+
// Nothing to provision: the URL this stage calls, and the credentials kept
|
|
400
|
+
// as a secret.
|
|
401
|
+
'external-api': (stack, d, props) => {
|
|
402
|
+
if (d.kind !== 'external-api') {
|
|
403
|
+
throw new UnknownDeclarationKind(d.kind, []);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return new ExternalApi(stack, d.id, {
|
|
407
|
+
...(props as Partial<ExternalApiProps>),
|
|
408
|
+
url: d.url,
|
|
409
|
+
});
|
|
410
|
+
},
|
|
388
411
|
};
|
|
389
412
|
|
|
390
413
|
/**
|
|
@@ -565,9 +588,7 @@ function rootCluster(component: Provisioned): Database {
|
|
|
565
588
|
* printed: one line per route, naming what it can reach.
|
|
566
589
|
*
|
|
567
590
|
* 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.
|
|
591
|
+
* routes" and "this printed nothing" look identical otherwise.
|
|
571
592
|
*
|
|
572
593
|
* Pure, so what gets printed can be asserted without a deploy.
|
|
573
594
|
*/
|
|
@@ -636,6 +657,8 @@ async function callersOf(
|
|
|
636
657
|
id: string,
|
|
637
658
|
manifest: ConstructManifest,
|
|
638
659
|
provisioned: ProvisionedManifest,
|
|
660
|
+
/** The app's name — a mobile caller's scheme base when it gives none. */
|
|
661
|
+
project: string,
|
|
639
662
|
): Promise<{ trustedOrigins: string; cookieDomain?: string }> {
|
|
640
663
|
// Every address here is a Pulumi output, not a string — a CloudFront domain
|
|
641
664
|
// and an API Gateway endpoint are both known only after their resource
|
|
@@ -660,8 +683,17 @@ async function callersOf(
|
|
|
660
683
|
const own = await resolved(provisioned[id]?.provides().url);
|
|
661
684
|
const domain = cookieDomain([...(own ? [own] : []), ...origins]);
|
|
662
685
|
|
|
686
|
+
// A mobile caller is reached by its scheme — the bare one, deployed: what
|
|
687
|
+
// the store build registers.
|
|
688
|
+
const schemes = dependentsOf(manifest, id).flatMap((caller) => {
|
|
689
|
+
const app = manifest[caller];
|
|
690
|
+
return app?.kind === 'mobile-app'
|
|
691
|
+
? mobileOrigins(schemeBase(project, app.scheme))
|
|
692
|
+
: [];
|
|
693
|
+
});
|
|
694
|
+
|
|
663
695
|
return {
|
|
664
|
-
trustedOrigins: origins.join(','),
|
|
696
|
+
trustedOrigins: [...origins, ...schemes].join(','),
|
|
665
697
|
...(domain ? { cookieDomain: domain } : {}),
|
|
666
698
|
};
|
|
667
699
|
}
|
|
@@ -711,6 +743,170 @@ export function isServed(id: string, manifest: ConstructManifest): boolean {
|
|
|
711
743
|
);
|
|
712
744
|
}
|
|
713
745
|
|
|
746
|
+
/**
|
|
747
|
+
* Kinds the in-order pass skips.
|
|
748
|
+
*
|
|
749
|
+
* - `mobile-app` ships through EAS and the stores. It is in the manifest for
|
|
750
|
+
* what it makes the surfaces trust, which `callersOf` reads off the graph.
|
|
751
|
+
* - `worker` is a process, not a resource: what it runs is provisioned as
|
|
752
|
+
* what it is — a queue's consumer by {@link subscribeConsumers}.
|
|
753
|
+
* - `cron` and `function` are Lambdas linked to what they depend on, so they
|
|
754
|
+
* are provisioned once all of it exists, by {@link provisionCompute}.
|
|
755
|
+
* - `telemetry` provisions nothing: where it goes is the stage's
|
|
756
|
+
* `deploy.telemetry`, a provider somebody else runs.
|
|
757
|
+
*/
|
|
758
|
+
export const PROVISIONED_ELSEWHERE: ReadonlySet<DeclarationKind> =
|
|
759
|
+
new Set<DeclarationKind>([
|
|
760
|
+
'mobile-app',
|
|
761
|
+
'worker',
|
|
762
|
+
'cron',
|
|
763
|
+
'function',
|
|
764
|
+
'telemetry',
|
|
765
|
+
]);
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Every function and cron: a Lambda running the handler the build wrote,
|
|
769
|
+
* linked to exactly what it depends on, in the database's network when it
|
|
770
|
+
* reaches one. A cron is that Lambda on its schedule; a function is reached
|
|
771
|
+
* at its URL, which only callers granted IAM access can invoke.
|
|
772
|
+
*
|
|
773
|
+
* After everything else is provisioned, because each links to things
|
|
774
|
+
* `provisionOrder` does not order before it.
|
|
775
|
+
*/
|
|
776
|
+
export function provisionCompute(
|
|
777
|
+
stack: StackType,
|
|
778
|
+
manifest: ConstructManifest,
|
|
779
|
+
provisioned: ProvisionedManifest,
|
|
780
|
+
): Cron[] {
|
|
781
|
+
const crons: Cron[] = [];
|
|
782
|
+
|
|
783
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
784
|
+
if (declaration?.kind !== 'function' && declaration?.kind !== 'cron') {
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const { link } = resolveEdges(declaration.dependencies, provisioned);
|
|
789
|
+
const vpc = vpcFor(link);
|
|
790
|
+
const lambda = new Function(
|
|
791
|
+
stack,
|
|
792
|
+
declaration.kind === 'cron' ? `${id}Function` : id,
|
|
793
|
+
{
|
|
794
|
+
name: stack.logicalPrefixedName(id),
|
|
795
|
+
handler: declaration.handler,
|
|
796
|
+
link,
|
|
797
|
+
...(vpc ? { vpc } : {}),
|
|
798
|
+
...(declaration.kind === 'function'
|
|
799
|
+
? { url: { authorization: 'iam' } }
|
|
800
|
+
: {}),
|
|
801
|
+
},
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
if (declaration.kind === 'cron') {
|
|
805
|
+
crons.push(
|
|
806
|
+
new Cron(stack, id, {
|
|
807
|
+
processor: lambda,
|
|
808
|
+
schedule: declaration.schedule as CronSchedule,
|
|
809
|
+
}),
|
|
810
|
+
);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
assertProvides(
|
|
815
|
+
id,
|
|
816
|
+
declaration.provides,
|
|
817
|
+
Object.keys(lambda.provides()).map((role) =>
|
|
818
|
+
providedKeyFor(id, declaration.kind, role),
|
|
819
|
+
),
|
|
820
|
+
);
|
|
821
|
+
provisioned[id] = lambda;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return crons;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/** A provisioned queue: what {@link subscribeConsumers} subscribes through. */
|
|
828
|
+
interface Consumable {
|
|
829
|
+
consume(consumer: {
|
|
830
|
+
handler: string;
|
|
831
|
+
link?: unknown[];
|
|
832
|
+
vpc?: sst.aws.Vpc;
|
|
833
|
+
}): unknown;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* The network a function has to run in to reach what it links to.
|
|
838
|
+
*
|
|
839
|
+
* A database is private to its VPC, so a handler linked to one — or to a
|
|
840
|
+
* schema tenant or reader of one — runs inside it, or its connection times out.
|
|
841
|
+
*/
|
|
842
|
+
export function vpcFor(link: readonly Provisioned[]): sst.aws.Vpc | undefined {
|
|
843
|
+
for (const component of link) {
|
|
844
|
+
const cluster = rootCluster(component);
|
|
845
|
+
if (cluster instanceof Database) return cluster.vpc;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return undefined;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Mount every surface's endpoints — the handlers the build wrote, each linked
|
|
853
|
+
* to exactly what that endpoint depends on.
|
|
854
|
+
*
|
|
855
|
+
* After every construct is provisioned, for the reason consumers are: an
|
|
856
|
+
* endpoint links to things `provisionOrder` does not order before its surface.
|
|
857
|
+
*/
|
|
858
|
+
export function mountRoutes(
|
|
859
|
+
manifest: ConstructManifest,
|
|
860
|
+
provisioned: ProvisionedManifest,
|
|
861
|
+
): void {
|
|
862
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
863
|
+
if (declaration?.kind !== 'rest-api') continue;
|
|
864
|
+
|
|
865
|
+
const surface = provisioned[id];
|
|
866
|
+
if (!(surface instanceof RestApiSurface)) continue;
|
|
867
|
+
|
|
868
|
+
for (const endpoint of declaration.endpoints) {
|
|
869
|
+
const { link } = resolveEdges(endpoint.dependencies, provisioned);
|
|
870
|
+
const vpc = vpcFor(link);
|
|
871
|
+
surface.mount({
|
|
872
|
+
method: endpoint.method,
|
|
873
|
+
path: endpoint.path,
|
|
874
|
+
handler: endpoint.handler,
|
|
875
|
+
link,
|
|
876
|
+
...(vpc ? { vpc } : {}),
|
|
877
|
+
...(endpoint.authorizer ? { authorizer: endpoint.authorizer } : {}),
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Subscribe every queue's one consumer — the handler the build wrote for it,
|
|
885
|
+
* linked to what that consumer depends on.
|
|
886
|
+
*
|
|
887
|
+
* After every construct is provisioned, because a consumer links to things
|
|
888
|
+
* that `provisionOrder` does not order before the queue.
|
|
889
|
+
*/
|
|
890
|
+
export function subscribeConsumers(
|
|
891
|
+
manifest: ConstructManifest,
|
|
892
|
+
provisioned: ProvisionedManifest,
|
|
893
|
+
): void {
|
|
894
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
895
|
+
if (declaration?.kind !== 'queue') continue;
|
|
896
|
+
|
|
897
|
+
const queue = provisioned[id] as unknown as Consumable | undefined;
|
|
898
|
+
if (!queue) continue;
|
|
899
|
+
|
|
900
|
+
const { link } = resolveEdges(declaration.worker.dependencies, provisioned);
|
|
901
|
+
const vpc = vpcFor(link);
|
|
902
|
+
queue.consume({
|
|
903
|
+
handler: declaration.worker.handler,
|
|
904
|
+
link,
|
|
905
|
+
...(vpc ? { vpc } : {}),
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
714
910
|
/** The provisioner for a kind. Pure — the lookup is testable without Pulumi. */
|
|
715
911
|
export function provisionerFor(kind: DeclarationKind): Provisioner {
|
|
716
912
|
const provisioner = PROVISIONERS[kind];
|
|
@@ -749,20 +945,34 @@ export function assertProvides(
|
|
|
749
945
|
* tenant, a read replica, and the surface over a bucket all resolve an address
|
|
750
946
|
* off something else, and a pass in map order would find it half the time.
|
|
751
947
|
*/
|
|
752
|
-
export function fromManifest
|
|
948
|
+
export function fromManifest<
|
|
949
|
+
const M extends ConstructManifest,
|
|
950
|
+
const B extends ManifestBackends = {},
|
|
951
|
+
>(
|
|
753
952
|
stack: StackType,
|
|
754
|
-
manifest:
|
|
755
|
-
|
|
953
|
+
manifest: M,
|
|
954
|
+
/**
|
|
955
|
+
* Provider-specific props, per construct id — typed from the manifest, so
|
|
956
|
+
* only its ids are keys, each takes what its kind's component reads, and
|
|
957
|
+
* what the synth cannot guess (a database's `vpc`, mail's `from`) is
|
|
958
|
+
* required. See {@link ManifestOverrides}.
|
|
959
|
+
*
|
|
960
|
+
* Neutral options — `versioned`, `fifo` — travel in the declaration,
|
|
961
|
+
* because the app legitimately has an opinion about them. Anything with S3
|
|
962
|
+
* in its name does not belong in application code, so it is supplied here.
|
|
963
|
+
* A third escape hatch needs no API: the components are returned, so
|
|
964
|
+
* `provisioned.Uploads.nodes.bucket` is reachable for anything else.
|
|
965
|
+
*/
|
|
966
|
+
overrides: ManifestOverrides<M, B>,
|
|
756
967
|
/**
|
|
757
968
|
* The backend choices that are config rather than declaration.
|
|
758
969
|
*
|
|
759
970
|
* Defaulted the same way the local target defaults them, so a stage deployed
|
|
760
|
-
* without saying gets the same backend a developer ran against.
|
|
971
|
+
* without saying gets the same backend a developer ran against. Typed
|
|
972
|
+
* literally, because the backend decides what is required above: an
|
|
973
|
+
* ElastiCache cache needs a `vpc`, Resend or SMTP mail a `url`.
|
|
761
974
|
*/
|
|
762
|
-
backends: {
|
|
763
|
-
cache?: 'upstash' | 'elasticache' | 'db';
|
|
764
|
-
email?: 'resend' | 'ses' | 'smtp';
|
|
765
|
-
} = {},
|
|
975
|
+
backends: B = {} as B,
|
|
766
976
|
): ProvisionedManifest {
|
|
767
977
|
const provisioned: ProvisionedManifest = {};
|
|
768
978
|
|
|
@@ -799,11 +1009,12 @@ export function fromManifest(
|
|
|
799
1009
|
for (const id of [...rest, ...sites]) {
|
|
800
1010
|
const declaration = manifest[id];
|
|
801
1011
|
if (!declaration) continue;
|
|
1012
|
+
if (PROVISIONED_ELSEWHERE.has(declaration.kind)) continue;
|
|
802
1013
|
|
|
803
1014
|
const component = provisionerFor(declaration.kind)(
|
|
804
1015
|
stack,
|
|
805
1016
|
declaration,
|
|
806
|
-
overrides[id] ?? {},
|
|
1017
|
+
(overrides as Record<string, Record<string, unknown>>)[id] ?? {},
|
|
807
1018
|
context,
|
|
808
1019
|
);
|
|
809
1020
|
|
|
@@ -822,6 +1033,12 @@ export function fromManifest(
|
|
|
822
1033
|
provisioned[id] = component;
|
|
823
1034
|
}
|
|
824
1035
|
|
|
1036
|
+
// The functions and crons, each queue's one consumer and each surface's
|
|
1037
|
+
// endpoints, once everything they link to exists.
|
|
1038
|
+
provisionCompute(stack, manifest, provisioned);
|
|
1039
|
+
subscribeConsumers(manifest, provisioned);
|
|
1040
|
+
mountRoutes(manifest, provisioned);
|
|
1041
|
+
|
|
825
1042
|
for (const line of describeRoutes(manifest)) console.log(line);
|
|
826
1043
|
|
|
827
1044
|
// Now everything exists, so every surface's callers can be resolved — the
|
|
@@ -829,7 +1046,7 @@ export function fromManifest(
|
|
|
829
1046
|
for (const [id, deferred] of callers) {
|
|
830
1047
|
// Resolved with the promise itself: the addresses it needs are Pulumi
|
|
831
1048
|
// outputs, so the answer arrives when they do.
|
|
832
|
-
deferred.resolve(callersOf(id, manifest, provisioned));
|
|
1049
|
+
deferred.resolve(callersOf(id, manifest, provisioned, $app.name));
|
|
833
1050
|
}
|
|
834
1051
|
|
|
835
1052
|
// Last, and only now: the roles exist as passwords and secrets from the
|
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';
|