@geekmidas/cloud 1.0.1 → 1.1.0

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.
@@ -0,0 +1,128 @@
1
+ import type { EnvValidator, ValidationResult } from '@geekmidas/envkit/sst';
2
+ import {
3
+ type FunctionInfo,
4
+ flattenManifestField,
5
+ type ManifestField,
6
+ } from '@geekmidas/manifest';
7
+ import { type GkmLinkable, ResourceType } from './Linkable';
8
+ import { LinkedEnvironment } from './LinkedEnvironment';
9
+ import type { StackType } from './Stack';
10
+
11
+ /**
12
+ * `Function` — wraps SST's `sst.aws.Function` with standard env defaults and
13
+ * before-deploy env-var validation.
14
+ *
15
+ * `FunctionProps` extends the native `sst.aws.FunctionArgs`, so every native
16
+ * option (`handler`, `name`, `nodejs`, `vpc`, `url`, `permissions`, …) passes
17
+ * through; the construct only merges env defaults, defaults the runtime to
18
+ * `nodejs24.x` and JSON logging (both overridable), and resolves/validates
19
+ * `link` from the `links` pool against the required `envVars`.
20
+ *
21
+ * Source-only (extends ambient `sst.aws.*`); see docs §2.
22
+ */
23
+ export class Function<
24
+ TStage extends string = string,
25
+ TDomain extends string = string,
26
+ >
27
+ extends sst.aws.Function
28
+ implements GkmLinkable
29
+ {
30
+ readonly _id!: string;
31
+ private readonly validator: EnvValidator;
32
+ private readonly envVars: readonly string[];
33
+
34
+ get _type() {
35
+ return ResourceType.Function;
36
+ }
37
+
38
+ constructor(
39
+ stack: StackType<TStage, TDomain>,
40
+ id: string,
41
+ props: FunctionProps,
42
+ ) {
43
+ const {
44
+ links = [],
45
+ envVars = [],
46
+ autoValidate = true,
47
+ environment,
48
+ runtime,
49
+ logging,
50
+ ...fnArgs
51
+ } = props;
52
+
53
+ const mergedEnvironment = {
54
+ ...LinkedEnvironment.createBaseEnvironment(stack, id),
55
+ ...environment,
56
+ };
57
+
58
+ const linked = new LinkedEnvironment(links, {
59
+ whitelist: Object.keys(mergedEnvironment),
60
+ context: id,
61
+ });
62
+ if (autoValidate) {
63
+ linked.validator.assert(envVars);
64
+ }
65
+
66
+ super(id, {
67
+ ...fnArgs,
68
+ environment: mergedEnvironment,
69
+ // Linking is managed via the `links`/`envVars` flow, so this overrides
70
+ // any native `link` passed through `fnArgs`.
71
+ link: linked.resolveLink(envVars),
72
+ runtime: runtime ?? 'nodejs24.x',
73
+ logging: logging ?? { format: 'json' },
74
+ });
75
+
76
+ this._id = id;
77
+ this.validator = linked.validator;
78
+ this.envVars = envVars;
79
+ }
80
+
81
+ /** Re-runs validation and returns the result (does not throw). */
82
+ validate(): ValidationResult {
83
+ return this.validator.validate(this.envVars);
84
+ }
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
+ }
116
+
117
+ export interface FunctionProps extends sst.aws.FunctionArgs {
118
+ /** Required env vars for this function; validated against `links`. */
119
+ envVars?: readonly string[];
120
+ /** Pool of linkable resources `envVars` are resolved and validated against. */
121
+ links?: GkmLinkable[];
122
+ /**
123
+ * Validate `envVars` against the links in the constructor (fails synth before
124
+ * deploy on a missing variable).
125
+ * @default true
126
+ */
127
+ autoValidate?: boolean;
128
+ }
@@ -0,0 +1,19 @@
1
+ import { ResourceType } from '@geekmidas/envkit/sst';
2
+
3
+ export { ResourceType };
4
+
5
+ /**
6
+ * A linkable resource: an SST component carrying a stable `_id` (its name, used
7
+ * as the environment-variable prefix) and a `_type` drawn from the shared
8
+ * `ResourceType` vocabulary in `@geekmidas/envkit/sst`. The same `_type` values
9
+ * drive the runtime resolvers, so a linked resource resolves to predictable
10
+ * environment variables — and can be validated before deploy (see the validation
11
+ * model in `packages/cloud/docs/sst-constructs.md`).
12
+ *
13
+ * `_type` is the infra-time analogue of the runtime resource's `type`; the Api
14
+ * construct bridges the two when validating (`{ [_id]: { type: _type } }`).
15
+ */
16
+ export interface GkmLinkable {
17
+ readonly _id: string;
18
+ readonly _type: ResourceType;
19
+ }
@@ -0,0 +1,61 @@
1
+ import { EnvValidator, type LinkRecord } from '@geekmidas/envkit/sst';
2
+ import type { GkmLinkable } from './Linkable';
3
+ import type { StackType } from './Stack';
4
+
5
+ /**
6
+ * Shared environment defaults + env-var validation + least-privilege linking for
7
+ * the Lambda-backed constructs — `Function` (once) and `Api` (per route).
8
+ * Bridges infra-time links (`_id`/`_type`) to the runtime resolver shape, builds
9
+ * one `EnvValidator`, and resolves the minimal set of links a required-vars set
10
+ * needs.
11
+ *
12
+ * Used by **composition**, not inheritance: the constructs already extend their
13
+ * SST base component (`sst.aws.Function` / `sst.aws.ApiGatewayV2`), so they hold
14
+ * a `LinkedEnvironment` rather than subclassing a common base.
15
+ */
16
+ export class LinkedEnvironment {
17
+ readonly validator: EnvValidator;
18
+ private readonly linkByName: Map<string, GkmLinkable>;
19
+
20
+ constructor(
21
+ links: GkmLinkable[],
22
+ options: { whitelist: readonly string[]; context?: string },
23
+ ) {
24
+ this.linkByName = new Map(links.map((link) => [link._id, link]));
25
+ const linkRecord: LinkRecord = Object.fromEntries(
26
+ links.map((link) => [link._id, { type: link._type }]),
27
+ );
28
+ this.validator = new EnvValidator(linkRecord, {
29
+ platform: 'aws',
30
+ whitelist: options.whitelist,
31
+ context: options.context,
32
+ });
33
+ }
34
+
35
+ /**
36
+ * The standard environment defaults every Lambda-backed construct injects from
37
+ * its stack. `serviceName` adds `SERVICE_NAME` (functions set it to their id;
38
+ * the API leaves it off). Callers spread their own `environment` over the
39
+ * result, so the user's values always win.
40
+ */
41
+ static createBaseEnvironment<TStage extends string, TDomain extends string>(
42
+ stack: StackType<TStage, TDomain>,
43
+ serviceName?: string,
44
+ ): Record<string, string> {
45
+ return {
46
+ NODE_ENV: 'production',
47
+ ...(serviceName ? { SERVICE_NAME: serviceName } : {}),
48
+ STAGE: stack.stage,
49
+ REGION: stack.region,
50
+ APP_NAME: stack.app.name,
51
+ };
52
+ }
53
+
54
+ /** The link objects that provide at least one of `envVars` (least privilege). */
55
+ resolveLink(envVars: readonly string[]): GkmLinkable[] {
56
+ return this.validator
57
+ .getProvidersForEnvVars(envVars)
58
+ .map((name) => this.linkByName.get(name))
59
+ .filter((link): link is GkmLinkable => link !== undefined);
60
+ }
61
+ }
@@ -0,0 +1,46 @@
1
+ import { type GkmLinkable, ResourceType } from './Linkable';
2
+ import type { StackType } from './Stack';
3
+
4
+ /**
5
+ * `Queue` — a linkable SQS queue (wraps `sst.aws.Queue`), the point-to-point
6
+ * work queue. Link it to a producer and the runtime resolves `<NAME>_URL`,
7
+ * `<NAME>_ARN`, and a `<NAME>_PUBLISHER_CONNECTION_STRING` (`sqs://?queueUrl=…`)
8
+ * that `@geekmidas/events`'s `Publisher.fromConnectionString` consumes. Its
9
+ * single consumer is wired by `QueueSubscriber`.
10
+ *
11
+ * SST's native `Queue` link exposes only `url`, so `getSSTLink` is overridden to
12
+ * also expose `arn` (what the resolver needs). `QueueProps` extends
13
+ * `sst.aws.QueueArgs`. Source-only (extends ambient `sst.aws.*`); see docs §2.
14
+ */
15
+ export class Queue<
16
+ TStage extends string = string,
17
+ TDomain extends string = string,
18
+ >
19
+ extends sst.aws.Queue
20
+ implements GkmLinkable
21
+ {
22
+ readonly _id!: string;
23
+
24
+ get _type() {
25
+ return ResourceType.Queue;
26
+ }
27
+
28
+ constructor(
29
+ _stack: StackType<TStage, TDomain>,
30
+ name: string,
31
+ props: QueueProps = {},
32
+ ) {
33
+ super(name, props);
34
+ this._id = name;
35
+ }
36
+
37
+ override getSSTLink() {
38
+ const link = super.getSSTLink();
39
+ return {
40
+ ...link,
41
+ properties: { ...link.properties, arn: this.arn },
42
+ };
43
+ }
44
+ }
45
+
46
+ export interface QueueProps extends sst.aws.QueueArgs {}
@@ -0,0 +1,55 @@
1
+ import type { App, StageValues } from './App';
2
+
3
+ /**
4
+ * Stack context: an `App` scoped to a logical stack name. Created via
5
+ * `app.stack(name)` (see docs §5). Delegates `stage`/`region`/`domain` to the
6
+ * app and prefixes resource names with the stack name.
7
+ */
8
+ export class Stack<
9
+ TStage extends string = string,
10
+ TDomain extends string = string,
11
+ > {
12
+ constructor(
13
+ readonly app: App<TStage, TDomain>,
14
+ /** This stack's logical name (e.g. `api`), used in resource prefixes. */
15
+ readonly name: string,
16
+ ) {}
17
+
18
+ get stage(): TStage {
19
+ return this.app.stage;
20
+ }
21
+
22
+ get region(): string {
23
+ return this.app.region;
24
+ }
25
+
26
+ get domain(): TDomain {
27
+ return this.app.domain;
28
+ }
29
+
30
+ /**
31
+ * Kebab-cased `{stage}-{appName}-{stackName}-{resource}` physical name.
32
+ * Delegates to the app's scheme (so the two can't drift) and includes the app
33
+ * name so resources stay unique across apps sharing an account/stage.
34
+ */
35
+ logicalPrefixedName(resource: string): string {
36
+ return this.app.logicalPrefixedName(`${this.name}-${resource}`);
37
+ }
38
+
39
+ select<T>(values: StageValues<TStage, T>): T {
40
+ return this.app.select(values);
41
+ }
42
+
43
+ getSubdomain<TSub extends string>(subdomain: TSub) {
44
+ return this.app.getSubdomain(subdomain);
45
+ }
46
+
47
+ getURL<TSub extends string>(subdomain?: TSub) {
48
+ return this.app.getURL(subdomain);
49
+ }
50
+ }
51
+
52
+ export type StackType<
53
+ TStage extends string = string,
54
+ TDomain extends string = string,
55
+ > = Stack<TStage, TDomain>;
@@ -0,0 +1,55 @@
1
+ import { type GkmLinkable, ResourceType } from './Linkable';
2
+ import type { StackType } from './Stack';
3
+
4
+ /**
5
+ * `Storage` — a linkable S3 bucket (wraps `sst.aws.Bucket`). Link it to a
6
+ * `Function`/`Api` route and the runtime resolves a `<NAME>_NAME` environment
7
+ * variable (via the `Bucket` resolver in `@geekmidas/envkit/sst`) holding the
8
+ * bucket's name — exactly what `@geekmidas/storage`'s `AmazonStorageClient`
9
+ * consumes:
10
+ *
11
+ * ```ts
12
+ * // app: a service backed by @geekmidas/storage
13
+ * const storage = {
14
+ * serviceName: 'storage' as const,
15
+ * async register(env) {
16
+ * const { bucket } = env.create((get) => ({
17
+ * bucket: get('UPLOADS_NAME').string(),
18
+ * })).parse();
19
+ * return AmazonStorageClient.create({ bucket });
20
+ * },
21
+ * };
22
+ *
23
+ * // infra: provision + link
24
+ * const uploads = new Storage(stack, 'uploads');
25
+ * new Function(stack, 'Upload', { handler, links: [uploads], envVars: ['UPLOADS_NAME'] });
26
+ * ```
27
+ *
28
+ * The construct id (`uploads`) is the link's `_id`, which becomes the env-var
29
+ * prefix; `StorageProps` extends `sst.aws.BucketArgs` so native options pass
30
+ * through. Source-only (extends ambient `sst.aws.*`); see docs §2.
31
+ */
32
+ export class Storage<
33
+ TStage extends string = string,
34
+ TDomain extends string = string,
35
+ >
36
+ extends sst.aws.Bucket
37
+ implements GkmLinkable
38
+ {
39
+ readonly _id!: string;
40
+
41
+ get _type() {
42
+ return ResourceType.Bucket;
43
+ }
44
+
45
+ constructor(
46
+ _stack: StackType<TStage, TDomain>,
47
+ name: string,
48
+ props: StorageProps = {},
49
+ ) {
50
+ super(name, props);
51
+ this._id = name;
52
+ }
53
+ }
54
+
55
+ export interface StorageProps extends sst.aws.BucketArgs {}
@@ -0,0 +1,37 @@
1
+ import { type GkmLinkable, ResourceType } from './Linkable';
2
+ import type { StackType } from './Stack';
3
+
4
+ /**
5
+ * `Topic` — a linkable SNS topic (wraps `sst.aws.SnsTopic`), the pub/sub fan-out
6
+ * bus. Link it to a publisher and the runtime resolves `<NAME>_ARN` and a
7
+ * `<NAME>_PUBLISHER_CONNECTION_STRING` (`sns://?topicArn=…`) that
8
+ * `@geekmidas/events`'s `Publisher.fromConnectionString` consumes. Subscribers
9
+ * attach via `TopicSubscriber`/`Subscriber`.
10
+ *
11
+ * `StorageProps`-style: `TopicProps` extends `sst.aws.SnsTopicArgs`.
12
+ * Source-only (extends ambient `sst.aws.*`); see docs §2.
13
+ */
14
+ export class Topic<
15
+ TStage extends string = string,
16
+ TDomain extends string = string,
17
+ >
18
+ extends sst.aws.SnsTopic
19
+ implements GkmLinkable
20
+ {
21
+ readonly _id!: string;
22
+
23
+ get _type() {
24
+ return ResourceType.SnsTopic;
25
+ }
26
+
27
+ constructor(
28
+ _stack: StackType<TStage, TDomain>,
29
+ name: string,
30
+ props: TopicProps = {},
31
+ ) {
32
+ super(name, props);
33
+ this._id = name;
34
+ }
35
+ }
36
+
37
+ export interface TopicProps extends sst.aws.SnsTopicArgs {}
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { App } from '../App';
3
+ import { type GkmLinkable, ResourceType } from '../Linkable';
4
+ import { LinkedEnvironment } from '../LinkedEnvironment';
5
+
6
+ // App/Stack are runtime-pure (they reference SST only via erased type
7
+ // annotations), so they can be constructed directly in tests.
8
+ const stack = new App({
9
+ name: 'my-app',
10
+ stage: 'prod',
11
+ domain: 'example.com',
12
+ hostedZoneId: 'Z123',
13
+ region: 'us-east-1',
14
+ }).stack('api');
15
+
16
+ const db: GkmLinkable = { _id: 'db', _type: ResourceType.Postgres };
17
+ const uploads: GkmLinkable = { _id: 'uploads', _type: ResourceType.Bucket };
18
+
19
+ describe('LinkedEnvironment', () => {
20
+ describe('createBaseEnvironment', () => {
21
+ it('builds the stack env defaults (no SERVICE_NAME without a name)', () => {
22
+ expect(LinkedEnvironment.createBaseEnvironment(stack)).toEqual({
23
+ NODE_ENV: 'production',
24
+ STAGE: 'prod',
25
+ REGION: 'us-east-1',
26
+ APP_NAME: 'my-app',
27
+ });
28
+ });
29
+
30
+ it('adds SERVICE_NAME when a service name is given', () => {
31
+ const env = LinkedEnvironment.createBaseEnvironment(stack, 'processor');
32
+ expect(env.SERVICE_NAME).toBe('processor');
33
+ expect(env.APP_NAME).toBe('my-app');
34
+ });
35
+ });
36
+
37
+ describe('validation', () => {
38
+ const linked = new LinkedEnvironment([db, uploads], {
39
+ whitelist: ['APP_NAME'],
40
+ context: 'orders-fn',
41
+ });
42
+
43
+ it('exposes link-derived vars, the platform whitelist, and extras', () => {
44
+ expect(linked.validator.has('DB_HOST')).toBe(true);
45
+ expect(linked.validator.has('UPLOADS_NAME')).toBe(true);
46
+ expect(linked.validator.has('AWS_REGION')).toBe(true); // platform: aws
47
+ expect(linked.validator.has('APP_NAME')).toBe(true); // extra whitelist
48
+ expect(linked.validator.has('NOPE')).toBe(false);
49
+ });
50
+
51
+ it('validates required vars against the links', () => {
52
+ expect(linked.validator.validate(['DB_HOST', 'APP_NAME']).valid).toBe(
53
+ true,
54
+ );
55
+ expect(linked.validator.validate(['MISSING_ONE']).valid).toBe(false);
56
+ });
57
+ });
58
+
59
+ describe('resolveLink (least privilege)', () => {
60
+ const linked = new LinkedEnvironment([db, uploads], {
61
+ whitelist: [],
62
+ });
63
+
64
+ it('attaches only the links that provide a requested var', () => {
65
+ expect(linked.resolveLink(['DB_HOST'])).toEqual([db]);
66
+ });
67
+
68
+ it('attaches multiple links when several are needed', () => {
69
+ expect(linked.resolveLink(['DB_URL', 'UPLOADS_NAME'])).toEqual([
70
+ db,
71
+ uploads,
72
+ ]);
73
+ });
74
+
75
+ it('attaches nothing for a non-link (e.g. platform) var', () => {
76
+ expect(linked.resolveLink(['AWS_REGION'])).toEqual([]);
77
+ });
78
+ });
79
+ });
@@ -0,0 +1,72 @@
1
+ // Type-level tests for the Api authorizer generics. Checked by `ts:check:sst`
2
+ // (it's under src/sst and not a *.spec.ts, so the gate type-checks it; vitest
3
+ // ignores it). Each `@ts-expect-error` self-validates: if the enforcement
4
+ // regresses, the now-unused directive becomes a TS error the gate catches.
5
+
6
+ import { Api } from '../Api';
7
+ import { App } from '../App';
8
+ import { Function } from '../Function';
9
+
10
+ const stack = new App({
11
+ name: 'a',
12
+ stage: 'dev',
13
+ domain: 'example.com',
14
+ hostedZoneId: 'Z',
15
+ region: 'us-east-1',
16
+ }).stack('api');
17
+
18
+ // Valid: built-ins (`iam`/`none`) plus the declared `jwt` and custom names.
19
+ export const ok = new Api(stack, 'Ok', {
20
+ authorizers: {
21
+ jwt: { issuer: 'https://issuer', audiences: ['aud'] },
22
+ employee: { handler: 'src/employee-auth.handler' },
23
+ },
24
+ routes: [
25
+ { method: 'GET', path: '/a', handler: 'a.handler', authorizer: 'iam' },
26
+ { method: 'GET', path: '/b', handler: 'b.handler', authorizer: 'none' },
27
+ { method: 'GET', path: '/c', handler: 'c.handler', authorizer: 'jwt' },
28
+ { method: 'GET', path: '/d', handler: 'd.handler', authorizer: 'employee' },
29
+ { method: 'GET', path: '/e', handler: 'e.handler' },
30
+ ],
31
+ });
32
+
33
+ // An undeclared authorizer name is rejected.
34
+ export const badName = new Api(stack, 'BadName', {
35
+ authorizers: { jwt: { issuer: 'i', audiences: ['a'] } },
36
+ routes: [
37
+ {
38
+ method: 'GET',
39
+ path: '/x',
40
+ handler: 'x.handler',
41
+ // @ts-expect-error 'nope' is not a declared authorizer
42
+ authorizer: 'nope',
43
+ },
44
+ ],
45
+ });
46
+
47
+ // A `jwt` authorizer must supply JWT settings (`audiences` required here).
48
+ export const badJwt = new Api(stack, 'BadJwt', {
49
+ authorizers: {
50
+ // @ts-expect-error jwt requires `audiences`
51
+ jwt: { issuer: 'i' },
52
+ },
53
+ routes: [],
54
+ });
55
+
56
+ // A custom (Lambda) authorizer must supply a `handler`.
57
+ export const badLambda = new Api(stack, 'BadLambda', {
58
+ authorizers: {
59
+ // @ts-expect-error custom authorizer requires `handler`
60
+ employee: { payload: '2.0' },
61
+ },
62
+ routes: [],
63
+ });
64
+
65
+ // A Lambda authorizer `handler` accepts one of our `Function` constructs.
66
+ const authFn = new Function(stack, 'AuthFn', { handler: 'src/auth.handler' });
67
+ export const okFnHandler = new Api(stack, 'OkFn', {
68
+ authorizers: { employee: { handler: authFn } },
69
+ routes: [
70
+ { method: 'GET', path: '/x', handler: 'x.handler', authorizer: 'employee' },
71
+ ],
72
+ });
@@ -0,0 +1,77 @@
1
+ // Type-level checks that the `fromManifest` integrators accept the unified
2
+ // `gkm build` manifest shape (`export const manifest = { … } as const`) — both
3
+ // the flat and the partitioned `ManifestField` forms. Checked by `ts:check:sst`.
4
+
5
+ import type { Manifest, ManifestField, RouteInfo } from '@geekmidas/manifest';
6
+ import { Api } from '../Api';
7
+ import { App } from '../App';
8
+ import { Cron } from '../Cron';
9
+ import { Function } from '../Function';
10
+ import { type GkmLinkable, ResourceType } from '../Linkable';
11
+
12
+ const stack = new App({
13
+ name: 'a',
14
+ stage: 'dev',
15
+ domain: 'example.com',
16
+ hostedZoneId: 'Z',
17
+ region: 'us-east-1',
18
+ }).stack('api');
19
+
20
+ const db: GkmLinkable = { _id: 'db', _type: ResourceType.Postgres };
21
+
22
+ // Mirrors a generated `manifest/aws.ts` — one object, `as const`.
23
+ const manifest = {
24
+ routes: [
25
+ {
26
+ path: '/users/{id}',
27
+ method: 'GET',
28
+ handler: 'users.handler',
29
+ authorizer: 'none',
30
+ environment: ['DB_HOST'],
31
+ timeout: 30,
32
+ memorySize: 1024,
33
+ },
34
+ ],
35
+ functions: [
36
+ { name: 'worker', handler: 'worker.handler', environment: ['DB_URL'] },
37
+ ],
38
+ crons: [
39
+ { name: 'nightly', handler: 'nightly.handler', schedule: 'rate(1 day)' },
40
+ ],
41
+ } as const satisfies Manifest;
42
+
43
+ // Each integrator takes the manifest *field*.
44
+ export const api = Api.fromManifest(stack, 'Api', manifest.routes, {
45
+ links: [db],
46
+ authorizers: { jwt: { issuer: 'https://i', audiences: ['a'] } },
47
+ });
48
+ export const workers = Function.fromManifest(stack, manifest.functions, {
49
+ links: [db],
50
+ });
51
+ export const crons = Cron.fromManifest(stack, manifest.crons, { links: [db] });
52
+
53
+ // The partitioned `ManifestField` form is also accepted.
54
+ const partitionedRoutes = {
55
+ admin: [
56
+ {
57
+ path: '/admin',
58
+ method: 'GET',
59
+ handler: 'admin.handler',
60
+ authorizer: 'iam',
61
+ },
62
+ ],
63
+ default: [
64
+ {
65
+ path: '/health',
66
+ method: 'GET',
67
+ handler: 'health.handler',
68
+ authorizer: 'none',
69
+ },
70
+ ],
71
+ } satisfies ManifestField<RouteInfo>;
72
+ export const partitionedApi = Api.fromManifest(
73
+ stack,
74
+ 'PartApi',
75
+ partitionedRoutes,
76
+ { links: [db] },
77
+ );
@@ -0,0 +1,29 @@
1
+ // Type-level check that Queue/Topic are linkables whose publisher connection
2
+ // strings validate. Checked by `ts:check:sst`; vitest ignores it.
3
+
4
+ import { App } from '../App';
5
+ import { Function } from '../Function';
6
+ import { Queue } from '../Queue';
7
+ import { Topic } from '../Topic';
8
+
9
+ const stack = new App({
10
+ name: 'shop',
11
+ stage: 'dev',
12
+ domain: 'example.com',
13
+ hostedZoneId: 'Z',
14
+ region: 'us-east-1',
15
+ }).stack('events');
16
+
17
+ const orders = new Queue(stack, 'orders');
18
+ const events = new Topic(stack, 'events');
19
+
20
+ // A producer linked to both resolves each resource's publisher connection
21
+ // string (namespaced by name) — env validation passes for both.
22
+ export const producer = new Function(stack, 'Producer', {
23
+ handler: 'producer.handler',
24
+ links: [orders, events],
25
+ envVars: [
26
+ 'ORDERS_PUBLISHER_CONNECTION_STRING',
27
+ 'EVENTS_PUBLISHER_CONNECTION_STRING',
28
+ ],
29
+ });
@@ -0,0 +1,24 @@
1
+ // Type-level check that Storage is a linkable usable in a Function's links.
2
+ // Checked by `ts:check:sst`; vitest ignores it.
3
+
4
+ import { App } from '../App';
5
+ import { Function } from '../Function';
6
+ import { Storage } from '../Storage';
7
+
8
+ const stack = new App({
9
+ name: 'a',
10
+ stage: 'dev',
11
+ domain: 'example.com',
12
+ hostedZoneId: 'Z',
13
+ region: 'us-east-1',
14
+ }).stack('files');
15
+
16
+ const uploads = new Storage(stack, 'uploads');
17
+
18
+ // A Storage is a GkmLinkable, so it can be linked; `UPLOADS_NAME` is the env var
19
+ // its `Bucket` resolver yields and what `@geekmidas/storage` consumes.
20
+ export const upload = new Function(stack, 'Upload', {
21
+ handler: 'upload.handler',
22
+ links: [uploads],
23
+ envVars: ['UPLOADS_NAME'],
24
+ });