@geekmidas/cloud 10.0.0-alpha.5 → 10.0.0-alpha.50
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 +7 -7
- 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/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 +229 -20
- package/src/sst/index.ts +1 -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
|
@@ -28,6 +28,11 @@ const manifest = {
|
|
|
28
28
|
provides: ['API_URL', 'API_TRUSTED_ORIGINS', 'API_COOKIE_DOMAIN'],
|
|
29
29
|
},
|
|
30
30
|
Orders: { kind: 'database', id: 'Orders', provides: ['ORDERS_URL'] },
|
|
31
|
+
Mailer: {
|
|
32
|
+
kind: 'email',
|
|
33
|
+
id: 'Mailer',
|
|
34
|
+
provides: ['MAILER_URL', 'MAILER_FROM'],
|
|
35
|
+
},
|
|
31
36
|
Console: {
|
|
32
37
|
kind: 'site',
|
|
33
38
|
id: 'Console',
|
|
@@ -37,6 +42,7 @@ const manifest = {
|
|
|
37
42
|
{ target: 'Api', kind: 'rest-api' },
|
|
38
43
|
{ target: 'Orders', kind: 'database' },
|
|
39
44
|
{ target: 'UploadsServer', kind: 'file-server' },
|
|
45
|
+
{ target: 'Mailer', kind: 'email' },
|
|
40
46
|
],
|
|
41
47
|
provides: ['CONSOLE_URL'],
|
|
42
48
|
},
|
|
@@ -101,6 +107,10 @@ describe('siteEnvironment', () => {
|
|
|
101
107
|
Api: provided({ url: 'https://api.example.com' }),
|
|
102
108
|
Orders: provided({ url: 'postgres://user:pw@db/orders' }),
|
|
103
109
|
UploadsServer: provided({ url: 'https://files.example.com' }),
|
|
110
|
+
Mailer: provided({
|
|
111
|
+
url: 'smtp://user:pw@email-smtp.example.com:587',
|
|
112
|
+
from: 'hello@shop.test',
|
|
113
|
+
}),
|
|
104
114
|
}),
|
|
105
115
|
);
|
|
106
116
|
|
|
@@ -122,6 +132,13 @@ describe('siteEnvironment', () => {
|
|
|
122
132
|
expect(built()).not.toHaveProperty('VITE_ORDERS_URL');
|
|
123
133
|
});
|
|
124
134
|
|
|
135
|
+
it('has no inbox to share for deployed mail, and never shares the mail URL', () => {
|
|
136
|
+
// The inbox is Mailpit's, and exists only locally: deployed, an app that
|
|
137
|
+
// reads it falls back to the phone's own mail app.
|
|
138
|
+
expect(built()).not.toHaveProperty('VITE_MAILER_INBOX_URL');
|
|
139
|
+
expect(built()).not.toHaveProperty('VITE_MAILER_URL');
|
|
140
|
+
});
|
|
141
|
+
|
|
125
142
|
it('fails loudly on an edge nothing provisioned', () => {
|
|
126
143
|
// A smaller environment builds and then fails at runtime against
|
|
127
144
|
// `http:///`, with nothing to point at.
|
package/src/sst/aws/Api.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { EnvValidationError } from '@geekmidas/envkit/sst';
|
|
3
|
-
import {
|
|
4
|
-
flattenManifestField,
|
|
5
|
-
type ManifestField,
|
|
6
|
-
type RouteInfo,
|
|
7
|
-
} from '@geekmidas/manifest';
|
|
8
3
|
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
9
4
|
import { LinkedEnvironment } from '../LinkedEnvironment';
|
|
10
5
|
import type { StackType } from '../Stack';
|
|
@@ -153,48 +148,10 @@ export class Api<
|
|
|
153
148
|
// Fail the whole synth if any route is misconfigured, with one actionable
|
|
154
149
|
// message per offending route.
|
|
155
150
|
if (failures.length) {
|
|
156
|
-
throw new
|
|
151
|
+
throw new RoutesMissingEnvironment(id, failures);
|
|
157
152
|
}
|
|
158
153
|
}
|
|
159
154
|
|
|
160
|
-
/**
|
|
161
|
-
* Build an `Api` from a `gkm build` manifest's `routes` field (flat or
|
|
162
|
-
* partitioned): each `RouteInfo` becomes a route (env vars, authorizer,
|
|
163
|
-
* timeout/memory mapped). Supply `authorizers` (JWT/Lambda settings),
|
|
164
|
-
* `links`, and any native args via `props`.
|
|
165
|
-
*
|
|
166
|
-
* ```ts
|
|
167
|
-
* import { manifest } from './.gkm/manifest/aws';
|
|
168
|
-
* Api.fromManifest(stack, 'Api', manifest.routes, { links: [db] });
|
|
169
|
-
* ```
|
|
170
|
-
*/
|
|
171
|
-
static fromManifest<
|
|
172
|
-
TAuthorizers extends Record<string, unknown> = {},
|
|
173
|
-
TStage extends string = string,
|
|
174
|
-
TDomain extends string = string,
|
|
175
|
-
>(
|
|
176
|
-
stack: StackType<TStage, TDomain>,
|
|
177
|
-
id: string,
|
|
178
|
-
routes: ManifestField<RouteInfo>,
|
|
179
|
-
props: Omit<ApiProps<TAuthorizers>, 'routes'> = {},
|
|
180
|
-
): Api<TAuthorizers, TStage, TDomain> {
|
|
181
|
-
const routeTable = flattenManifestField(routes).map(
|
|
182
|
-
(route): Route<AuthorizerName<TAuthorizers>> => ({
|
|
183
|
-
method: route.method as Route['method'],
|
|
184
|
-
path: route.path,
|
|
185
|
-
handler: route.handler,
|
|
186
|
-
environment: route.environment,
|
|
187
|
-
authorizer: route.authorizer as AuthorizerName<TAuthorizers>,
|
|
188
|
-
timeout: route.timeout ? `${route.timeout} seconds` : undefined,
|
|
189
|
-
memory: route.memorySize ? `${route.memorySize} MB` : undefined,
|
|
190
|
-
}),
|
|
191
|
-
);
|
|
192
|
-
return new Api(stack, id, {
|
|
193
|
-
...props,
|
|
194
|
-
routes: routeTable,
|
|
195
|
-
} as ApiProps<TAuthorizers>);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
155
|
/** Resolves a route's `authorizer` name to the SST `auth` option. */
|
|
199
156
|
private static buildRouteAuth(
|
|
200
157
|
route: Route<string>,
|
|
@@ -307,3 +264,18 @@ export interface ApiProps<TAuthorizers extends Record<string, unknown> = {}>
|
|
|
307
264
|
* may override it. */
|
|
308
265
|
runtime?: sst.aws.FunctionArgs['runtime'];
|
|
309
266
|
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Routes that read variables nothing links or whitelists. Collected across
|
|
270
|
+
* every route so one synth reports all of them, each with its own suggestions,
|
|
271
|
+
* rather than stopping at the first.
|
|
272
|
+
*/
|
|
273
|
+
export class RoutesMissingEnvironment extends Error {
|
|
274
|
+
constructor(
|
|
275
|
+
readonly api: string,
|
|
276
|
+
readonly failures: readonly EnvValidationError[],
|
|
277
|
+
) {
|
|
278
|
+
super(failures.map((f) => f.message).join('\n\n'));
|
|
279
|
+
this.name = 'RoutesMissingEnvironment';
|
|
280
|
+
}
|
|
281
|
+
}
|
package/src/sst/aws/Cache.ts
CHANGED
|
@@ -206,7 +206,7 @@ export function withCacheTable(url: string, table: string): string {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
/**
|
|
209
|
-
* `
|
|
209
|
+
* A `'db'` cache in an app that declares more than one database.
|
|
210
210
|
*
|
|
211
211
|
* Picking one would put the cache somewhere nobody chose, and the symptom —
|
|
212
212
|
* entries that are never found — appears long after the deploy reported
|
|
@@ -222,7 +222,7 @@ export class CacheIsAmbiguous extends Error {
|
|
|
222
222
|
`'${id}' is a cache backed by the database, and this app declares ` +
|
|
223
223
|
`${databases.length}: ${databases.join(', ')}. Declare the cache from ` +
|
|
224
224
|
`its database — e.g. ${databases[0]}.cache('${id}') — rather than ` +
|
|
225
|
-
`
|
|
225
|
+
`as a bare new Cache().`,
|
|
226
226
|
);
|
|
227
227
|
this.name = 'CacheIsAmbiguous';
|
|
228
228
|
}
|
|
@@ -232,8 +232,8 @@ export class CacheNeedsDatabase extends Error {
|
|
|
232
232
|
constructor(readonly id: string) {
|
|
233
233
|
super(
|
|
234
234
|
`'${id}' is a cache backed by the database, and this app declares no ` +
|
|
235
|
-
`database for it to live in. Declare one, or
|
|
236
|
-
`
|
|
235
|
+
`database for it to live in. Declare one, or declare the cache from ` +
|
|
236
|
+
`the database it belongs to.`,
|
|
237
237
|
);
|
|
238
238
|
this.name = 'CacheNeedsDatabase';
|
|
239
239
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
2
|
+
import type { StackType } from '../Stack';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* `Credential` — a third-party credential SST holds, and the infra half of the
|
|
@@ -8,21 +9,53 @@ import { Secret, type SecretProps } from './Secret';
|
|
|
8
9
|
* the lifecycle rather than the mechanism: a secret is generated and rotated by
|
|
9
10
|
* the platform, while a credential is issued by someone else and has a shape
|
|
10
11
|
* the construct validates on the way in. Both are values you set out of band
|
|
11
|
-
* with `sst secret set
|
|
12
|
+
* with `sst secret set`, under the construct's id.
|
|
12
13
|
*
|
|
13
|
-
* The role is `
|
|
14
|
+
* The role is `credentials` rather than `value`, and that is not cosmetic. The
|
|
14
15
|
* role *is* the contract — `providedKeyFor` turns it into the key the app
|
|
15
16
|
* declared — so a credential providing `value` would supply `STRIPE_VALUE`
|
|
16
|
-
* against a declared `
|
|
17
|
-
* stack at synth.
|
|
17
|
+
* against a declared `STRIPE_CREDENTIALS`, and `assertProvides` would reject
|
|
18
|
+
* the stack at synth.
|
|
19
|
+
*
|
|
20
|
+
* It holds an `sst.Secret` rather than being one, and reports its own type,
|
|
21
|
+
* for the same reason one level down: the type decides the keys a link
|
|
22
|
+
* resolves to. As an SST secret it resolved to `STRIPE`, so a function that
|
|
23
|
+
* declared `STRIPE_CREDENTIALS` was linked to nothing.
|
|
18
24
|
*/
|
|
19
25
|
export class Credential<
|
|
20
26
|
TStage extends string = string,
|
|
21
27
|
TDomain extends string = string,
|
|
22
|
-
>
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
> implements GkmLinkable
|
|
29
|
+
{
|
|
30
|
+
readonly _id: string;
|
|
31
|
+
|
|
32
|
+
private readonly secret: sst.Secret;
|
|
33
|
+
|
|
34
|
+
get _type() {
|
|
35
|
+
return ResourceType.Credential;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
_stack: StackType<TStage, TDomain>,
|
|
40
|
+
name: string,
|
|
41
|
+
props: CredentialProps = {},
|
|
42
|
+
) {
|
|
43
|
+
this._id = name;
|
|
44
|
+
// The id itself, as it always was: a value already set with
|
|
45
|
+
// `sst secret set Stripe …` is still the one read.
|
|
46
|
+
this.secret = new sst.Secret(name, props.placeholder);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
provides(): Record<string, $util.Input<string>> {
|
|
50
|
+
return { credentials: this.secret.value };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getSSTLink() {
|
|
54
|
+
return { properties: { ...this.provides() } };
|
|
25
55
|
}
|
|
26
56
|
}
|
|
27
57
|
|
|
28
|
-
export interface CredentialProps
|
|
58
|
+
export interface CredentialProps {
|
|
59
|
+
/** A value until one is set, for a preview stage. */
|
|
60
|
+
placeholder?: string;
|
|
61
|
+
}
|
package/src/sst/aws/Cron.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type CronInfo,
|
|
3
|
-
flattenManifestField,
|
|
4
|
-
type ManifestField,
|
|
5
|
-
} from '@geekmidas/manifest';
|
|
6
|
-
import type { GkmLinkable } from '../Linkable';
|
|
7
1
|
import type { StackType } from '../Stack';
|
|
8
|
-
import { Function } from './Function';
|
|
2
|
+
import type { Function } from './Function';
|
|
9
3
|
|
|
10
4
|
export type CronExpressionValue = number | '*' | '?' | `${number}/${number}`;
|
|
11
5
|
export type CronExpressionDay =
|
|
@@ -52,6 +46,9 @@ export class Cron<
|
|
|
52
46
|
TStage extends string = string,
|
|
53
47
|
TDomain extends string = string,
|
|
54
48
|
> extends sst.aws.CronV2 {
|
|
49
|
+
/** The function it invokes. */
|
|
50
|
+
readonly processor: CronProps['processor'];
|
|
51
|
+
|
|
55
52
|
constructor(
|
|
56
53
|
_stack: StackType<TStage, TDomain>,
|
|
57
54
|
name: string,
|
|
@@ -63,44 +60,7 @@ export class Cron<
|
|
|
63
60
|
schedule,
|
|
64
61
|
function: processor.arn,
|
|
65
62
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Build one `Cron` per entry in a `gkm build` manifest's `crons` field (flat
|
|
70
|
-
* or partitioned). Each cron's handler becomes a validated `Function` (the
|
|
71
|
-
* cron's target), so pass `links` for that function's env validation;
|
|
72
|
-
* remaining `props` are CronV2 args.
|
|
73
|
-
*
|
|
74
|
-
* ```ts
|
|
75
|
-
* Cron.fromManifest(stack, manifest.crons, { links: [db] });
|
|
76
|
-
* ```
|
|
77
|
-
*/
|
|
78
|
-
static fromManifest<
|
|
79
|
-
TStage extends string = string,
|
|
80
|
-
TDomain extends string = string,
|
|
81
|
-
>(
|
|
82
|
-
stack: StackType<TStage, TDomain>,
|
|
83
|
-
crons: ManifestField<CronInfo>,
|
|
84
|
-
props: Omit<CronProps, 'processor' | 'schedule'> & {
|
|
85
|
-
links?: GkmLinkable[];
|
|
86
|
-
} = {},
|
|
87
|
-
): Cron<TStage, TDomain>[] {
|
|
88
|
-
const { links, ...cronArgs } = props;
|
|
89
|
-
return flattenManifestField(crons).map((cron) => {
|
|
90
|
-
const processor = new Function(stack, `${cron.name}Function`, {
|
|
91
|
-
name: stack.logicalPrefixedName(cron.name),
|
|
92
|
-
handler: cron.handler,
|
|
93
|
-
envVars: cron.environment,
|
|
94
|
-
links,
|
|
95
|
-
timeout: cron.timeout ? `${cron.timeout} seconds` : undefined,
|
|
96
|
-
memory: cron.memorySize ? `${cron.memorySize} MB` : undefined,
|
|
97
|
-
});
|
|
98
|
-
return new Cron(stack, cron.name, {
|
|
99
|
-
...cronArgs,
|
|
100
|
-
processor,
|
|
101
|
-
schedule: cron.schedule as CronSchedule,
|
|
102
|
-
});
|
|
103
|
-
});
|
|
63
|
+
this.processor = processor;
|
|
104
64
|
}
|
|
105
65
|
}
|
|
106
66
|
|
package/src/sst/aws/Database.ts
CHANGED
|
@@ -84,7 +84,21 @@ export class Database<
|
|
|
84
84
|
* produces a database that looks empty.
|
|
85
85
|
*/
|
|
86
86
|
provides(): Record<string, $util.Input<string>> {
|
|
87
|
-
return { url: this.urlFor({}) };
|
|
87
|
+
return { url: this.urlFor(this.role ? { as: this.role } : {}) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The runtime role this database's own handlers connect as.
|
|
92
|
+
*
|
|
93
|
+
* Set by the adapter once the bootstrap has registered the role — the
|
|
94
|
+
* cluster exists before any role does, so it cannot be a constructor
|
|
95
|
+
* argument. Until it is set (and under `roles: false`, where it never is),
|
|
96
|
+
* the URL is the master's.
|
|
97
|
+
*/
|
|
98
|
+
private role: { user: string; password: $util.Input<string> } | undefined;
|
|
99
|
+
|
|
100
|
+
connectAs(role: { user: string; password: $util.Input<string> }): void {
|
|
101
|
+
this.role = role;
|
|
88
102
|
}
|
|
89
103
|
|
|
90
104
|
/**
|
|
@@ -65,6 +65,8 @@ export class DatabaseBootstrap {
|
|
|
65
65
|
runtime: string;
|
|
66
66
|
owner: string;
|
|
67
67
|
reader?: string;
|
|
68
|
+
/** The database's own roles, whose owner may create in it. */
|
|
69
|
+
ownsDatabase?: boolean;
|
|
68
70
|
}): { runtime: $util.Output<string>; reader?: $util.Output<string> } {
|
|
69
71
|
const runtime = this.password(tenant.runtime);
|
|
70
72
|
const reader = tenant.reader ? this.password(tenant.reader) : undefined;
|
|
@@ -75,6 +77,7 @@ export class DatabaseBootstrap {
|
|
|
75
77
|
runtime: tenant.runtime,
|
|
76
78
|
owner: tenant.owner,
|
|
77
79
|
...(tenant.reader ? { reader: tenant.reader } : {}),
|
|
80
|
+
...(tenant.ownsDatabase ? { ownsDatabase: true } : {}),
|
|
78
81
|
// Filled in by `bootstrapEvent` once the outputs resolve. The function
|
|
79
82
|
// reads them straight out of its input and never fetches a secret, so
|
|
80
83
|
// it needs no IAM to read one.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { kmsUrl } from '@geekmidas/manifest';
|
|
2
|
+
import { kms } from '@pulumi/aws';
|
|
3
|
+
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
4
|
+
import type { StackType } from '../Stack';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `Encryption` — two KMS keys, and the infra half of the `encryption` kind.
|
|
8
|
+
*
|
|
9
|
+
* One key encrypts, by envelope: each value is sealed with a data key KMS
|
|
10
|
+
* mints and wraps, so the key itself never leaves KMS. It rotates yearly, and
|
|
11
|
+
* KMS keeps every version it rotated through, so nothing written is ever
|
|
12
|
+
* stranded. The other is an HMAC key for the blind index, which does not
|
|
13
|
+
* rotate — a rotated index key would make every stored index miss.
|
|
14
|
+
*
|
|
15
|
+
* Both are reached through one `kms://` URL, and a function linked to this is
|
|
16
|
+
* granted exactly the calls the cipher makes on exactly these two keys. Not
|
|
17
|
+
* `kms:*`, and not any other key in the account.
|
|
18
|
+
*
|
|
19
|
+
* It holds the keys rather than being one, and reports its own type, so a link
|
|
20
|
+
* resolves to `<ID>_URL` — the key the construct declared.
|
|
21
|
+
*/
|
|
22
|
+
export class Encryption<
|
|
23
|
+
TStage extends string = string,
|
|
24
|
+
TDomain extends string = string,
|
|
25
|
+
> implements GkmLinkable
|
|
26
|
+
{
|
|
27
|
+
readonly _id: string;
|
|
28
|
+
|
|
29
|
+
readonly key: kms.Key;
|
|
30
|
+
readonly indexKey: kms.Key;
|
|
31
|
+
private readonly region: string;
|
|
32
|
+
|
|
33
|
+
get _type() {
|
|
34
|
+
return ResourceType.Encryption;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
_stack: StackType<TStage, TDomain>,
|
|
39
|
+
name: string,
|
|
40
|
+
props: EncryptionProps,
|
|
41
|
+
) {
|
|
42
|
+
this._id = name;
|
|
43
|
+
this.region = props.region;
|
|
44
|
+
|
|
45
|
+
this.key = new kms.Key(`${name}Key`, {
|
|
46
|
+
description: `${name}: encrypts what the application stores`,
|
|
47
|
+
enableKeyRotation: true,
|
|
48
|
+
deletionWindowInDays: props.deletionWindowInDays ?? 30,
|
|
49
|
+
});
|
|
50
|
+
this.indexKey = new kms.Key(`${name}IndexKey`, {
|
|
51
|
+
description: `${name}: the blind index's HMAC key`,
|
|
52
|
+
keyUsage: 'GENERATE_VERIFY_MAC',
|
|
53
|
+
customerMasterKeySpec: 'HMAC_256',
|
|
54
|
+
deletionWindowInDays: props.deletionWindowInDays ?? 30,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
provides(): Record<string, $util.Input<string>> {
|
|
59
|
+
return {
|
|
60
|
+
url: $util
|
|
61
|
+
.all([this.key.arn, this.indexKey.arn])
|
|
62
|
+
.apply(([key, index]) => kmsUrl({ region: this.region, key, index })),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getSSTLink() {
|
|
67
|
+
return {
|
|
68
|
+
properties: { ...this.provides() },
|
|
69
|
+
include: [
|
|
70
|
+
sst.aws.permission({
|
|
71
|
+
actions: ['kms:GenerateDataKey', 'kms:Decrypt'],
|
|
72
|
+
resources: [this.key.arn],
|
|
73
|
+
}),
|
|
74
|
+
sst.aws.permission({
|
|
75
|
+
actions: ['kms:GenerateMac'],
|
|
76
|
+
resources: [this.indexKey.arn],
|
|
77
|
+
}),
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface EncryptionProps {
|
|
84
|
+
/** Where the keys live — the stage's region. */
|
|
85
|
+
region: string;
|
|
86
|
+
/**
|
|
87
|
+
* How long a deleted key can still be recovered, 7–30 days. A key's
|
|
88
|
+
* deletion is the deletion of everything it encrypted, so the longest.
|
|
89
|
+
*/
|
|
90
|
+
deletionWindowInDays?: number;
|
|
91
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { externalApiUrl } from '@geekmidas/manifest';
|
|
2
|
+
import { type GkmLinkable, ResourceType } from '../Linkable';
|
|
3
|
+
import type { StackType } from '../Stack';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `ExternalApi` — the infra half of the `external-api` kind.
|
|
7
|
+
*
|
|
8
|
+
* Nothing is provisioned: the API is somebody else's. What this holds is the
|
|
9
|
+
* two facts a function needs to call it on this stage — the URL the
|
|
10
|
+
* declaration names for it, and the credentials the provider issued, kept as
|
|
11
|
+
* an SST secret and set out of band:
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* sst secret set PolarCredentials '{"clientId":"…","clientSecret":"…"}' --stage prod
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export class ExternalApi<
|
|
18
|
+
TStage extends string = string,
|
|
19
|
+
TDomain extends string = string,
|
|
20
|
+
> implements GkmLinkable
|
|
21
|
+
{
|
|
22
|
+
readonly _id: string;
|
|
23
|
+
|
|
24
|
+
private readonly url: string;
|
|
25
|
+
|
|
26
|
+
private readonly credentials: sst.Secret;
|
|
27
|
+
|
|
28
|
+
get _type() {
|
|
29
|
+
return ResourceType.ExternalApi;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
stack: StackType<TStage, TDomain>,
|
|
34
|
+
name: string,
|
|
35
|
+
props: ExternalApiProps,
|
|
36
|
+
) {
|
|
37
|
+
this._id = name;
|
|
38
|
+
// Read at synth, so a stage the declaration has no URL for fails the
|
|
39
|
+
// deploy — with the stage named — rather than the first request.
|
|
40
|
+
this.url = externalApiUrl({ id: name, url: props.url }, stack.stage);
|
|
41
|
+
this.credentials = new sst.Secret(`${name}Credentials`, props.placeholder);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
provides(): Record<string, $util.Input<string>> {
|
|
45
|
+
return { url: this.url, credentials: this.credentials.value };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getSSTLink() {
|
|
49
|
+
return { properties: { ...this.provides() } };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ExternalApiProps {
|
|
54
|
+
/** The declaration's `url` — one for every stage, or one per stage name. */
|
|
55
|
+
url: string | Readonly<Record<string, string>>;
|
|
56
|
+
/** A value for the credentials secret until one is set, for a preview stage. */
|
|
57
|
+
placeholder?: string;
|
|
58
|
+
}
|
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
|
+
}
|