@geekmidas/cloud 9.0.2 → 10.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/{index-ByEJy40r.d.cts → index-B5CZ1xVf.d.cts} +17 -9
  2. package/dist/index-B5CZ1xVf.d.cts.map +1 -0
  3. package/dist/{index-DZ0QrJQr.d.mts → index-DhHRjduZ.d.mts} +17 -9
  4. package/dist/index-DhHRjduZ.d.mts.map +1 -0
  5. package/dist/index.cjs +1 -1
  6. package/dist/index.d.cts +1 -1
  7. package/dist/index.d.mts +1 -1
  8. package/dist/index.mjs +1 -1
  9. package/dist/utils/index.cjs +1 -1
  10. package/dist/utils/index.d.cts +1 -1
  11. package/dist/utils/index.d.mts +1 -1
  12. package/dist/utils/index.mjs +1 -1
  13. package/dist/{utils-CtMjuIMR.cjs → utils-B1a2UuEO.cjs} +5 -5
  14. package/dist/utils-B1a2UuEO.cjs.map +1 -0
  15. package/dist/{utils-BdKG20_m.mjs → utils-DMOXJ27j.mjs} +5 -5
  16. package/dist/utils-DMOXJ27j.mjs.map +1 -0
  17. package/package.json +44 -7
  18. package/src/dokploy/Application.ts +259 -0
  19. package/src/dokploy/__tests__/Application.spec.ts +69 -0
  20. package/src/dokploy/index.ts +18 -0
  21. package/src/sst/__tests__/LinkedEnvironment.spec.ts +4 -1
  22. package/src/sst/__tests__/backends.spec.ts +249 -0
  23. package/src/sst/__tests__/bootstrap.spec.ts +140 -0
  24. package/src/sst/__tests__/database.spec.ts +119 -0
  25. package/src/sst/__tests__/fromManifest.spec.ts +266 -0
  26. package/src/sst/__tests__/provides.spec.ts +107 -0
  27. package/src/sst/__tests__/ses.spec.ts +93 -0
  28. package/src/sst/__tests__/surfaces.spec.ts +132 -0
  29. package/src/sst/__type-tests__/authorizers.type-test.ts +2 -2
  30. package/src/sst/__type-tests__/manifest.type-test.ts +3 -3
  31. package/src/sst/__type-tests__/messaging.type-test.ts +3 -3
  32. package/src/sst/__type-tests__/storage.type-test.ts +2 -2
  33. package/src/sst/{Api.ts → aws/Api.ts} +3 -3
  34. package/src/sst/aws/Cache.ts +259 -0
  35. package/src/sst/aws/Credential.ts +28 -0
  36. package/src/sst/{Cron.ts → aws/Cron.ts} +2 -2
  37. package/src/sst/aws/Database.ts +190 -0
  38. package/src/sst/aws/DatabaseBootstrap.ts +309 -0
  39. package/src/sst/aws/DerivedDatabase.ts +117 -0
  40. package/src/sst/aws/Email.ts +181 -0
  41. package/src/sst/aws/FileServer.ts +82 -0
  42. package/src/sst/{Function.ts → aws/Function.ts} +3 -3
  43. package/src/sst/aws/ObjectStorage.ts +76 -0
  44. package/src/sst/aws/Queue.ts +116 -0
  45. package/src/sst/aws/RestApiSurface.ts +92 -0
  46. package/src/sst/aws/Secret.ts +76 -0
  47. package/src/sst/aws/StaticSite.ts +92 -0
  48. package/src/sst/{Storage.ts → aws/Storage.ts} +3 -3
  49. package/src/sst/aws/Topic.ts +68 -0
  50. package/src/sst/aws/bootstrap/handler.ts +110 -0
  51. package/src/sst/aws/ses.ts +132 -0
  52. package/src/sst/errors.ts +65 -0
  53. package/src/sst/fromManifest.ts +899 -0
  54. package/src/sst/index.ts +62 -7
  55. package/src/sst/naming.ts +37 -16
  56. package/src/sst/tsconfig.json +2 -2
  57. package/src/sst/upstash.d.ts +38 -0
  58. package/dist/index-ByEJy40r.d.cts.map +0 -1
  59. package/dist/index-DZ0QrJQr.d.mts.map +0 -1
  60. package/dist/utils-BdKG20_m.mjs.map +0 -1
  61. package/dist/utils-CtMjuIMR.cjs.map +0 -1
  62. package/src/sst/Queue.ts +0 -46
  63. package/src/sst/Topic.ts +0 -37
@@ -0,0 +1,259 @@
1
+ import { elasticache } from '@pulumi/aws';
2
+ import { type GkmLinkable, ResourceType } from '../Linkable';
3
+ import type { StackType } from '../Stack';
4
+
5
+ /**
6
+ * `Cache` — a key/value cache, and the infra half of the `cache` kind.
7
+ *
8
+ * The kind where the *backend* is the whole design, because unlike mail the
9
+ * three do not speak the same protocol. What this composes is a URL whose
10
+ * scheme names one of them, and the generated entry registers the matching
11
+ * driver — so the two agree by construction rather than by being kept in step.
12
+ *
13
+ * Only one of the three provisions anything:
14
+ *
15
+ * - **`db`** provisions nothing at all. The cache is a table in the database
16
+ * already declared, so this resolves that database's URL and the table is
17
+ * created by the same thing that creates the schema. No second resource, no
18
+ * second credential, and nothing to pay for while idle.
19
+ * - **`upstash`** provisions a Redis database through Upstash's own Pulumi
20
+ * provider, which SST installs on demand: `sst add upstash`. It is not one of
21
+ * the two providers SST preloads, so the component checks for its global and
22
+ * says which command is missing rather than failing on an undefined name. A
23
+ * URL can still be supplied instead, for a database that already exists.
24
+ * - **`elasticache`** is the one that creates something, and it comes with the
25
+ * constraints of a thing in a VPC: it is reachable from functions in that VPC
26
+ * and from nothing else.
27
+ */
28
+ export class Cache<
29
+ TStage extends string = string,
30
+ TDomain extends string = string,
31
+ > implements GkmLinkable
32
+ {
33
+ readonly _id: string;
34
+
35
+ private readonly url: $util.Input<string>;
36
+
37
+ get _type() {
38
+ return ResourceType.Cache;
39
+ }
40
+
41
+ constructor(
42
+ _stack: StackType<TStage, TDomain>,
43
+ name: string,
44
+ props: CacheProps,
45
+ ) {
46
+ this._id = name;
47
+ this.url = props.url ?? this.provision(name, props);
48
+ }
49
+
50
+ /**
51
+ * Create the cache, for the two backends that can be created.
52
+ *
53
+ * `db` never reaches here — it resolves the declared database's URL, which
54
+ * the caller passes in as `url`.
55
+ */
56
+ private provision(name: string, props: CacheProps): $util.Input<string> {
57
+ return props.backend === 'upstash'
58
+ ? this.provisionUpstash(name, props)
59
+ : this.provisionElastiCache(name, props);
60
+ }
61
+
62
+ /**
63
+ * An Upstash Redis database, over HTTP.
64
+ *
65
+ * The default backend, because HTTP with a token is reachable from a Lambda
66
+ * with no VPC and no connection pool — the same argument that makes it worth
67
+ * running a proxy in front of Redis locally so both speak one protocol.
68
+ *
69
+ * The token goes in the URL's userinfo rather than a second key, because an
70
+ * address and the credential that opens it are one fact.
71
+ */
72
+ private provisionUpstash(
73
+ name: string,
74
+ props: CacheProps,
75
+ ): $util.Input<string> {
76
+ // Upstash is installed on demand rather than preloaded, so its global is
77
+ // absent until `sst add upstash` has run. Checking for it turns an
78
+ // undefined-name crash into a sentence naming the command.
79
+ if (typeof upstash === 'undefined') throw new CacheNeedsProvider(name);
80
+
81
+ const database = new upstash.RedisDatabase(`${name}Cache`, {
82
+ databaseName: name,
83
+ // `global` needs a primary region named alongside it, so a plain
84
+ // region is the default that needs no second decision.
85
+ region: props.region ?? 'eu-west-1',
86
+ // In transit, always. The client speaks HTTPS either way, and a cache
87
+ // reachable unencrypted over the public internet is not one.
88
+ tls: true,
89
+ });
90
+
91
+ return $util
92
+ .all([database.endpoint, database.restToken])
93
+ .apply(
94
+ ([endpoint, token]) =>
95
+ `https://:${encodeURIComponent(token)}@${endpoint}`,
96
+ );
97
+ }
98
+
99
+ /**
100
+ * A serverless Valkey cache, in the VPC the caller named.
101
+ *
102
+ * Serverless rather than a node group because this design provisions per
103
+ * stage, and a node running around the clock under every preview stage is a
104
+ * bill nobody chose. The database went the other way — a plain RDS instance —
105
+ * because a cluster buys complexity there rather than savings; the two are
106
+ * separate calls, not one principle applied twice.
107
+ *
108
+ * TLS is not optional — ElastiCache Serverless only accepts encrypted
109
+ * connections — which is why the URL is `rediss://` and why the driver
110
+ * registers both schemes.
111
+ */
112
+ private provisionElastiCache(
113
+ name: string,
114
+ props: CacheProps,
115
+ ): $util.Input<string> {
116
+ if (!props.vpc) throw new CacheNeedsVpc(name);
117
+
118
+ const cache = new elasticache.ServerlessCache(`${name}Cache`, {
119
+ engine: 'valkey',
120
+ name,
121
+ subnetIds: props.vpc.subnets,
122
+ securityGroupIds: props.vpc.securityGroups,
123
+ });
124
+
125
+ return $util.output(cache.endpoints).apply((endpoints) => {
126
+ const endpoint = endpoints[0];
127
+ if (!endpoint) throw new CacheNeedsVpc(name);
128
+
129
+ return $util
130
+ .all([endpoint.address, endpoint.port])
131
+ .apply(([address, port]) => `rediss://${address}:${port}`);
132
+ });
133
+ }
134
+
135
+ /** One key, the URL — and its scheme is the contract with the driver. */
136
+ provides(): Record<string, $util.Input<string>> {
137
+ return { url: this.url };
138
+ }
139
+
140
+ getSSTLink() {
141
+ return { properties: { ...this.provides() } };
142
+ }
143
+ }
144
+
145
+ export interface CacheProps {
146
+ /** Which backend to create. `db` never creates anything and never gets here. */
147
+ backend?: 'upstash' | 'elasticache' | 'db';
148
+ /** The region to create an Upstash database in. */
149
+ region?: string;
150
+ /**
151
+ * The cache's URL, whose scheme picks the driver.
152
+ *
153
+ * Supplied for the two backends that are not provisioned here — the declared
154
+ * database's URL for `db`, a secret for `upstash`. Absent for `elasticache`,
155
+ * which creates a cluster and composes its own.
156
+ */
157
+ url?: $util.Input<string>;
158
+ /**
159
+ * The VPC to put an ElastiCache cluster in.
160
+ *
161
+ * Required for that backend and meaningless for the others. A cache in a VPC
162
+ * is reachable from functions in that VPC and from nothing else, which is
163
+ * the trade that comes with choosing it.
164
+ */
165
+ vpc?: {
166
+ subnets: $util.Input<$util.Input<string>[]>;
167
+ securityGroups: $util.Input<$util.Input<string>[]>;
168
+ };
169
+ }
170
+
171
+ /** An ElastiCache-backed cache was declared and no VPC was supplied for it. */
172
+ export class CacheNeedsVpc extends Error {
173
+ constructor(readonly id: string) {
174
+ super(
175
+ `'${id}' is an ElastiCache cache and needs a VPC to live in. Supply ` +
176
+ `one through the deploy layer — ` +
177
+ `fromManifest(stack, manifest, { ${id}: { vpc } }) — the same one ` +
178
+ `the functions that read it run in, since a cache in a VPC is ` +
179
+ `reachable from nowhere else.`,
180
+ );
181
+ this.name = 'CacheNeedsVpc';
182
+ }
183
+ }
184
+
185
+ /**
186
+ * A cache backed by the database was declared, and no database was.
187
+ *
188
+ * The same rule pg-boss has, and the same reason: provisioning a database to
189
+ * hold only a cache is the resource this design refuses to invent on your
190
+ * behalf.
191
+ */
192
+ /**
193
+ * A cache's address: its database's URL, carrying the table it reads.
194
+ *
195
+ * The table travels in the URL rather than beside it because a database-backed
196
+ * cache has no address of its own — two caches in one database resolve the same
197
+ * connection string, so the parameter is the only thing that identifies which
198
+ * one a client is holding. Mirrors what the local target composes, so the same
199
+ * application code reads the same shape in both places.
200
+ */
201
+ export function withCacheTable(url: string, table: string): string {
202
+ const parsed = new URL(url);
203
+ parsed.searchParams.set('table', table);
204
+
205
+ return parsed.toString();
206
+ }
207
+
208
+ /**
209
+ * `services.cache: 'db'` in an app that declares more than one database.
210
+ *
211
+ * Picking one would put the cache somewhere nobody chose, and the symptom —
212
+ * entries that are never found — appears long after the deploy reported
213
+ * success. The fix is a stronger statement in application code:
214
+ * `orders.cache('Sessions')` names the database.
215
+ */
216
+ export class CacheIsAmbiguous extends Error {
217
+ constructor(
218
+ readonly id: string,
219
+ readonly databases: readonly string[],
220
+ ) {
221
+ super(
222
+ `'${id}' is a cache backed by the database, and this app declares ` +
223
+ `${databases.length}: ${databases.join(', ')}. Declare the cache from ` +
224
+ `its database — e.g. ${databases[0]}.cache('${id}') — rather than ` +
225
+ `with services.cache.`,
226
+ );
227
+ this.name = 'CacheIsAmbiguous';
228
+ }
229
+ }
230
+
231
+ export class CacheNeedsDatabase extends Error {
232
+ constructor(readonly id: string) {
233
+ super(
234
+ `'${id}' is a cache backed by the database, and this app declares no ` +
235
+ `database for it to live in. Declare one, or set services.cache to ` +
236
+ `'upstash' or 'elasticache'.`,
237
+ );
238
+ this.name = 'CacheNeedsDatabase';
239
+ }
240
+ }
241
+
242
+ /**
243
+ * An Upstash cache was declared and Upstash's provider is not installed.
244
+ *
245
+ * SST preloads two providers and installs the rest on demand, so the global this
246
+ * component reaches for does not exist until somebody runs the command. Naming
247
+ * the command beats an undefined-name crash halfway through a synth.
248
+ */
249
+ export class CacheNeedsProvider extends Error {
250
+ constructor(readonly id: string) {
251
+ super(
252
+ `'${id}' is an Upstash cache and the Upstash provider is not ` +
253
+ `installed. Run \`sst add upstash\` in the app, or supply a URL for ` +
254
+ `a database that already exists — ` +
255
+ `fromManifest(stack, manifest, { ${id}: { url } }).`,
256
+ );
257
+ this.name = 'CacheNeedsProvider';
258
+ }
259
+ }
@@ -0,0 +1,28 @@
1
+ import { Secret, type SecretProps } from './Secret';
2
+
3
+ /**
4
+ * `Credential` — a third-party credential SST holds, and the infra half of the
5
+ * `credential` kind.
6
+ *
7
+ * The *same storage* as a secret and a different kind, because what differs is
8
+ * the lifecycle rather than the mechanism: a secret is generated and rotated by
9
+ * the platform, while a credential is issued by someone else and has a shape
10
+ * the construct validates on the way in. Both are values you set out of band
11
+ * with `sst secret set`.
12
+ *
13
+ * The role is `credential` rather than `value`, and that is not cosmetic. The
14
+ * role *is* the contract — `providedKeyFor` turns it into the key the app
15
+ * declared — so a credential providing `value` would supply `STRIPE_VALUE`
16
+ * against a declared `STRIPE_CREDENTIAL`, and `assertProvides` would reject the
17
+ * stack at synth. Which is the check working; renaming here is the fix.
18
+ */
19
+ export class Credential<
20
+ TStage extends string = string,
21
+ TDomain extends string = string,
22
+ > extends Secret<TStage, TDomain> {
23
+ override provides(): Record<string, $util.Input<string>> {
24
+ return { credential: this.value };
25
+ }
26
+ }
27
+
28
+ export interface CredentialProps extends SecretProps {}
@@ -3,9 +3,9 @@ import {
3
3
  flattenManifestField,
4
4
  type ManifestField,
5
5
  } from '@geekmidas/manifest';
6
+ import type { GkmLinkable } from '../Linkable';
7
+ import type { StackType } from '../Stack';
6
8
  import { Function } from './Function';
7
- import type { GkmLinkable } from './Linkable';
8
- import type { StackType } from './Stack';
9
9
 
10
10
  export type CronExpressionValue = number | '*' | '?' | `${number}/${number}`;
11
11
  export type CronExpressionDay =
@@ -0,0 +1,190 @@
1
+ import * as postgresUrl from '@geekmidas/db/pg/url';
2
+ import { type GkmLinkable, ResourceType } from '../Linkable';
3
+ import type { StackType } from '../Stack';
4
+
5
+ /**
6
+ * `Database` — an RDS Postgres instance, and the infra half of the `database`
7
+ * kind.
8
+ *
9
+ * **A provisioned instance rather than Aurora Serverless v2.** Aurora is the
10
+ * more interesting answer on paper — it scales to zero, so an idle preview
11
+ * stage costs storage and nothing else, which suits a design that provisions
12
+ * per stage. It is also a cluster: more moving parts, a different resource
13
+ * type, and pricing that is harder to predict for the steady-state workload
14
+ * most stages actually are. A plain instance is the ordinary thing, and the
15
+ * ordinary thing is the better default.
16
+ *
17
+ * Aurora is not reachable from here: this class *is* the RDS component and its
18
+ * props are that component's args, so a stage wanting a cluster needs a second
19
+ * class rather than an override. Worth adding when something wants it; not
20
+ * worth pretending it already exists.
21
+ *
22
+ * :::caution
23
+ * Moving between the two replaces the database. They are different resources,
24
+ * not different settings — the data does not come with you.
25
+ * :::
26
+ *
27
+ * **Who provisions the read replica** — nobody does, and a reader resolves to
28
+ * the writer's address. An Aurora cluster has a reader endpoint; an RDS
29
+ * instance has no second address to hand out, so `DatabaseReader` returns this
30
+ * one. That is the fallback the design already specified for `--target=server`
31
+ * and it is safe for the same reason: read-only is enforced by the role's
32
+ * grants, never by which endpoint you happened to reach. Adding `replicas`
33
+ * creates instances but no endpoint that balances across them, so it changes
34
+ * nothing here.
35
+ */
36
+ export class Database<
37
+ TStage extends string = string,
38
+ TDomain extends string = string,
39
+ >
40
+ extends sst.aws.Postgres
41
+ implements GkmLinkable
42
+ {
43
+ readonly _id!: string;
44
+
45
+ get _type() {
46
+ return ResourceType.SSTPostgres;
47
+ }
48
+
49
+ /** The schema pinned on the connection's `search_path`, where one applies. */
50
+ private readonly schema: string | undefined;
51
+
52
+ /**
53
+ * The VPC this cluster lives in.
54
+ *
55
+ * Kept so the bootstrap function can be put in the same one — a database
56
+ * reachable from outside its VPC is the problem the requirement avoids, and
57
+ * the DDL has to reach it from inside.
58
+ */
59
+ readonly vpc: sst.aws.Vpc;
60
+
61
+ constructor(
62
+ _stack: StackType<TStage, TDomain>,
63
+ name: string,
64
+ props: DatabaseProps,
65
+ ) {
66
+ const { schema, ...args } = props;
67
+
68
+ super(name, args);
69
+ this._id = name;
70
+ this.schema = schema;
71
+ this.vpc = args.vpc;
72
+ }
73
+
74
+ /**
75
+ * One key, the URL a running handler connects with.
76
+ *
77
+ * The owner URL is deliberately not here. It exists — a migrator needs DDL
78
+ * rights — but it is wired straight into the migrator by the adapter, so no
79
+ * edge in any manifest can name it and nothing can be granted it by mistake.
80
+ *
81
+ * Composed through `@geekmidas/db`'s codec rather than by hand, which is what
82
+ * puts `search_path` in as a libpq `options` parameter. A plain
83
+ * `?search_path=` is accepted by every URL parser, ignored by the server, and
84
+ * produces a database that looks empty.
85
+ */
86
+ provides(): Record<string, $util.Input<string>> {
87
+ return { url: this.urlFor({}) };
88
+ }
89
+
90
+ /**
91
+ * This cluster's connection URL, optionally through the reader endpoint or
92
+ * pinned to a different schema.
93
+ *
94
+ * One composition serving three callers — the cluster itself, a reader, and
95
+ * a schema tenant — so the three cannot come to disagree about how a URL is
96
+ * put together. That matters more than it sounds: `search_path` is the part
97
+ * that goes quietly missing when the join is done by hand.
98
+ */
99
+ urlFor(options: {
100
+ /**
101
+ * Ask for the read path.
102
+ *
103
+ * Kept in the signature and currently resolved to the same address: an
104
+ * RDS instance has one endpoint. It states the caller's intent — a reader
105
+ * wants the read path — so moving a stage to a cluster that *has* a second
106
+ * endpoint changes one line here rather than every call site. What makes
107
+ * the connection read-only is the role it authenticates as, not this.
108
+ */
109
+ reader?: boolean;
110
+ schema?: string;
111
+ /**
112
+ * Connect as a role other than the master.
113
+ *
114
+ * What every tenant does. The master credential exists before any role
115
+ * does, which is why the bootstrap uses it and why nothing else should: a
116
+ * handler holding it could drop the database it was reading.
117
+ */
118
+ as?: { user: $util.Input<string>; password: $util.Input<string> };
119
+ }): $util.Input<string> {
120
+ // One address: an RDS instance has no reader endpoint. Reading through the
121
+ // writer is safe because the reader *role* is what forbids writing.
122
+ const host = this.host;
123
+ // A role carries its own `search_path`, pinned by `ALTER ROLE`. It goes in
124
+ // the URL only for the master, which has no role of its own to pin it on.
125
+ const searchPath = options.as ? undefined : (options.schema ?? this.schema);
126
+
127
+ return $util
128
+ .all([
129
+ host,
130
+ this.port,
131
+ this.database,
132
+ options.as?.user ?? this.username,
133
+ options.as?.password ?? this.password,
134
+ ])
135
+ .apply(([resolvedHost, port, database, username, password]) =>
136
+ postgresUrl.build({
137
+ host: resolvedHost,
138
+ port,
139
+ database,
140
+ username,
141
+ password,
142
+ ...(searchPath ? { searchPath } : {}),
143
+ }),
144
+ );
145
+ }
146
+
147
+ override getSSTLink() {
148
+ const link = super.getSSTLink();
149
+ return {
150
+ ...link,
151
+ properties: { ...link.properties, ...this.provides() },
152
+ };
153
+ }
154
+ }
155
+
156
+ export interface DatabaseProps extends Omit<sst.aws.PostgresArgs, 'vpc'> {
157
+ /** The schema to pin on the connection's `search_path`. */
158
+ schema?: string;
159
+ /**
160
+ * The VPC the database lives in.
161
+ *
162
+ * Narrowed to the component from the wider argument the RDS component
163
+ * accepts, because the bootstrap function has to run in this same VPC and a
164
+ * function needs security groups as well as subnets — which the loose form
165
+ * cannot carry. Requiring the component means one thing is passed and both
166
+ * halves can use it.
167
+ */
168
+ vpc: sst.aws.Vpc;
169
+ }
170
+
171
+ /**
172
+ * A database was declared and the deploy layer supplied no VPC.
173
+ *
174
+ * Not something the adapter can default. RDS lives in a VPC, and creating one
175
+ * means creating a NAT gateway — a real monthly cost, in an account whose
176
+ * networking may already be someone else's decision. So it is required, named,
177
+ * and supplied where other provider-specific props are.
178
+ */
179
+ export class DatabaseNeedsVpc extends Error {
180
+ constructor(readonly id: string) {
181
+ super(
182
+ `'${id}' is a database, and a database needs a VPC to live in. ` +
183
+ `Supply one through the deploy layer — ` +
184
+ `fromManifest(stack, manifest, { ${id}: { vpc } }) — because ` +
185
+ `creating one means creating a NAT gateway, which costs money in an ` +
186
+ `account whose networking may already be someone else's decision.`,
187
+ );
188
+ this.name = 'DatabaseNeedsVpc';
189
+ }
190
+ }