@geekmidas/cloud 10.0.0-alpha.36 → 10.0.0-alpha.38

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cloud",
3
- "version": "10.0.0-alpha.36",
3
+ "version": "10.0.0-alpha.38",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -41,8 +41,8 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "lodash.snakecase": "~4.1.1",
44
- "@geekmidas/envkit": "10.0.0-alpha.36",
45
- "@geekmidas/manifest": "10.0.0-alpha.36"
44
+ "@geekmidas/envkit": "10.0.0-alpha.38",
45
+ "@geekmidas/manifest": "10.0.0-alpha.38"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/lodash.get": "~4.4.9",
@@ -58,10 +58,10 @@
58
58
  "@pulumi/aws": ">=7.47.0",
59
59
  "@pulumi/random": ">=4.21.2",
60
60
  "@pulumi/pulumi": ">=3.263.0",
61
- "@geekmidas/manifest": "^10.0.0-alpha.36",
62
- "@geekmidas/storage": "^10.0.0-alpha.36",
63
- "@geekmidas/events": "^10.0.0-alpha.36",
64
- "@geekmidas/db": "^10.0.0-alpha.36"
61
+ "@geekmidas/manifest": "^10.0.0-alpha.38",
62
+ "@geekmidas/storage": "^10.0.0-alpha.38",
63
+ "@geekmidas/events": "^10.0.0-alpha.38",
64
+ "@geekmidas/db": "^10.0.0-alpha.38"
65
65
  },
66
66
  "peerDependenciesMeta": {
67
67
  "@geekmidas/manifest": {
@@ -1,5 +1,5 @@
1
1
  import { resolveEnvKeys } from '@geekmidas/envkit/sst';
2
- import { providedKeyFor, provideKey } from '@geekmidas/manifest';
2
+ import { NoUrlForStage, providedKeyFor, provideKey } from '@geekmidas/manifest';
3
3
  import { describe, expect, it } from 'vitest';
4
4
  import { ObjectStorage } from '../aws/ObjectStorage';
5
5
  import { type ProvidesMismatch, UnknownDeclarationKind } from '../errors';
@@ -249,18 +249,91 @@ describe('the key a provided role becomes', () => {
249
249
  describe('Credential', () => {
250
250
  it('provides under the role the declaration named', async () => {
251
251
  // The role *is* the contract: providing `value` against a declared
252
- // `STRIPE_CREDENTIAL` supplies `STRIPE_VALUE`, and assertProvides
252
+ // `STRIPE_CREDENTIALS` supplies `STRIPE_VALUE`, and assertProvides
253
253
  // rejects the stack at synth. Which is the check working.
254
254
  const { Credential } = await import('../aws/Credential');
255
255
 
256
256
  expect(
257
257
  Object.keys(new Credential({} as never, 'Stripe').provides()),
258
- ).toEqual(['credential']);
258
+ ).toEqual(['credentials']);
259
259
  });
260
260
 
261
261
  it('agrees with what the construct declares', () => {
262
- expect(providedKeyFor('Stripe', 'credential', 'credential')).toBe(
263
- 'STRIPE_CREDENTIAL',
262
+ expect(providedKeyFor('Stripe', 'credential', 'credentials')).toBe(
263
+ 'STRIPE_CREDENTIALS',
264
264
  );
265
265
  });
266
+
267
+ it('links under the key the construct reads, not the secret’s bare name', async () => {
268
+ const { Credential } = await import('../aws/Credential');
269
+ const stripe = new Credential({} as never, 'Stripe');
270
+
271
+ expect(resolveEnvKeys({ Stripe: { type: stripe._type } })).toEqual([
272
+ 'STRIPE_CREDENTIALS',
273
+ ]);
274
+ expect(stripe.getSSTLink().properties).toEqual(stripe.provides());
275
+ });
276
+
277
+ it('is what a function depending on it is linked to', async () => {
278
+ const { Credential } = await import('../aws/Credential');
279
+ const stripe = new Credential({} as never, 'Stripe');
280
+
281
+ const { link, envKeys } = resolveEdges(
282
+ [{ target: 'Stripe', kind: 'credential' }],
283
+ { Stripe: stripe },
284
+ );
285
+
286
+ expect(link).toEqual([stripe]);
287
+ expect(envKeys).toEqual(['STRIPE_CREDENTIALS']);
288
+ });
289
+ });
290
+
291
+ describe('ExternalApi', () => {
292
+ const stack = (stage: string) => ({ stage }) as never;
293
+
294
+ it('provides the URL for its stage and its credentials, under the declared keys', async () => {
295
+ const { ExternalApi } = await import('../aws/ExternalApi');
296
+ const payfast = new ExternalApi(stack('production'), 'PayFast', {
297
+ url: {
298
+ production: 'https://www.payfast.co.za',
299
+ default: 'https://sandbox.payfast.co.za',
300
+ },
301
+ });
302
+
303
+ expect(payfast.provides().url).toBe('https://www.payfast.co.za');
304
+ expect(
305
+ Object.keys(payfast.provides()).map((role) =>
306
+ providedKeyFor('PayFast', 'external-api', role),
307
+ ),
308
+ ).toEqual(['PAY_FAST_URL', 'PAY_FAST_CREDENTIALS']);
309
+ });
310
+
311
+ it('falls back to the default URL for a stage it does not name', async () => {
312
+ const { ExternalApi } = await import('../aws/ExternalApi');
313
+ const payfast = new ExternalApi(stack('staging'), 'PayFast', {
314
+ url: {
315
+ production: 'https://www.payfast.co.za',
316
+ default: 'https://sandbox.payfast.co.za',
317
+ },
318
+ });
319
+
320
+ expect(payfast.provides().url).toBe('https://sandbox.payfast.co.za');
321
+ });
322
+
323
+ it('fails synth for a stage it has no URL for', async () => {
324
+ const { ExternalApi } = await import('../aws/ExternalApi');
325
+
326
+ expect(
327
+ () =>
328
+ new ExternalApi(stack('staging'), 'PayFast', {
329
+ url: { production: 'https://www.payfast.co.za' },
330
+ }),
331
+ ).toThrow(NoUrlForStage);
332
+ });
333
+
334
+ it('flattens into the same keys at runtime', () => {
335
+ expect(
336
+ resolveEnvKeys({ PayFast: { type: 'gkm:aws:ExternalApi' } }).sort(),
337
+ ).toEqual(['PAY_FAST_CREDENTIALS', 'PAY_FAST_URL']);
338
+ });
266
339
  });
@@ -1,4 +1,5 @@
1
- import { Secret, type SecretProps } from './Secret';
1
+ import { type GkmLinkable, ResourceType } from '../Linkable';
2
+ import type { StackType } from '../Stack';
2
3
 
3
4
  /**
4
5
  * `Credential` — a third-party credential SST holds, and the infra half of the
@@ -8,21 +9,53 @@ import { Secret, type SecretProps } from './Secret';
8
9
  * the lifecycle rather than the mechanism: a secret is generated and rotated by
9
10
  * the platform, while a credential is issued by someone else and has a shape
10
11
  * the construct validates on the way in. Both are values you set out of band
11
- * with `sst secret set`.
12
+ * with `sst secret set`, under the construct's id.
12
13
  *
13
- * The role is `credential` rather than `value`, and that is not cosmetic. The
14
+ * The role is `credentials` rather than `value`, and that is not cosmetic. The
14
15
  * role *is* the contract — `providedKeyFor` turns it into the key the app
15
16
  * declared — so a credential providing `value` would supply `STRIPE_VALUE`
16
- * against a declared `STRIPE_CREDENTIAL`, and `assertProvides` would reject the
17
- * stack at synth. Which is the check working; renaming here is the fix.
17
+ * against a declared `STRIPE_CREDENTIALS`, and `assertProvides` would reject
18
+ * the stack at synth.
19
+ *
20
+ * It holds an `sst.Secret` rather than being one, and reports its own type,
21
+ * for the same reason one level down: the type decides the keys a link
22
+ * resolves to. As an SST secret it resolved to `STRIPE`, so a function that
23
+ * declared `STRIPE_CREDENTIALS` was linked to nothing.
18
24
  */
19
25
  export class Credential<
20
26
  TStage extends string = string,
21
27
  TDomain extends string = string,
22
- > extends Secret<TStage, TDomain> {
23
- override provides(): Record<string, $util.Input<string>> {
24
- return { credential: this.value };
28
+ > implements GkmLinkable
29
+ {
30
+ readonly _id: string;
31
+
32
+ private readonly secret: sst.Secret;
33
+
34
+ get _type() {
35
+ return ResourceType.Credential;
36
+ }
37
+
38
+ constructor(
39
+ _stack: StackType<TStage, TDomain>,
40
+ name: string,
41
+ props: CredentialProps = {},
42
+ ) {
43
+ this._id = name;
44
+ // The id itself, as it always was: a value already set with
45
+ // `sst secret set Stripe …` is still the one read.
46
+ this.secret = new sst.Secret(name, props.placeholder);
47
+ }
48
+
49
+ provides(): Record<string, $util.Input<string>> {
50
+ return { credentials: this.secret.value };
51
+ }
52
+
53
+ getSSTLink() {
54
+ return { properties: { ...this.provides() } };
25
55
  }
26
56
  }
27
57
 
28
- export interface CredentialProps extends SecretProps {}
58
+ export interface CredentialProps {
59
+ /** A value until one is set, for a preview stage. */
60
+ placeholder?: string;
61
+ }
@@ -0,0 +1,58 @@
1
+ import { externalApiUrl } from '@geekmidas/manifest';
2
+ import { type GkmLinkable, ResourceType } from '../Linkable';
3
+ import type { StackType } from '../Stack';
4
+
5
+ /**
6
+ * `ExternalApi` — the infra half of the `external-api` kind.
7
+ *
8
+ * Nothing is provisioned: the API is somebody else's. What this holds is the
9
+ * two facts a function needs to call it on this stage — the URL the
10
+ * declaration names for it, and the credentials the provider issued, kept as
11
+ * an SST secret and set out of band:
12
+ *
13
+ * ```
14
+ * sst secret set PolarCredentials '{"clientId":"…","clientSecret":"…"}' --stage prod
15
+ * ```
16
+ */
17
+ export class ExternalApi<
18
+ TStage extends string = string,
19
+ TDomain extends string = string,
20
+ > implements GkmLinkable
21
+ {
22
+ readonly _id: string;
23
+
24
+ private readonly url: string;
25
+
26
+ private readonly credentials: sst.Secret;
27
+
28
+ get _type() {
29
+ return ResourceType.ExternalApi;
30
+ }
31
+
32
+ constructor(
33
+ stack: StackType<TStage, TDomain>,
34
+ name: string,
35
+ props: ExternalApiProps,
36
+ ) {
37
+ this._id = name;
38
+ // Read at synth, so a stage the declaration has no URL for fails the
39
+ // deploy — with the stage named — rather than the first request.
40
+ this.url = externalApiUrl({ id: name, url: props.url }, stack.stage);
41
+ this.credentials = new sst.Secret(`${name}Credentials`, props.placeholder);
42
+ }
43
+
44
+ provides(): Record<string, $util.Input<string>> {
45
+ return { url: this.url, credentials: this.credentials.value };
46
+ }
47
+
48
+ getSSTLink() {
49
+ return { properties: { ...this.provides() } };
50
+ }
51
+ }
52
+
53
+ export interface ExternalApiProps {
54
+ /** The declaration's `url` — one for every stage, or one per stage name. */
55
+ url: string | Readonly<Record<string, string>>;
56
+ /** A value for the credentials secret until one is set, for a preview stage. */
57
+ placeholder?: string;
58
+ }
@@ -45,6 +45,7 @@ import { Database, DatabaseNeedsVpc, type DatabaseProps } from './aws/Database';
45
45
  import { DatabaseBootstrap } from './aws/DatabaseBootstrap';
46
46
  import { DatabaseReader, DatabaseSchema } from './aws/DerivedDatabase';
47
47
  import { Email, EmailNeedsSender } from './aws/Email';
48
+ import { ExternalApi, type ExternalApiProps } from './aws/ExternalApi';
48
49
  import { FileServer } from './aws/FileServer';
49
50
  import { ObjectStorage } from './aws/ObjectStorage';
50
51
  import { Queue } from './aws/Queue';
@@ -399,6 +400,19 @@ const PROVISIONERS: Partial<Record<DeclarationKind, Provisioner>> = {
399
400
 
400
401
  // The same storage as a secret, under a different role — see the component.
401
402
  credential: (stack, d, props) => new Credential(stack, d.id, props),
403
+
404
+ // Nothing to provision: the URL this stage calls, and the credentials kept
405
+ // as a secret.
406
+ 'external-api': (stack, d, props) => {
407
+ if (d.kind !== 'external-api') {
408
+ throw new UnknownDeclarationKind(d.kind, []);
409
+ }
410
+
411
+ return new ExternalApi(stack, d.id, {
412
+ ...(props as Partial<ExternalApiProps>),
413
+ url: d.url,
414
+ });
415
+ },
402
416
  };
403
417
 
404
418
  /**