@geekmidas/manifest 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.
- package/dist/index.cjs +1021 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1105 -13
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1105 -13
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +998 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -1
- package/CHANGELOG.md +0 -115
- package/src/index.ts +0 -127
- package/tsconfig.json +0 -9
- package/tsdown.config.ts +0 -3
package/dist/index.d.cts
CHANGED
|
@@ -1,22 +1,1071 @@
|
|
|
1
|
-
//#region src/
|
|
1
|
+
//#region src/declaration.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The construct manifest — the contract between what an application declares
|
|
4
|
+
* and what a target adapter provisions.
|
|
5
|
+
*
|
|
6
|
+
* Kinds are added here as each one lands, not up front: a declaration for a
|
|
7
|
+
* construct nobody has built yet is a guess that the implementation will
|
|
8
|
+
* contradict. See `docs/design/constructs-paradigm.md`.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* A construct's canonical id — PascalCase, unique within the manifest.
|
|
12
|
+
*
|
|
13
|
+
* Inputs canonicalise, so `uploads`, `Uploads`, `user_uploads`, and
|
|
14
|
+
* `user-uploads` are the *same* id rather than four that collide. Everything
|
|
15
|
+
* else derives from it: the service key is its `Uncapitalize`, the env prefix
|
|
16
|
+
* its SCREAMING_SNAKE form, the cloud name its kebab form scoped by stage and
|
|
17
|
+
* app.
|
|
18
|
+
*/
|
|
19
|
+
type ConstructId = string;
|
|
20
|
+
type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
|
|
21
|
+
/**
|
|
22
|
+
* Constrains a construct name at the point it is written.
|
|
23
|
+
*
|
|
24
|
+
* Resolves to the name itself when valid, and otherwise to a string explaining
|
|
25
|
+
* why — so the compiler reports *"not assignable to type 'a construct name
|
|
26
|
+
* cannot start with a digit'"* rather than the unhelpful `never`.
|
|
27
|
+
*
|
|
28
|
+
* Only the cases a template-literal type can see are caught here; `canonicalId`
|
|
29
|
+
* enforces the rest at runtime, which is also what covers JavaScript callers.
|
|
30
|
+
*
|
|
31
|
+
* @example new ObjectStorage('Uploads') // ok
|
|
32
|
+
* @example new ObjectStorage('2fa') // a construct name cannot start with a digit
|
|
33
|
+
*/
|
|
34
|
+
type ConstructName<S extends string> = S extends '' ? 'a construct name cannot be empty' : S extends `${Digit}${string}` ? 'a construct name cannot start with a digit' : S;
|
|
35
|
+
/** Shared by every declaration. */
|
|
36
|
+
interface Node {
|
|
37
|
+
id: ConstructId;
|
|
38
|
+
/**
|
|
39
|
+
* Env keys this construct resolves onto anything that depends on it.
|
|
40
|
+
* Names only — the values are composed by the adapter from the provisioned
|
|
41
|
+
* resource's own attributes.
|
|
42
|
+
*/
|
|
43
|
+
provides?: readonly string[];
|
|
44
|
+
/**
|
|
45
|
+
* Env keys this construct needs. Derivable from `dependencies`, so it is an
|
|
46
|
+
* assertion rather than an input: the adapter composes env from the edges and
|
|
47
|
+
* checks the result against this, which catches app/infra drift at synth.
|
|
48
|
+
*/
|
|
49
|
+
requires?: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A dependency edge. Records only *what* is depended on — never permissions.
|
|
53
|
+
* From one edge the framework derives env and the runtime binding; a target
|
|
54
|
+
* adapter separately derives cloud access.
|
|
55
|
+
*/
|
|
56
|
+
interface Dependency<TTarget extends ConstructId = ConstructId> {
|
|
57
|
+
/**
|
|
58
|
+
* The {@link ConstructId} of the consumed construct. Left open here because a
|
|
59
|
+
* declaration is written before the manifest that contains it; once assembled,
|
|
60
|
+
* `IdsOf` narrows it and the build's reference-integrity check enforces it.
|
|
61
|
+
*/
|
|
62
|
+
target: TTarget;
|
|
63
|
+
kind: DeclarationKind;
|
|
64
|
+
}
|
|
65
|
+
/** Anything with a handler. */
|
|
66
|
+
interface Fn extends Node {
|
|
67
|
+
handler: string;
|
|
68
|
+
dependencies: readonly Dependency[];
|
|
69
|
+
}
|
|
70
|
+
/** Blob storage. `--target=aws` provisions a bucket. */
|
|
71
|
+
interface ObjectsDeclaration extends Node {
|
|
72
|
+
kind: 'objects';
|
|
73
|
+
versioned?: boolean;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A domain that serves a bucket's objects.
|
|
77
|
+
*
|
|
78
|
+
* Its own construct rather than a flag on the bucket, because three things it
|
|
79
|
+
* has to express are not properties of a bucket: a surface can front several
|
|
80
|
+
* origins, a bucket can have several surfaces over it, and issuing a
|
|
81
|
+
* certificate and writing a DNS record is a domain lifecycle that has no
|
|
82
|
+
* business living inside an `objects` provisioner. It shares its infrastructure
|
|
83
|
+
* with a static site rather than with storage — a site is the same
|
|
84
|
+
* distribution over a build output instead of over live contents.
|
|
85
|
+
*
|
|
86
|
+
* It derives from the bucket by `of`, which costs the one thing the flag gave
|
|
87
|
+
* for free: the bucket alone no longer says whether it is served, and finding
|
|
88
|
+
* out means finding whoever points at it. That is answered the way every other
|
|
89
|
+
* derivation is — the reference check at manifest build, so an unresolvable
|
|
90
|
+
* origin is a build failure and `gkm` can name, for any bucket, the surfaces
|
|
91
|
+
* over it.
|
|
92
|
+
*
|
|
93
|
+
* Private by default. `open` is an exception list, because a bucket where
|
|
94
|
+
* forgetting a flag publishes user uploads is the wrong default — and paths
|
|
95
|
+
* rather than per-object flags, because a path pattern is what the
|
|
96
|
+
* infrastructure actually enforces and a per-object ACL is a thing nobody
|
|
97
|
+
* audits.
|
|
98
|
+
*
|
|
99
|
+
* "Open" never means the bucket is world-readable. It means the server serves
|
|
100
|
+
* that path without a signature; the bucket is private in both cases.
|
|
101
|
+
*/
|
|
102
|
+
interface FileServerDeclaration extends Node {
|
|
103
|
+
kind: 'file-server';
|
|
104
|
+
/** The bucket whose objects it serves. */
|
|
105
|
+
of: ConstructId;
|
|
106
|
+
/**
|
|
107
|
+
* Paths served without a signature — everything else requires one.
|
|
108
|
+
*
|
|
109
|
+
* Globs, matched most-literally by the infrastructure: a CDN keys its
|
|
110
|
+
* behaviours off path patterns, and a bucket policy names prefixes.
|
|
111
|
+
*/
|
|
112
|
+
open?: readonly string[];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Outbound email.
|
|
116
|
+
*
|
|
117
|
+
* Provides one `smtp://` URL and nothing else, because email is delivered over
|
|
118
|
+
* SMTP whatever the provider — Mailpit locally, SES through its SMTP interface,
|
|
119
|
+
* Resend and Postmark through theirs. There is no `provider` field here for the
|
|
120
|
+
* same reason there is no `ses://` scheme: which service delivers the mail
|
|
121
|
+
* differs between dev and prod, so by this design's own test it is stage-varying
|
|
122
|
+
* config rather than a structural fact about the app.
|
|
123
|
+
*
|
|
124
|
+
* What *is* structural is only that the app sends mail at all. The sending
|
|
125
|
+
* domain is not: it is `myapp.test` locally and `example.com` deployed, so it
|
|
126
|
+
* fails the same test the provider does and resolves at deploy alongside every
|
|
127
|
+
* other address.
|
|
128
|
+
*/
|
|
129
|
+
interface EmailDeclaration extends Node {
|
|
130
|
+
kind: 'email';
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* A logical database, its schema, and the roles that reach it.
|
|
134
|
+
*
|
|
135
|
+
* Provides one key — the *runtime* role's URL. The owner URL exists but is
|
|
136
|
+
* deliberately absent from `provides`: it is wired by the adapter straight into
|
|
137
|
+
* the migrator and seeder this construct declares, so no edge in any manifest
|
|
138
|
+
* can name it and nothing else can be granted it by mistake.
|
|
139
|
+
*/
|
|
2
140
|
/**
|
|
3
|
-
*
|
|
4
|
-
* project's deployable units (routes, functions, crons, subscribers, queues)
|
|
5
|
-
* with the metadata an infrastructure layer needs to provision them.
|
|
141
|
+
* The Postgres major versions this toolbox provisions.
|
|
6
142
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
143
|
+
* A union rather than a string, because the two targets that read it are not
|
|
144
|
+
* equally forgiving: locally it becomes a container tag, where a typo yields a
|
|
145
|
+
* confusing pull failure, and on AWS it becomes an engine version, where a
|
|
146
|
+
* wrong value fails partway through a deploy. Both are better as a compile
|
|
147
|
+
* error.
|
|
148
|
+
*
|
|
149
|
+
* The union is what this toolbox knows how to name, not a promise that every
|
|
150
|
+
* target offers every one of them. A deployed stage is limited by the engine
|
|
151
|
+
* catalogue in its region, so check before pinning an unusual version:
|
|
152
|
+
*
|
|
153
|
+
* ```
|
|
154
|
+
* aws rds describe-db-engine-versions --engine postgres \
|
|
155
|
+
* --query 'DBEngineVersions[].EngineVersion' --output text
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* RDS offered 11 through 18 in `eu-west-1` when this was written. 11 and 12 are
|
|
159
|
+
* left out because they are past upstream end-of-life: still provisionable, but
|
|
160
|
+
* not something to make easy to reach for.
|
|
161
|
+
*/
|
|
162
|
+
type PostgresVersion = 13 | 14 | 15 | 16 | 17 | 18;
|
|
163
|
+
/**
|
|
164
|
+
* The version used when a database names none.
|
|
165
|
+
*
|
|
166
|
+
* The point is not which number this is but that there is only one of them.
|
|
167
|
+
* Local ran 18 while Aurora provisioned its own default of 17.7, and nothing in
|
|
168
|
+
* any declaration recorded the difference — a stage could behave differently
|
|
169
|
+
* from a developer's machine for a reason neither could see. Both now read
|
|
170
|
+
* this.
|
|
171
|
+
*/
|
|
172
|
+
declare const DEFAULT_POSTGRES_VERSION: PostgresVersion;
|
|
173
|
+
interface DatabaseDeclaration extends Node {
|
|
174
|
+
kind: 'database';
|
|
175
|
+
engine?: 'postgres';
|
|
176
|
+
/**
|
|
177
|
+
* The engine's major version, read by every target that provisions one.
|
|
178
|
+
*
|
|
179
|
+
* Declared rather than configured per target, because a version set in a
|
|
180
|
+
* compose file and a version set in a deploy config are two statements of
|
|
181
|
+
* one fact — and they had already drifted apart, silently, by a major.
|
|
182
|
+
*
|
|
183
|
+
* Defaults to {@link DEFAULT_POSTGRES_VERSION}.
|
|
184
|
+
*/
|
|
185
|
+
version?: PostgresVersion;
|
|
186
|
+
/**
|
|
187
|
+
* The schema, pinned on both roles' `search_path`. Names the role the schema
|
|
188
|
+
* plays rather than restating the database's own name, so `app` reads
|
|
189
|
+
* correctly beside `auth` and `pgboss`.
|
|
190
|
+
*/
|
|
191
|
+
schema?: string;
|
|
192
|
+
/**
|
|
193
|
+
* Whether to provision the owner/runtime role split. Off falls back to the
|
|
194
|
+
* cluster's master credential in both URLs — a deliberate downgrade, not a
|
|
195
|
+
* default. See `roles: false` in the design doc.
|
|
196
|
+
*/
|
|
197
|
+
roles?: boolean;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* A read-only endpoint on an existing database or schema.
|
|
201
|
+
*
|
|
202
|
+
* Provisions no cluster of its own — `of` names the parent it reads from.
|
|
203
|
+
* Read-only is enforced by the role's grants rather than by which endpoint it
|
|
204
|
+
* resolves to, so falling back to the writer where no replica exists stays safe.
|
|
205
|
+
*/
|
|
206
|
+
interface DatabaseReaderDeclaration extends Node {
|
|
207
|
+
kind: 'database-reader';
|
|
208
|
+
of: ConstructId;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* A second schema inside an existing database, with its own role(s) and URL.
|
|
212
|
+
*
|
|
213
|
+
* The mechanism behind tenancy: the parent's role holds no grant on these
|
|
214
|
+
* tables at all. pg-boss is an instance of this rather than a special case.
|
|
215
|
+
*/
|
|
216
|
+
interface DatabaseSchemaDeclaration extends Node {
|
|
217
|
+
kind: 'database-schema';
|
|
218
|
+
of: ConstructId;
|
|
219
|
+
schema: string;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* A generated secret — a signing key, a token, anything with no address.
|
|
223
|
+
*
|
|
224
|
+
* It provides a value rather than a URL, which is why it is a node of its own
|
|
225
|
+
* instead of a field on whatever needs it: the thing that generates it, the
|
|
226
|
+
* thing that stores it, and the thing that reads it are three different systems
|
|
227
|
+
* deployed, and one derived string locally.
|
|
228
|
+
*/
|
|
229
|
+
interface SecretDeclaration extends Node {
|
|
230
|
+
kind: 'secret';
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* A third-party credential with a shape.
|
|
234
|
+
*
|
|
235
|
+
* Distinct from {@link SecretDeclaration} by *lifecycle*, which is the only
|
|
236
|
+
* distinction worth having two kinds for. A secret is generated and rotated by
|
|
237
|
+
* the platform — `gkm secrets`, `sst secret set` — and is one opaque string
|
|
238
|
+
* whose name is its key. A credential is issued by someone else, arrives with
|
|
239
|
+
* several fields, and is validated on the way in: a Stripe key pair, an OAuth
|
|
240
|
+
* client, a webhook signing secret.
|
|
241
|
+
*
|
|
242
|
+
* It provides one key holding a JSON object, rather than one key per field.
|
|
243
|
+
* That is what a secret manager actually stores, and it is also the only shape
|
|
244
|
+
* that works with an arbitrary StandardSchema — the spec has no introspection
|
|
245
|
+
* API, so enumerating a schema's fields means reaching into one library's
|
|
246
|
+
* internals and being wrong for every other.
|
|
247
|
+
*/
|
|
248
|
+
interface CredentialDeclaration extends Node {
|
|
249
|
+
kind: 'credential';
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* An identity provider somebody else runs.
|
|
253
|
+
*
|
|
254
|
+
* Provisions nothing — the issuer already exists — so a target's whole job is
|
|
255
|
+
* to hand the process an issuer and an audience, and the verifier discovers the
|
|
256
|
+
* rest at runtime.
|
|
257
|
+
*
|
|
258
|
+
* It is a node rather than a field on whatever authenticates through it because
|
|
259
|
+
* two surfaces can name the same provider, and because *which* population a
|
|
260
|
+
* surface admits is a fact worth reading off the graph: an admin console
|
|
261
|
+
* authenticated by the customer auth server is a finding, and it is only
|
|
262
|
+
* visible if both are declarations.
|
|
263
|
+
*/
|
|
264
|
+
interface OidcDeclaration extends Node {
|
|
265
|
+
kind: 'oidc';
|
|
266
|
+
/**
|
|
267
|
+
* The issuer, when it does not vary by deployment.
|
|
268
|
+
*
|
|
269
|
+
* Absent means it arrives in the environment instead — a staging tenant, a
|
|
270
|
+
* per-customer directory. The audience is never here: it identifies one
|
|
271
|
+
* deployment to the provider.
|
|
272
|
+
*/
|
|
273
|
+
issuer?: string;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* A key/value cache.
|
|
277
|
+
*
|
|
278
|
+
* Provides one URL. What is *in* that URL is the backend's business — Upstash's
|
|
279
|
+
* REST API, a Redis endpoint, or a table in a database — and the scheme is what
|
|
280
|
+
* picks the client, exactly as it does for object storage.
|
|
281
|
+
*
|
|
282
|
+
* Two ways to declare one, and the difference is a real statement rather than a
|
|
283
|
+
* spelling. `new Cache('Sessions')` says *this app caches*, leaving where to the
|
|
284
|
+
* deployment; `orders.cache('Sessions')` says *this app caches in that
|
|
285
|
+
* database*, which is a fact about the application and belongs in its code. The
|
|
286
|
+
* second is the same strengthening `orders.schema('AuthDb')` is over declaring a
|
|
287
|
+
* second database.
|
|
288
|
+
*/
|
|
289
|
+
interface CacheDeclaration extends Node {
|
|
290
|
+
kind: 'cache';
|
|
291
|
+
/**
|
|
292
|
+
* The database this cache lives in, when it lives in one.
|
|
293
|
+
*
|
|
294
|
+
* Present only for a cache derived from a database. It removes a guess the
|
|
295
|
+
* backend selection otherwise has to make — "the declared database" is
|
|
296
|
+
* unambiguous with one and arbitrary with two — and it means the table's
|
|
297
|
+
* schema and the role that reaches it come from the parent rather than from
|
|
298
|
+
* a second convention.
|
|
299
|
+
*/
|
|
300
|
+
of?: ConstructId;
|
|
301
|
+
/**
|
|
302
|
+
* The table entries are kept in, resolved against the connection's
|
|
303
|
+
* `search_path`. Defaults to `cache`.
|
|
304
|
+
*/
|
|
305
|
+
table?: string;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* An HTTP surface and the handlers mounted on it.
|
|
309
|
+
*
|
|
310
|
+
* The first kind that is not a resource in the ordinary sense: it owns an
|
|
311
|
+
* address, and the functions it triggers are *nested inside it* rather than
|
|
312
|
+
* listed beside it, because position carries the trigger — a handler here is
|
|
313
|
+
* reached by its method and path and by nothing else.
|
|
314
|
+
*
|
|
315
|
+
* `authorizers` are names. A bare string is resolved by the target (`iam`), while
|
|
316
|
+
* a {@link ConstructId} names a construct that carries its own implementation,
|
|
317
|
+
* its database dependency, and its session typing.
|
|
318
|
+
*/
|
|
319
|
+
interface RestApiDeclaration extends Node {
|
|
320
|
+
kind: 'rest-api';
|
|
321
|
+
/**
|
|
322
|
+
* Where the process serving it is built from, relative to the workspace
|
|
323
|
+
* root — the same thing a `site` says, for the same reason.
|
|
324
|
+
*
|
|
325
|
+
* A surface is a deploy unit: one of these is one server. Without it the
|
|
326
|
+
* deploy had to ask the *config* which apps to build, and a surface could
|
|
327
|
+
* never be its own process because it was not in that list. Two surfaces in
|
|
328
|
+
* one app then had to share one container, which is how an auth server ended
|
|
329
|
+
* up mounted into an API by a hook.
|
|
330
|
+
*
|
|
331
|
+
* Optional, and its absence is meaningful: a surface with no app of its own
|
|
332
|
+
* is served by the surface that named it — an auth server mounted into the
|
|
333
|
+
* API that called `.auth()` on it, rather than a second container nobody
|
|
334
|
+
* asked for.
|
|
335
|
+
*/
|
|
336
|
+
app?: AppSpec;
|
|
337
|
+
/**
|
|
338
|
+
* The construct that authenticates this surface.
|
|
339
|
+
*
|
|
340
|
+
* An edge like any other — so the auth server learns this surface's origin,
|
|
341
|
+
* and the two share a cookie domain — but a *named* one, because "who
|
|
342
|
+
* authenticates me" is a different fact from "who I happen to call", and
|
|
343
|
+
* only one of them decides what a request is allowed to be.
|
|
344
|
+
*
|
|
345
|
+
* It is the id rather than the client: what every endpoint consumes is
|
|
346
|
+
* `verify(request) → Session | null`, and that is the one thing every
|
|
347
|
+
* provider shares. A surface names its authenticator; the target decides
|
|
348
|
+
* what verifying means.
|
|
349
|
+
*/
|
|
350
|
+
auth?: ConstructId;
|
|
351
|
+
/**
|
|
352
|
+
* CORS tunables for this surface.
|
|
353
|
+
*
|
|
354
|
+
* *Who* may call it is never here — that is derived from the constructs
|
|
355
|
+
* declaring an edge to this surface, and arrives as `<ID>_TRUSTED_ORIGINS`.
|
|
356
|
+
* A hand-written origin list is the thing this model removes; these are the
|
|
357
|
+
* knobs that genuinely cannot be derived from a graph.
|
|
358
|
+
*
|
|
359
|
+
* Omitted entirely, a surface still gets CORS — with the derived origins and
|
|
360
|
+
* sensible defaults. There is nothing to opt into.
|
|
361
|
+
*/
|
|
362
|
+
cors?: {
|
|
363
|
+
/** Preflight cache lifetime in seconds. Default 86400. */
|
|
364
|
+
maxAge?: number;
|
|
365
|
+
/** Whether the browser may send credentials. Default true. */
|
|
366
|
+
credentials?: boolean;
|
|
367
|
+
/** Extra request headers to allow, beyond content-type and authorization. */
|
|
368
|
+
allowHeaders?: readonly string[];
|
|
369
|
+
/** Response headers the browser may read. */
|
|
370
|
+
exposeHeaders?: readonly string[];
|
|
371
|
+
};
|
|
372
|
+
authorizers?: readonly string[];
|
|
373
|
+
/** The authorizer applied where an endpoint names none. */
|
|
374
|
+
defaultAuthorizer?: string;
|
|
375
|
+
/**
|
|
376
|
+
* Every route on this surface.
|
|
377
|
+
*
|
|
378
|
+
* Complete, always — a manifest that says "the routes are over there, run
|
|
379
|
+
* this glob to find them" is not a manifest, it is a pointer to one. An
|
|
380
|
+
* earlier version carried a `routes` glob for an application's own API and
|
|
381
|
+
* left this empty, which meant the document *claimed no routes* while five
|
|
382
|
+
* existed. Being incomplete is a gap; being wrong is worse.
|
|
383
|
+
*
|
|
384
|
+
* So a surface takes its endpoints as a list and reads method, path and
|
|
385
|
+
* handler off them. That also removes a duplicate: the glob was written once
|
|
386
|
+
* in `gkm.config.ts` and again on the construct, two strings that could
|
|
387
|
+
* disagree about the same thing.
|
|
388
|
+
*/
|
|
389
|
+
endpoints: readonly RestApiEndpoint[];
|
|
390
|
+
/**
|
|
391
|
+
* Other surfaces this one calls.
|
|
392
|
+
*
|
|
393
|
+
* **Not a dependency, and deliberately not spelled like one.** A dependency
|
|
394
|
+
* is an injection: `resolveEdges` gives a function exactly the constructs it
|
|
395
|
+
* declared and nothing else, which is what makes least privilege fall out of
|
|
396
|
+
* the graph instead of out of discipline. A surface-level `dependencies`
|
|
397
|
+
* would hand *every route* on this API whatever the surface named — which is
|
|
398
|
+
* precisely the over-granting that rule exists to prevent.
|
|
399
|
+
*
|
|
400
|
+
* What this records is weaker and only flows one way: it puts this API's
|
|
401
|
+
* origin on the called surface's trusted-origin list. Nothing links from it,
|
|
402
|
+
* nothing is granted by it, and per-route edges stay on the endpoints where
|
|
403
|
+
* they belong.
|
|
404
|
+
*/
|
|
405
|
+
calls?: readonly Dependency[];
|
|
406
|
+
}
|
|
407
|
+
/** One glob, or several. Mirrors the CLI's `Routes` without depending on it. */
|
|
408
|
+
type Glob = string | readonly string[];
|
|
409
|
+
/**
|
|
410
|
+
* How the process serving a declaration is built and run.
|
|
411
|
+
*
|
|
412
|
+
* The half of an application that a graph genuinely cannot derive. *What*
|
|
413
|
+
* exists — a surface, a site, the edges between them — is declared and read
|
|
414
|
+
* back from the manifest. *Where its source lives and which globs find its
|
|
415
|
+
* code* is not derivable from anything: it is a fact about a directory.
|
|
416
|
+
*
|
|
417
|
+
* It lives on the declaration rather than in a config `apps` block because the
|
|
418
|
+
* two were the same list written twice, and the copy in config was the one that
|
|
419
|
+
* could disagree. A site declared `path: 'apps/web'` and an app entry declared
|
|
420
|
+
* `path: 'apps/web'`, and nothing checked them against each other; a surface
|
|
421
|
+
* that config had no entry for simply never deployed.
|
|
422
|
+
*
|
|
423
|
+
* Only the declaration that *is* an app carries one. Two surfaces in one
|
|
424
|
+
* process means one of them has the spec and the other collapses onto it —
|
|
425
|
+
* which is the same rule that decides deploy units, now stated once.
|
|
426
|
+
*/
|
|
427
|
+
/**
|
|
428
|
+
* Where an app's code lives when nobody says otherwise.
|
|
429
|
+
*
|
|
430
|
+
* One glob, every kind — the same rule the `constructs` glob follows. A handler
|
|
431
|
+
* in one of these directories is found; anywhere else needs a `code` glob, and
|
|
432
|
+
* saying so is the whole reason the field still exists.
|
|
433
|
+
*/
|
|
434
|
+
declare const DEFAULT_APP_CODE = "./{endpoints,functions,crons,queues,topics,subscribers}/**/*.ts";
|
|
435
|
+
interface AppSpec {
|
|
436
|
+
/**
|
|
437
|
+
* Where its source lives, relative to the workspace root.
|
|
438
|
+
*
|
|
439
|
+
* Optional, and normally omitted: `apps/<kebab-id>` when that directory
|
|
440
|
+
* exists, and the workspace root otherwise. An `Api` construct in a
|
|
441
|
+
* monorepo means `apps/api`, and in a single-app project it means `.` —
|
|
442
|
+
* both of which are answerable by looking, which is why neither was worth
|
|
443
|
+
* making someone write down.
|
|
444
|
+
*
|
|
445
|
+
* Set one only when the layout is genuinely different, e.g.
|
|
446
|
+
* `path: 'services/api'`.
|
|
447
|
+
*/
|
|
448
|
+
path?: string;
|
|
449
|
+
/**
|
|
450
|
+
* The port it answers on locally.
|
|
451
|
+
*
|
|
452
|
+
* Optional, and normally omitted: ports are assigned in a stable order so
|
|
453
|
+
* that adding a site does not renumber the others. Set one only when
|
|
454
|
+
* something outside the workspace has to know it in advance.
|
|
455
|
+
*/
|
|
456
|
+
port?: number;
|
|
457
|
+
/**
|
|
458
|
+
* One glob that finds everything this app defines, relative to `path`.
|
|
459
|
+
*
|
|
460
|
+
* Every export of every matching module is inspected, and each kind is
|
|
461
|
+
* picked out by whatever recognises it — the same rule the `constructs`
|
|
462
|
+
* glob already follows. A glob per kind was the specialness this model
|
|
463
|
+
* removes: five patterns that had to be kept in step, where a handler in the
|
|
464
|
+
* wrong directory simply never loaded and nothing said so.
|
|
465
|
+
*
|
|
466
|
+
* The per-kind fields below still work, and still win where both are given,
|
|
467
|
+
* because a single-app `defineConfig` has always been written that way.
|
|
468
|
+
*
|
|
469
|
+
* Optional, and normally omitted: the conventional directories under
|
|
470
|
+
* `path`, which is `DEFAULT_APP_CODE`. A glob is worth writing only when
|
|
471
|
+
* the code is somewhere else.
|
|
472
|
+
*/
|
|
473
|
+
code?: Glob;
|
|
474
|
+
/** Globs that find one kind of thing. Prefer `code`. */
|
|
475
|
+
routes?: Glob;
|
|
476
|
+
functions?: Glob;
|
|
477
|
+
crons?: Glob;
|
|
478
|
+
queues?: Glob;
|
|
479
|
+
topics?: Glob;
|
|
480
|
+
subscribers?: Glob;
|
|
481
|
+
/** `./config/env#envParser` — module, optionally with an export. */
|
|
482
|
+
envParser?: string;
|
|
483
|
+
logger?: string;
|
|
484
|
+
telescope?: string | boolean | Record<string, unknown>;
|
|
485
|
+
studio?: string | boolean | Record<string, unknown>;
|
|
486
|
+
openapi?: boolean | Record<string, unknown>;
|
|
487
|
+
runtime?: 'node' | 'bun';
|
|
488
|
+
/** Env files to load, in order. */
|
|
489
|
+
env?: Glob;
|
|
490
|
+
/** Entry module for an app the build does not generate. */
|
|
491
|
+
entry?: string;
|
|
492
|
+
/** Modules to import when sniffing which env vars a frontend reads. */
|
|
493
|
+
config?: {
|
|
494
|
+
client?: string;
|
|
495
|
+
server?: string;
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* A frontend — a construct like any other, which is what removes the last
|
|
500
|
+
* mechanism that ran in parallel to the graph.
|
|
501
|
+
*
|
|
502
|
+
* Its edges are what make it worth declaring. A site depending on an API is the
|
|
503
|
+
* single fact behind four things that are hand-maintained otherwise: the site's
|
|
504
|
+
* build-time `VITE_API_URL`, the API's CORS origins, the auth server's trusted
|
|
505
|
+
* origins, and which generated client lands in which app. None of those are
|
|
506
|
+
* declared anywhere here, because all four are the *same* edge read from one
|
|
507
|
+
* end or the other.
|
|
508
|
+
*
|
|
509
|
+
* `variant` is the framework, because the framework changes the code you write:
|
|
510
|
+
* it selects how the values are delivered (`VITE_`, `NEXT_PUBLIC_`, a
|
|
511
|
+
* `config.json`), never which values there are.
|
|
512
|
+
*/
|
|
513
|
+
interface SiteDeclaration extends Node {
|
|
514
|
+
kind: 'site';
|
|
515
|
+
variant: 'static' | 'next' | 'tanstack';
|
|
516
|
+
/**
|
|
517
|
+
* How it is built and run, `path` included.
|
|
518
|
+
*
|
|
519
|
+
* Required, where a surface's is optional: a site is always its own app.
|
|
520
|
+
* There is no arrangement in which two sites are one process.
|
|
521
|
+
*/
|
|
522
|
+
app?: AppSpec;
|
|
523
|
+
/**
|
|
524
|
+
* Whether this is the site the base domain points at.
|
|
525
|
+
*
|
|
526
|
+
* Structural rather than config: *which* site is primary does not vary by
|
|
527
|
+
* stage, even though its hostname does — that is what `app.domain` is for.
|
|
528
|
+
*
|
|
529
|
+
* Only meaningful when a project has more than one site, and then only when
|
|
530
|
+
* none of them is named `web`. The convention still holds first, because it
|
|
531
|
+
* is a convention people already rely on.
|
|
532
|
+
*/
|
|
533
|
+
root?: boolean;
|
|
534
|
+
/**
|
|
535
|
+
* What it calls. On a node rather than on a handler because a site has no
|
|
536
|
+
* single entrypoint — the whole app is the consumer.
|
|
537
|
+
*/
|
|
538
|
+
dependencies: readonly Dependency[];
|
|
539
|
+
}
|
|
540
|
+
/** One route on a surface. */
|
|
541
|
+
interface RestApiEndpoint extends Fn {
|
|
542
|
+
method: string;
|
|
543
|
+
path: string;
|
|
544
|
+
authorizer?: string;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* A point-to-point queue and the single consumer that drains it.
|
|
548
|
+
*
|
|
549
|
+
* Provides one key, the producer's connection string. The protocol in it picks
|
|
550
|
+
* the transport — `pgboss://` locally, `sqs://` deployed — so a producer names
|
|
551
|
+
* no broker, exactly as a database consumer names no cloud.
|
|
552
|
+
*
|
|
553
|
+
* The consumer side provides nothing: a worker is reached *through* its queue,
|
|
554
|
+
* so there is no second key and nothing can depend on a handler.
|
|
555
|
+
*/
|
|
556
|
+
interface QueueDeclaration extends Node {
|
|
557
|
+
kind: 'queue';
|
|
558
|
+
/** FIFO ordering, where the transport offers it. */
|
|
559
|
+
fifo?: boolean;
|
|
560
|
+
/**
|
|
561
|
+
* The single consumer that drains it.
|
|
562
|
+
*
|
|
563
|
+
* Nested rather than listed beside the queue, because **position carries the
|
|
564
|
+
* trigger**: a handler here is reached by messages arriving on this queue and
|
|
565
|
+
* by nothing else, so there is no `trigger` field to keep in step with it.
|
|
566
|
+
*/
|
|
567
|
+
worker: Fn;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* A topic — pub/sub fan-out, one publisher and any number of subscribers.
|
|
571
|
+
*
|
|
572
|
+
* Like a queue it provides only the producer's string; a subscriber is bound to
|
|
573
|
+
* the topic rather than depending on it, so the binding is an edge the deploy
|
|
574
|
+
* target reads, not an env key. Locally both sides meet on the same pg-boss
|
|
575
|
+
* connection, which is why the subscriber needs no key of its own.
|
|
576
|
+
*/
|
|
577
|
+
interface TopicDeclaration extends Node {
|
|
578
|
+
kind: 'topic';
|
|
579
|
+
/** The event type names this topic carries. */
|
|
580
|
+
events: readonly string[];
|
|
581
|
+
/**
|
|
582
|
+
* The handlers bound to it, each with the events it wants.
|
|
583
|
+
*
|
|
584
|
+
* Nested for the same reason a queue's worker is: position is the trigger. A
|
|
585
|
+
* subscriber is *bound* to a topic rather than depending on it, which is why
|
|
586
|
+
* it holds no key of its own and cannot be reached except through the topic.
|
|
587
|
+
*/
|
|
588
|
+
subscribers: readonly (Fn & {
|
|
589
|
+
events: readonly string[];
|
|
590
|
+
})[];
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* A function invoked directly, with no surface in front of it.
|
|
594
|
+
*
|
|
595
|
+
* An `Fn` rather than a `Node`, because a function *is* a handler — there is no
|
|
596
|
+
* resource beside it to declare. It provides its own address, so something else
|
|
597
|
+
* can depend on it and be given a way to call it.
|
|
598
|
+
*/
|
|
599
|
+
interface FunctionDeclaration extends Fn {
|
|
600
|
+
kind: 'function';
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* A function on a schedule.
|
|
604
|
+
*
|
|
605
|
+
* The schedule is the trigger and it is structural: *that* something runs
|
|
606
|
+
* nightly is a fact about the application, while which timezone a stage
|
|
607
|
+
* interprets it in is not.
|
|
608
|
+
*/
|
|
609
|
+
interface CronDeclaration extends Fn {
|
|
610
|
+
kind: 'cron';
|
|
611
|
+
/** A rate or cron expression, e.g. `rate(1 day)`. */
|
|
612
|
+
schedule: string;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Every declaration. A discriminated union, so `kind` gives exhaustiveness *and*
|
|
616
|
+
* per-kind fields — there is no separate enum to keep in step, and no shape
|
|
617
|
+
* carrying fields that belong to a different kind.
|
|
618
|
+
*/
|
|
619
|
+
type Declaration = ObjectsDeclaration | FileServerDeclaration | EmailDeclaration | DatabaseDeclaration | DatabaseReaderDeclaration | DatabaseSchemaDeclaration | CacheDeclaration | SecretDeclaration | CredentialDeclaration | RestApiDeclaration | SiteDeclaration | OidcDeclaration | QueueDeclaration | TopicDeclaration | FunctionDeclaration | CronDeclaration;
|
|
620
|
+
/** A declaration that provisions nothing of its own and names a parent. */
|
|
621
|
+
/**
|
|
622
|
+
* A declaration that names a parent.
|
|
623
|
+
*
|
|
624
|
+
* A union rather than an `Extract`, because one kind is *optionally* derived: a
|
|
625
|
+
* cache lives in a database when it was declared from one and stands alone
|
|
626
|
+
* otherwise, so `of` is optional on it and an `Extract<…, { of: ConstructId }>`
|
|
627
|
+
* would not select it. {@link isDerived} tests the value rather than the kind
|
|
628
|
+
* for exactly that reason.
|
|
629
|
+
*/
|
|
630
|
+
type DerivedDeclaration = Extract<Declaration, {
|
|
631
|
+
of: ConstructId;
|
|
632
|
+
}> | CacheDeclaration;
|
|
633
|
+
type DerivedKind = DerivedDeclaration['kind'];
|
|
634
|
+
/**
|
|
635
|
+
* What each kind may derive from.
|
|
636
|
+
*
|
|
637
|
+
* Small enough to state exhaustively, and stating it makes cycles impossible
|
|
638
|
+
* without a graph walk: readers are terminal, so no chain can return to its
|
|
639
|
+
* start. There is no `writer` — the database *is* the writer, which is what
|
|
640
|
+
* keeps a replica from being reached by accident.
|
|
641
|
+
*/
|
|
642
|
+
declare const DERIVES_FROM: Readonly<Record<DerivedKind, readonly string[]>>;
|
|
643
|
+
type DeclarationKind = Declaration['kind'];
|
|
644
|
+
/**
|
|
645
|
+
* The manifest: every construct keyed by its id.
|
|
646
|
+
*
|
|
647
|
+
* Flat rather than grouped, because `Dependency.target` resolves as
|
|
648
|
+
* `m[target]` — a lookup that stays O(1) and identical whether the edge points
|
|
649
|
+
* at a resource, a surface, or another function.
|
|
650
|
+
*
|
|
651
|
+
* Use it as a **constraint, not an annotation**. `gkm build` emits
|
|
652
|
+
* `as const satisfies ConstructManifest`, which checks the shape while keeping
|
|
653
|
+
* every id, kind, and provided key a literal — annotating with this type
|
|
654
|
+
* instead would widen them all to `string` and consumers could no longer select
|
|
655
|
+
* anything:
|
|
9
656
|
*
|
|
10
657
|
* ```ts
|
|
11
|
-
* export const manifest = {
|
|
12
|
-
*
|
|
13
|
-
*
|
|
658
|
+
* export const manifest = {
|
|
659
|
+
* Uploads: { kind: 'objects', id: 'Uploads', provides: ['UPLOADS_URL'] },
|
|
660
|
+
* } as const satisfies ConstructManifest;
|
|
661
|
+
*
|
|
662
|
+
* type Ids = IdsOf<typeof manifest>; // 'Uploads'
|
|
663
|
+
* type Env = ProvidedKeys<typeof manifest, 'Uploads'>; // 'UPLOADS_URL'
|
|
14
664
|
* ```
|
|
665
|
+
*/
|
|
666
|
+
type ConstructManifest = Readonly<Record<ConstructId, Declaration>>;
|
|
667
|
+
/** Every id in a manifest. */
|
|
668
|
+
type IdsOf<M extends ConstructManifest> = Extract<keyof M, string>;
|
|
669
|
+
/** The declaration for one id. */
|
|
670
|
+
type DeclarationOf<M extends ConstructManifest, K extends IdsOf<M>> = M[K];
|
|
671
|
+
/** Every id of a given kind — what an adapter iterates when provisioning. */
|
|
672
|
+
type IdsOfKind<M extends ConstructManifest, K extends DeclarationKind> = { [Id in IdsOf<M>]: M[Id]['kind'] extends K ? Id : never }[IdsOf<M>];
|
|
673
|
+
/** The env keys one construct provides. */
|
|
674
|
+
type ProvidedKeys<M extends ConstructManifest, K extends IdsOf<M>> = M[K] extends {
|
|
675
|
+
provides: readonly (infer P)[];
|
|
676
|
+
} ? P : never;
|
|
677
|
+
/** Every env key any construct in the manifest provides. */
|
|
678
|
+
type AllProvidedKeys<M extends ConstructManifest> = { [Id in IdsOf<M>]: ProvidedKeys<M, Id> }[IdsOf<M>];
|
|
679
|
+
/**
|
|
680
|
+
* What each kind provides, by role rather than by provider syntax.
|
|
681
|
+
*
|
|
682
|
+
* This is the contract between the construct that declares a key and the cloud
|
|
683
|
+
* component that supplies its value — an interface rather than shared code,
|
|
684
|
+
* because a shared codec would have to contain `bucket` and `region`, and
|
|
685
|
+
* provider words in the neutral layer is the problem this design exists to fix.
|
|
686
|
+
*
|
|
687
|
+
* How a value is composed and parsed stays private to each provider pair, so
|
|
688
|
+
* `s3://` and `gs://` never appear here.
|
|
689
|
+
*/
|
|
690
|
+
interface ProvidesByKind {
|
|
691
|
+
objects: {
|
|
692
|
+
url: string;
|
|
693
|
+
};
|
|
694
|
+
/** Where the served objects answer. Public: a browser is the point of it. */
|
|
695
|
+
'file-server': {
|
|
696
|
+
url: string;
|
|
697
|
+
};
|
|
698
|
+
/**
|
|
699
|
+
* An `smtp://` URL, credentials included — never shippable — and the
|
|
700
|
+
* identity mail is sent from.
|
|
701
|
+
*
|
|
702
|
+
* The sending address is the one thing about mail that genuinely differs per
|
|
703
|
+
* stage (`myapp.test` locally, a verified domain deployed), so it travels
|
|
704
|
+
* beside the URL rather than being written into the construct.
|
|
705
|
+
*/
|
|
706
|
+
email: {
|
|
707
|
+
url: string;
|
|
708
|
+
from: string;
|
|
709
|
+
};
|
|
710
|
+
/**
|
|
711
|
+
* One key, the runtime role's. The owner URL is not here by design — see
|
|
712
|
+
* {@link DatabaseDeclaration}.
|
|
713
|
+
*/
|
|
714
|
+
database: {
|
|
715
|
+
url: string;
|
|
716
|
+
};
|
|
717
|
+
'database-reader': {
|
|
718
|
+
url: string;
|
|
719
|
+
};
|
|
720
|
+
'database-schema': {
|
|
721
|
+
url: string;
|
|
722
|
+
};
|
|
723
|
+
/** The endpoint and its token, in one string. */
|
|
724
|
+
cache: {
|
|
725
|
+
url: string;
|
|
726
|
+
};
|
|
727
|
+
/**
|
|
728
|
+
* Where the surface answers, who may call it, and the domain its cookies
|
|
729
|
+
* are scoped to.
|
|
730
|
+
*
|
|
731
|
+
* Only `url` is a fact about the surface itself. The other two are read off
|
|
732
|
+
* its *inbound* edges — every construct that depends on it — which is why a
|
|
733
|
+
* surface never lists its own callers: nothing enumerates the things that
|
|
734
|
+
* point at it, the graph already does.
|
|
735
|
+
*
|
|
736
|
+
* `trustedOrigins` and `cookieDomain` are one key each rather than a list
|
|
737
|
+
* and a structure, because both cross a process boundary as environment.
|
|
738
|
+
*/
|
|
739
|
+
'rest-api': {
|
|
740
|
+
url: string;
|
|
741
|
+
/** Comma-separated. Empty when nothing declares an edge to this surface. */
|
|
742
|
+
trustedOrigins: string;
|
|
743
|
+
/**
|
|
744
|
+
* The parent domain shared by the surface and its callers, leading dot
|
|
745
|
+
* included — `.example.com`. Absent where there is nothing to share:
|
|
746
|
+
* one host locally, unrelated hosts deployed.
|
|
747
|
+
*/
|
|
748
|
+
cookieDomain: string;
|
|
749
|
+
};
|
|
750
|
+
/** The value itself. A secret has no address to hand out instead. */
|
|
751
|
+
secret: {
|
|
752
|
+
value: string;
|
|
753
|
+
};
|
|
754
|
+
/**
|
|
755
|
+
* The credential as one JSON object, parsed and validated by the construct
|
|
756
|
+
* that declared the schema.
|
|
757
|
+
*/
|
|
758
|
+
credential: {
|
|
759
|
+
credential: string;
|
|
760
|
+
};
|
|
761
|
+
/**
|
|
762
|
+
* The producer's connection string. One key, not two: the consumer is
|
|
763
|
+
* reached through the queue rather than by an address of its own.
|
|
764
|
+
*/
|
|
765
|
+
queue: {
|
|
766
|
+
publisherConnectionString: string;
|
|
767
|
+
};
|
|
768
|
+
topic: {
|
|
769
|
+
publisherConnectionString: string;
|
|
770
|
+
};
|
|
771
|
+
/** Where to invoke it — what lets something else depend on a function. */
|
|
772
|
+
function: {
|
|
773
|
+
url: string;
|
|
774
|
+
};
|
|
775
|
+
/** A cron is reached by its schedule and by nothing else, so it provides none. */
|
|
776
|
+
cron: Record<never, never>;
|
|
777
|
+
/** Where the site is served. Public for the same reason an API's is. */
|
|
778
|
+
site: {
|
|
779
|
+
url: string;
|
|
780
|
+
};
|
|
781
|
+
/**
|
|
782
|
+
* Where tokens come from and which audience they must carry.
|
|
783
|
+
*
|
|
784
|
+
* Two keys because the halves have different lifetimes: the issuer may be a
|
|
785
|
+
* fact about the product, the audience is always a fact about one
|
|
786
|
+
* deployment.
|
|
787
|
+
*/
|
|
788
|
+
oidc: {
|
|
789
|
+
issuer: string;
|
|
790
|
+
audience: string;
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
type Provides<K extends keyof ProvidesByKind> = ProvidesByKind[K];
|
|
794
|
+
/**
|
|
795
|
+
* Which provided values may be shipped to a browser.
|
|
796
|
+
*
|
|
797
|
+
* Drives client-side prefixing (`VITE_`, `NEXT_PUBLIC_`) and nothing else — it
|
|
798
|
+
* is not a restriction on what may be depended on, since a server-side consumer
|
|
799
|
+
* can legitimately use any of them. A bucket's `url` presigns and stays private.
|
|
800
|
+
*/
|
|
801
|
+
declare const PUBLIC: { readonly [K in keyof ProvidesByKind]: readonly (keyof ProvidesByKind[K])[] };
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region src/derive.d.ts
|
|
804
|
+
/** Whether a declaration names a parent. */
|
|
805
|
+
declare function isDerived(declaration: Declaration): declaration is DerivedDeclaration & {
|
|
806
|
+
of: ConstructId;
|
|
807
|
+
};
|
|
808
|
+
/**
|
|
809
|
+
* Check every derived construct against its parent.
|
|
810
|
+
*
|
|
811
|
+
* Two rules: the parent exists, and its kind may vend this one. Together they
|
|
812
|
+
* make cycles unreachable — a reader is terminal, so no chain of `of` can
|
|
813
|
+
* return to where it started, and no walk is needed to prove it.
|
|
814
|
+
*/
|
|
815
|
+
declare function assertDerivations(manifest: ConstructManifest): void;
|
|
816
|
+
/**
|
|
817
|
+
* The order constructs must be provisioned in: every parent before its children.
|
|
818
|
+
*
|
|
819
|
+
* Resources are leaves and so come first in any order; only derived nodes
|
|
820
|
+
* constrain the sequence, and they form a shallow forest rather than a general
|
|
821
|
+
* graph. This walks each node's ancestors on demand instead of running a full
|
|
822
|
+
* topological sort, which is the same result at this depth and reads as what it
|
|
823
|
+
* is.
|
|
824
|
+
*
|
|
825
|
+
* Assumes {@link assertDerivations} has passed — a missing parent would
|
|
826
|
+
* otherwise be a silent omission here rather than an error.
|
|
827
|
+
*/
|
|
828
|
+
declare function provisionOrder(manifest: ConstructManifest): string[];
|
|
829
|
+
/**
|
|
830
|
+
* Every edge a declaration carries, wherever the kind happens to keep them.
|
|
831
|
+
*
|
|
832
|
+
* Dependencies live in two places by design: on a node when the whole construct
|
|
833
|
+
* is the consumer (a site), and on each nested handler when the construct is a
|
|
834
|
+
* surface (a `rest-api`, whose routes each depend on their own things and
|
|
835
|
+
* nothing more). Flattening that difference here is what lets every consumer of
|
|
836
|
+
* the graph — reverse lookups, filtering, reference checks — ask one question.
|
|
837
|
+
*/
|
|
838
|
+
declare function dependenciesOf(declaration: Declaration): readonly Dependency[];
|
|
839
|
+
/**
|
|
840
|
+
* The ids that depend on one construct — the graph read backwards.
|
|
841
|
+
*
|
|
842
|
+
* This is the whole mechanism behind CORS origins and trusted origins. Both are
|
|
843
|
+
* lists of *callers*, and a caller is exactly an inbound edge, so neither is
|
|
844
|
+
* ever written down: a surface that listed its own callers would have to be
|
|
845
|
+
* edited every time something new called it, which is the hand-maintained list
|
|
846
|
+
* this replaces.
|
|
847
|
+
*
|
|
848
|
+
* Sorted, because it feeds a comma-separated env value that would otherwise
|
|
849
|
+
* change whenever the manifest's key order did — and a value that churns is a
|
|
850
|
+
* container that redeploys for no reason.
|
|
851
|
+
*/
|
|
852
|
+
declare function dependentsOf(manifest: ConstructManifest, id: ConstructId): string[];
|
|
853
|
+
/**
|
|
854
|
+
* How each site variant names a value it ships to the browser.
|
|
855
|
+
*
|
|
856
|
+
* The prefix *is* the framework's contract — `VITE_`, `NEXT_PUBLIC_` and
|
|
857
|
+
* `EXPO_PUBLIC_` all mean "inline this into the bundle" — so it is the one thing
|
|
858
|
+
* a variant changes, and it changes nothing else.
|
|
859
|
+
*/
|
|
860
|
+
declare const PUBLIC_PREFIX: Record<SiteDeclaration['variant'], string>;
|
|
861
|
+
/**
|
|
862
|
+
* The keys a site's bundle needs, mapped to the key each value comes from —
|
|
863
|
+
* `{ VITE_API_URL: 'API_URL' }`.
|
|
864
|
+
*
|
|
865
|
+
* A rename, not a second derivation: `API_URL` is resolved once, by whatever
|
|
866
|
+
* resolved it for the server, and the site reads the same value under the name
|
|
867
|
+
* its bundler will inline. That is what keeps a site and its API from coming to
|
|
868
|
+
* disagree about where the API is.
|
|
869
|
+
*
|
|
870
|
+
* Filtered by `PUBLIC` rather than by what the site asked for. A site may
|
|
871
|
+
* legitimately depend on anything — its server half, where it has one, reads env
|
|
872
|
+
* exactly as a function does — so this is not a restriction on edges. It decides
|
|
873
|
+
* one thing: which values may be prefixed into a bundle, which is what keeps
|
|
874
|
+
* `ORDERS_URL` and its password out of a JavaScript file served to the public.
|
|
875
|
+
*
|
|
876
|
+
* Shared by every target for the same reason `providedKeyFor` is: a site built
|
|
877
|
+
* locally and the same site built by a deploy must inline the same names.
|
|
878
|
+
*/
|
|
879
|
+
declare function publicEnvFor(declaration: SiteDeclaration, manifest: ConstructManifest): Record<string, string>;
|
|
880
|
+
//# sourceMappingURL=derive.d.ts.map
|
|
881
|
+
//#endregion
|
|
882
|
+
//#region src/errors.d.ts
|
|
883
|
+
/**
|
|
884
|
+
* Manifest errors.
|
|
15
885
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
886
|
+
* Messages state the rule, which is constant; the offending value is a field.
|
|
887
|
+
* An interpolated message cannot be matched on, reads differently every time it
|
|
888
|
+
* is thrown, and carries user input into every log line that touches it.
|
|
19
889
|
*/
|
|
890
|
+
/** A construct id that cannot survive the names derived from it. */
|
|
891
|
+
declare class InvalidConstructId extends Error {
|
|
892
|
+
/** What was passed in. */
|
|
893
|
+
readonly input: string;
|
|
894
|
+
/** What canonicalising it produced, which is what failed the rule. */
|
|
895
|
+
readonly canonical: string;
|
|
896
|
+
constructor(input: string, canonical: string);
|
|
897
|
+
}
|
|
898
|
+
/** A derived construct naming a parent the manifest does not contain. */
|
|
899
|
+
declare class UnknownParent extends Error {
|
|
900
|
+
/** The derived construct. */
|
|
901
|
+
readonly id: string;
|
|
902
|
+
/** The parent it named. */
|
|
903
|
+
readonly of: string;
|
|
904
|
+
/** Ids the manifest does contain, for the caller to match against. */
|
|
905
|
+
readonly available: readonly string[];
|
|
906
|
+
constructor(id: string, of: string, available: readonly string[]);
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* A derived construct naming a parent that may not vend it — a reader of a
|
|
910
|
+
* reader, a schema of a schema.
|
|
911
|
+
*/
|
|
912
|
+
declare class IllegalDerivation extends Error {
|
|
913
|
+
readonly id: string;
|
|
914
|
+
readonly kind: string;
|
|
915
|
+
/** The kind of the parent it named. */
|
|
916
|
+
readonly parentKind: string;
|
|
917
|
+
/** The parent kinds that may vend this one. */
|
|
918
|
+
readonly allowed: readonly string[];
|
|
919
|
+
constructor(id: string, kind: string, parentKind: string, allowed: readonly string[]);
|
|
920
|
+
}
|
|
921
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
922
|
+
//#endregion
|
|
923
|
+
//#region src/naming.d.ts
|
|
924
|
+
/**
|
|
925
|
+
* `UPPER_SNAKE_CASE`, with numbers kept against the word they follow.
|
|
926
|
+
*
|
|
927
|
+
* Matches `environmentCase` in `@geekmidas/envkit`, which reads the values these
|
|
928
|
+
* names key. The two must agree exactly, so this is the implementation and that
|
|
929
|
+
* one should defer to it.
|
|
930
|
+
*
|
|
931
|
+
* @example environmentCase('sendEmail') // 'SEND_EMAIL'
|
|
932
|
+
* @example environmentCase('api2') // 'API2' (digit joins its word)
|
|
933
|
+
*/
|
|
934
|
+
declare function environmentCase(name: string): string;
|
|
935
|
+
/**
|
|
936
|
+
* The env key a construct provides for one of its roles.
|
|
937
|
+
*
|
|
938
|
+
* @example provideKey('Uploads', 'url') // 'UPLOADS_URL'
|
|
939
|
+
* @example provideKey('Uploads', 'cdnUrl') // 'UPLOADS_CDN_URL'
|
|
940
|
+
*/
|
|
941
|
+
declare function provideKey(id: string, role: string): string;
|
|
942
|
+
/**
|
|
943
|
+
* A construct's canonical id — PascalCase.
|
|
944
|
+
*
|
|
945
|
+
* `uploads`, `Uploads`, `user_uploads`, and `user-uploads` all canonicalise to
|
|
946
|
+
* the same id, so declaring two of them is a duplicate rather than a collision
|
|
947
|
+
* to detect.
|
|
948
|
+
*
|
|
949
|
+
* Runtime only. Writing the id in PascalCase is what keeps the *type* usable:
|
|
950
|
+
* the service key is `Uncapitalize<TName>`, a TypeScript intrinsic, so no
|
|
951
|
+
* type-level transform is needed and none has to be kept in step with this one.
|
|
952
|
+
*
|
|
953
|
+
* @example canonicalId('user-uploads') // 'UserUploads'
|
|
954
|
+
*/
|
|
955
|
+
declare function canonicalId(input: string): string;
|
|
956
|
+
/**
|
|
957
|
+
* The key a construct is reached under in the service record.
|
|
958
|
+
*
|
|
959
|
+
* The runtime twin of `Uncapitalize<TName>`, which types it — they must agree,
|
|
960
|
+
* so they live next to each other rather than being re-derived by each
|
|
961
|
+
* construct.
|
|
962
|
+
*
|
|
963
|
+
* @example serviceKey('UserUploads') // 'userUploads' → services.userUploads
|
|
964
|
+
*/
|
|
965
|
+
declare function serviceKey(id: string): string;
|
|
966
|
+
/**
|
|
967
|
+
* The table a cache keeps its entries in, when nobody named one.
|
|
968
|
+
*
|
|
969
|
+
* Derived from the cache's own id rather than fixed at `cache`, because a
|
|
970
|
+
* database may hold more than one and two caches sharing a table share a
|
|
971
|
+
* keyspace — `orders.cache('Sessions')` and `orders.cache('Rates')` would
|
|
972
|
+
* silently read each other's entries and evict each other's keys.
|
|
973
|
+
*
|
|
974
|
+
* Prefixed rather than suffixed so every cache sorts together in `\dt`, and
|
|
975
|
+
* prefixed at all so a cache named for a thing the application also stores —
|
|
976
|
+
* `orders.cache('Users')` — cannot collide with the table holding that thing.
|
|
977
|
+
*
|
|
978
|
+
* Read by whoever composes the URL and by whoever creates the table, so both
|
|
979
|
+
* default the same way.
|
|
980
|
+
*
|
|
981
|
+
* @example cacheTable('Sessions') // 'cache_sessions'
|
|
982
|
+
*/
|
|
983
|
+
declare function cacheTable(id: string): string;
|
|
984
|
+
/**
|
|
985
|
+
* Kebab-cases an identifier, acronym- and digit-aware.
|
|
986
|
+
*
|
|
987
|
+
* `userName` → `user-name`, `APIKey` → `api-key`, `S3Bucket` → `s3-bucket`.
|
|
988
|
+
*
|
|
989
|
+
* The last of those is why this is here rather than `snakecase(id)` with the
|
|
990
|
+
* underscores swapped: lodash splits a digit from the letter beside it, so
|
|
991
|
+
* `S3Bucket` became `s-3-bucket` on one provider and `s3-bucket` on the other.
|
|
992
|
+
* Two implementations of one rule, agreeing on every id anybody had tried.
|
|
993
|
+
*
|
|
994
|
+
* `environmentCase` already corrected for the same thing in the other
|
|
995
|
+
* direction — `api2` keeps its digit — so the two spellings of "kebab this id"
|
|
996
|
+
* in this file did not even agree with each other.
|
|
997
|
+
*/
|
|
998
|
+
declare function kebabCase(value: string): string;
|
|
999
|
+
/**
|
|
1000
|
+
* The physical name a target provisions a construct under — lowercase kebab,
|
|
1001
|
+
* scoped so two stages or apps sharing an account cannot collide.
|
|
1002
|
+
*
|
|
1003
|
+
* **One rule, every provider.** A construct is named the same thing on AWS and
|
|
1004
|
+
* on Dokploy, which is what lets a name be read across them: `Database` in the
|
|
1005
|
+
* `production` stage of `kitchen-sink` is `production-kitchen-sink-database`
|
|
1006
|
+
* wherever it lands. The SST target's `prefixedName` is this function under
|
|
1007
|
+
* another signature and defers to it.
|
|
1008
|
+
*
|
|
1009
|
+
* Idempotent in its prefix: an id that already carries the scope is not given a
|
|
1010
|
+
* second one, so composing names cannot double up.
|
|
1011
|
+
*
|
|
1012
|
+
* @example cloudName({ stage: 'prod', app: 'myapp' }, 'UserUploads')
|
|
1013
|
+
* // 'prod-myapp-user-uploads'
|
|
1014
|
+
*/
|
|
1015
|
+
declare function cloudName(scope: {
|
|
1016
|
+
stage: string;
|
|
1017
|
+
app: string;
|
|
1018
|
+
}, id: string): string;
|
|
1019
|
+
/**
|
|
1020
|
+
* {@link cloudName} for a caller that holds its scope as a list.
|
|
1021
|
+
*
|
|
1022
|
+
* The SST target's stacks add a segment of their own, so the prefix is not
|
|
1023
|
+
* always two parts — which is the only reason this signature exists.
|
|
1024
|
+
*/
|
|
1025
|
+
declare function scopedName(scope: readonly string[], id: string): string;
|
|
1026
|
+
/**
|
|
1027
|
+
* The domain a cookie must be scoped to so a surface and its callers share it.
|
|
1028
|
+
*
|
|
1029
|
+
* Derived from the addresses rather than configured, for the same reason the
|
|
1030
|
+
* origins are: the set of things that talk to a surface is already in the graph,
|
|
1031
|
+
* and the domain they have in common is a fact about that set. Returned with the
|
|
1032
|
+
* leading dot a `Domain` attribute wants.
|
|
1033
|
+
*
|
|
1034
|
+
* Returns `undefined` when there is nothing to scope, which is the common case
|
|
1035
|
+
* and not a failure:
|
|
1036
|
+
*
|
|
1037
|
+
* - **One host.** Locally everything is `localhost` on different ports, and
|
|
1038
|
+
* cookies ignore the port — so a `Domain` would add nothing and `.localhost`
|
|
1039
|
+
* is not a domain a browser will accept.
|
|
1040
|
+
* - **Nothing in common.** Unrelated hosts cannot share a cookie at all, and
|
|
1041
|
+
* emitting the longest common suffix anyway would be a value that silently
|
|
1042
|
+
* fails to set.
|
|
1043
|
+
*
|
|
1044
|
+
* **The public-suffix limit, stated rather than discovered.** Two apps on
|
|
1045
|
+
* `a.vercel.app` and `b.vercel.app` share `.vercel.app`, which every browser
|
|
1046
|
+
* rejects because it is a registrable suffix rather than a registrable domain.
|
|
1047
|
+
* Resolving that correctly needs the Public Suffix List, which is a downloaded,
|
|
1048
|
+
* expiring dataset — so this requires at least two labels and otherwise trusts
|
|
1049
|
+
* the addresses, and the value stays overridable for the case it gets wrong.
|
|
1050
|
+
*/
|
|
1051
|
+
declare function cookieDomain(urls: readonly string[]): string | undefined;
|
|
1052
|
+
/**
|
|
1053
|
+
* The env key a construct's provided role actually becomes.
|
|
1054
|
+
*
|
|
1055
|
+
* Almost always `provideKey(id, role)` — and `secret` is the exception, because
|
|
1056
|
+
* a secret's *name* is its key: `Auth` signs with `AUTH_SECRET`, which is also
|
|
1057
|
+
* what better-auth's own tooling looks for, and qualifying it by role would
|
|
1058
|
+
* produce `AUTH_SECRET_VALUE`.
|
|
1059
|
+
*
|
|
1060
|
+
* It lives here rather than in each target because two targets deriving the
|
|
1061
|
+
* same key separately is exactly the drift the app/infra contract check exists
|
|
1062
|
+
* to catch — and a check deriving the key differently from the thing it checks
|
|
1063
|
+
* cannot catch anything.
|
|
1064
|
+
*/
|
|
1065
|
+
declare function providedKeyFor(id: string, kind: DeclarationKind, role: string): string;
|
|
1066
|
+
//# sourceMappingURL=naming.d.ts.map
|
|
1067
|
+
//#endregion
|
|
1068
|
+
//#region src/index.d.ts
|
|
20
1069
|
/**
|
|
21
1070
|
* A manifest field is either a flat list or, when the build is partitioned
|
|
22
1071
|
* (e.g. by authorizer), an object keyed by partition name. Readonly-tolerant so
|
|
@@ -37,6 +1086,15 @@ interface RouteInfo {
|
|
|
37
1086
|
memorySize?: number;
|
|
38
1087
|
/** Required environment variables (a trailing `?` marks an optional var). */
|
|
39
1088
|
environment?: readonly string[];
|
|
1089
|
+
/**
|
|
1090
|
+
* The constructs this handler declared an edge to, by id.
|
|
1091
|
+
*
|
|
1092
|
+
* What `.dependsOn()` was given, carried through the build so the manifest
|
|
1093
|
+
* records the edge rather than only its shadow. `environment` is that shadow —
|
|
1094
|
+
* the keys the handler reads — and it cannot be turned back into edges, which
|
|
1095
|
+
* is why both exist and only this one grants anything.
|
|
1096
|
+
*/
|
|
1097
|
+
dependencies?: readonly string[];
|
|
40
1098
|
/** Authorizer name: `none`, `iam`, or a declared authorizer. */
|
|
41
1099
|
authorizer: string;
|
|
42
1100
|
}
|
|
@@ -47,6 +1105,15 @@ interface FunctionInfo {
|
|
|
47
1105
|
timeout?: number;
|
|
48
1106
|
memorySize?: number;
|
|
49
1107
|
environment?: readonly string[];
|
|
1108
|
+
/**
|
|
1109
|
+
* The constructs this handler declared an edge to, by id.
|
|
1110
|
+
*
|
|
1111
|
+
* What `.dependsOn()` was given, carried through the build so the manifest
|
|
1112
|
+
* records the edge rather than only its shadow. `environment` is that shadow —
|
|
1113
|
+
* the keys the handler reads — and it cannot be turned back into edges, which
|
|
1114
|
+
* is why both exist and only this one grants anything.
|
|
1115
|
+
*/
|
|
1116
|
+
dependencies?: readonly string[];
|
|
50
1117
|
}
|
|
51
1118
|
/** A scheduled (cron) function. */
|
|
52
1119
|
interface CronInfo {
|
|
@@ -57,6 +1124,15 @@ interface CronInfo {
|
|
|
57
1124
|
timeout?: number;
|
|
58
1125
|
memorySize?: number;
|
|
59
1126
|
environment?: readonly string[];
|
|
1127
|
+
/**
|
|
1128
|
+
* The constructs this handler declared an edge to, by id.
|
|
1129
|
+
*
|
|
1130
|
+
* What `.dependsOn()` was given, carried through the build so the manifest
|
|
1131
|
+
* records the edge rather than only its shadow. `environment` is that shadow —
|
|
1132
|
+
* the keys the handler reads — and it cannot be turned back into edges, which
|
|
1133
|
+
* is why both exist and only this one grants anything.
|
|
1134
|
+
*/
|
|
1135
|
+
dependencies?: readonly string[];
|
|
60
1136
|
}
|
|
61
1137
|
/** An event subscriber function (topic/queue resolved by `transport`). */
|
|
62
1138
|
interface SubscriberInfo {
|
|
@@ -70,6 +1146,15 @@ interface SubscriberInfo {
|
|
|
70
1146
|
timeout?: number;
|
|
71
1147
|
memorySize?: number;
|
|
72
1148
|
environment?: readonly string[];
|
|
1149
|
+
/**
|
|
1150
|
+
* The constructs this handler declared an edge to, by id.
|
|
1151
|
+
*
|
|
1152
|
+
* What `.dependsOn()` was given, carried through the build so the manifest
|
|
1153
|
+
* records the edge rather than only its shadow. `environment` is that shadow —
|
|
1154
|
+
* the keys the handler reads — and it cannot be turned back into edges, which
|
|
1155
|
+
* is why both exist and only this one grants anything.
|
|
1156
|
+
*/
|
|
1157
|
+
dependencies?: readonly string[];
|
|
73
1158
|
}
|
|
74
1159
|
/**
|
|
75
1160
|
* A pub/sub topic — fan-out. A *resource* (no handler): it declares the event
|
|
@@ -94,6 +1179,12 @@ interface QueueInfo {
|
|
|
94
1179
|
timeout?: number;
|
|
95
1180
|
memorySize?: number;
|
|
96
1181
|
environment?: readonly string[];
|
|
1182
|
+
/**
|
|
1183
|
+
* The constructs the worker declared an edge to, by id.
|
|
1184
|
+
*
|
|
1185
|
+
* See {@link RouteInfo.dependencies} — same field, same reason.
|
|
1186
|
+
*/
|
|
1187
|
+
dependencies?: readonly string[];
|
|
97
1188
|
}
|
|
98
1189
|
/**
|
|
99
1190
|
* The full deployment manifest — the shape of `export const manifest` in a
|
|
@@ -109,6 +1200,7 @@ interface Manifest {
|
|
|
109
1200
|
topics?: ManifestField<TopicInfo>;
|
|
110
1201
|
}
|
|
111
1202
|
//# sourceMappingURL=index.d.ts.map
|
|
1203
|
+
|
|
112
1204
|
//#endregion
|
|
113
|
-
export { CronInfo, FunctionInfo, Manifest, ManifestField, QueueInfo, RouteInfo, SubscriberInfo, TopicInfo, flattenManifestField };
|
|
1205
|
+
export { AllProvidedKeys, AppSpec, CacheDeclaration, ConstructId, ConstructManifest, ConstructName, CredentialDeclaration, CronDeclaration, CronInfo, DEFAULT_APP_CODE, DEFAULT_POSTGRES_VERSION, DERIVES_FROM, DatabaseDeclaration, DatabaseReaderDeclaration, DatabaseSchemaDeclaration, Declaration, DeclarationKind, DeclarationOf, Dependency, DerivedDeclaration, DerivedKind, EmailDeclaration, FileServerDeclaration, Fn, FunctionDeclaration, FunctionInfo, Glob, IdsOf, IdsOfKind, IllegalDerivation, InvalidConstructId, Manifest, ManifestField, Node, ObjectsDeclaration, OidcDeclaration, PUBLIC, PUBLIC_PREFIX, PostgresVersion, ProvidedKeys, Provides, ProvidesByKind, QueueDeclaration, QueueInfo, RestApiDeclaration, RestApiEndpoint, RouteInfo, SecretDeclaration, SiteDeclaration, SubscriberInfo, TopicDeclaration, TopicInfo, UnknownParent, assertDerivations, cacheTable, canonicalId, cloudName, cookieDomain, dependenciesOf, dependentsOf, environmentCase, flattenManifestField, isDerived, kebabCase, provideKey, providedKeyFor, provisionOrder, publicEnvFor, scopedName, serviceKey };
|
|
114
1206
|
//# sourceMappingURL=index.d.cts.map
|