@rc312/effect-aws-imds 0.0.0-stage → 0.2.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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -2
  3. package/dist/aws-imds.d.ts +58 -0
  4. package/dist/aws-imds.d.ts.map +1 -0
  5. package/dist/aws-imds.js +65 -0
  6. package/dist/aws-imds.js.map +1 -0
  7. package/dist/config.d.ts +64 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +37 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/identity-document.d.ts +33 -0
  12. package/dist/identity-document.d.ts.map +1 -0
  13. package/dist/identity-document.js +58 -0
  14. package/dist/identity-document.js.map +1 -0
  15. package/dist/index.d.ts +48 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +48 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/instance-identity.d.ts +43 -0
  20. package/dist/instance-identity.d.ts.map +1 -0
  21. package/dist/instance-identity.js +38 -0
  22. package/dist/instance-identity.js.map +1 -0
  23. package/dist/internal.d.ts +11 -0
  24. package/dist/internal.d.ts.map +1 -0
  25. package/dist/internal.js +15 -0
  26. package/dist/internal.js.map +1 -0
  27. package/dist/layer.d.ts +25 -0
  28. package/dist/layer.d.ts.map +1 -0
  29. package/dist/layer.js +26 -0
  30. package/dist/layer.js.map +1 -0
  31. package/dist/metadata-error.d.ts +22 -0
  32. package/dist/metadata-error.d.ts.map +1 -0
  33. package/dist/metadata-error.js +19 -0
  34. package/dist/metadata-error.js.map +1 -0
  35. package/dist/token-error.d.ts +19 -0
  36. package/dist/token-error.d.ts.map +1 -0
  37. package/dist/token-error.js +16 -0
  38. package/dist/token-error.js.map +1 -0
  39. package/dist/token.d.ts +27 -0
  40. package/dist/token.d.ts.map +1 -0
  41. package/dist/token.js +53 -0
  42. package/dist/token.js.map +1 -0
  43. package/dist/transport.d.ts +26 -0
  44. package/dist/transport.d.ts.map +1 -0
  45. package/dist/transport.js +46 -0
  46. package/dist/transport.js.map +1 -0
  47. package/package.json +42 -4
  48. package/src/aws-imds.ts +118 -0
  49. package/src/config.ts +96 -0
  50. package/src/identity-document.ts +86 -0
  51. package/src/index.ts +66 -0
  52. package/src/instance-identity.ts +63 -0
  53. package/src/internal.ts +21 -0
  54. package/src/layer.ts +36 -0
  55. package/src/metadata-error.ts +21 -0
  56. package/src/token-error.ts +18 -0
  57. package/src/token.ts +114 -0
  58. package/src/transport.ts +101 -0
@@ -0,0 +1,43 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { AwsImds } from "./aws-imds.ts";
3
+ import type { AwsImdsError } from "./aws-imds.ts";
4
+ import { IdentityDocument } from "./identity-document.ts";
5
+ declare const InstanceIdentity_base: Context.ServiceClass<InstanceIdentity, "@rc312/effect-aws-imds/InstanceIdentity", {
6
+ readonly document: Effect.Effect<IdentityDocument, AwsImdsError>;
7
+ readonly invalidate: Effect.Effect<void>;
8
+ }>;
9
+ /**
10
+ * The decoded instance identity document, memoized after the first success.
11
+ * Failures are not cached. Provide a fixed document with `Layer.succeed` to
12
+ * test code that depends on it without any HTTP.
13
+ *
14
+ * @since 0.1.0
15
+ */
16
+ declare class InstanceIdentity extends InstanceIdentity_base {
17
+ /** Reads the document through `AwsImds`. */
18
+ static readonly layer: Layer.Layer<InstanceIdentity, never, AwsImds>;
19
+ }
20
+ /**
21
+ * The memoized, decoded instance identity document.
22
+ *
23
+ * @since 0.1.0
24
+ */
25
+ declare const identityDocument: Effect.Effect<{
26
+ readonly accountId: string;
27
+ readonly architecture: "arm64" | "i386" | "x86_64";
28
+ readonly availabilityZone: string;
29
+ readonly billingProducts?: readonly string[] | null | undefined;
30
+ readonly devpayProductCodes?: readonly string[] | null | undefined;
31
+ readonly imageId: string;
32
+ readonly instanceId: string;
33
+ readonly instanceType: string;
34
+ readonly kernelId?: string | null | undefined;
35
+ readonly marketplaceProductCodes?: readonly string[] | null | undefined;
36
+ readonly pendingTime: Date;
37
+ readonly privateIp: string;
38
+ readonly ramdiskId?: string | null | undefined;
39
+ readonly region: string;
40
+ readonly version: string;
41
+ }, AwsImdsError, InstanceIdentity>;
42
+ export { identityDocument, InstanceIdentity };
43
+ //# sourceMappingURL=instance-identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance-identity.d.ts","sourceRoot":"","sources":["../src/instance-identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,OAAO,EAAY,MAAM,EAAE,KAAK,EAAU,MAAM,QAAQ,CAAC;AAEzE,OAAO,EAAE,OAAO,EAAY,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;;uBAoBnC,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,YAAY,CAAC;yBAC3C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;;AAX5C;;;;;;GAMG;AACH,cAAM,gBAAiB,SAAQ,qBAMe;IAC5C,4CAA4C;IAC5C,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,gBAAgB,EAAE,KAAK,EAAE,OAAO,CAAC,CAuBhE;CACL;AAED;;;;GAIG;AACH,QAAA,MAAM,gBAAgB;;;;;;;;;;;;;;;;kCAAwD,CAAC;AAE/E,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAC"}
@@ -0,0 +1,38 @@
1
+ import { Cache, Context, Duration, Effect, Layer, Schema } from "effect";
2
+ import { AwsImds, decodeAt } from "./aws-imds.js";
3
+ import { IdentityDocument } from "./identity-document.js";
4
+ import { cacheOnSuccess } from "./internal.js";
5
+ const IDENTITY_DOCUMENT_PATH = "/latest/dynamic/instance-identity/document";
6
+ const decodeDocument = decodeAt(IDENTITY_DOCUMENT_PATH, Schema.fromJsonString(IdentityDocument));
7
+ /**
8
+ * The decoded instance identity document, memoized after the first success.
9
+ * Failures are not cached. Provide a fixed document with `Layer.succeed` to
10
+ * test code that depends on it without any HTTP.
11
+ *
12
+ * @since 0.1.0
13
+ */
14
+ class InstanceIdentity extends Context.Service()("@rc312/effect-aws-imds/InstanceIdentity") {
15
+ /** Reads the document through `AwsImds`. */
16
+ static layer = Layer.effect(InstanceIdentity, Effect.gen(function* make() {
17
+ const imds = yield* AwsImds;
18
+ const resolve = imds
19
+ .get(IDENTITY_DOCUMENT_PATH)
20
+ .pipe(Effect.flatMap(decodeDocument), Effect.withSpan("InstanceIdentity.resolve"));
21
+ const documents = yield* Cache.makeWith(() => resolve, {
22
+ capacity: 1,
23
+ timeToLive: cacheOnSuccess(Duration.infinity),
24
+ });
25
+ return InstanceIdentity.of({
26
+ document: Cache.get(documents, IDENTITY_DOCUMENT_PATH),
27
+ invalidate: Cache.invalidate(documents, IDENTITY_DOCUMENT_PATH),
28
+ });
29
+ }));
30
+ }
31
+ /**
32
+ * The memoized, decoded instance identity document.
33
+ *
34
+ * @since 0.1.0
35
+ */
36
+ const identityDocument = InstanceIdentity.use((identity) => identity.document);
37
+ export { identityDocument, InstanceIdentity };
38
+ //# sourceMappingURL=instance-identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance-identity.js","sourceRoot":"","sources":["../src/instance-identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEzE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,sBAAsB,GAAG,4CAA4C,CAAC;AAE5E,MAAM,cAAc,GAAG,QAAQ,CAC7B,sBAAsB,EACtB,MAAM,CAAC,cAAc,CAAC,gBAAgB,CAAC,CACxC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,gBAAiB,SAAQ,OAAO,CAAC,OAAO,EAM3C,CAAC,yCAAyC,CAAC;IAC5C,4CAA4C;IAC5C,MAAM,CAAU,KAAK,GACnB,KAAK,CAAC,MAAM,CACV,gBAAgB,EAChB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI;QACvB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC;QAE5B,MAAM,OAAO,GAAG,IAAI;aACjB,GAAG,CAAC,sBAAsB,CAAC;aAC3B,IAAI,CACH,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,EAC9B,MAAM,CAAC,QAAQ,CAAC,0BAA0B,CAAC,CAC5C,CAAC;QAEJ,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE;YACrD,QAAQ,EAAE,CAAC;YACX,UAAU,EAAE,cAAc,CAAC,QAAQ,CAAC,QAAQ,CAAC;SAC9C,CAAC,CAAC;QAEH,OAAO,gBAAgB,CAAC,EAAE,CAAC;YACzB,QAAQ,EAAE,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,sBAAsB,CAAC;YACtD,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,SAAS,EAAE,sBAAsB,CAAC;SAChE,CAAC,CAAC;IACL,CAAC,CAAC,CACH,CAAC;CACL;AAED;;;;GAIG;AACH,MAAM,gBAAgB,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAE/E,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { Duration, Exit } from "effect";
2
+ declare const failureDetails: (cause: Error) => {
3
+ message: string;
4
+ status?: never;
5
+ } | {
6
+ message: string;
7
+ status: number;
8
+ };
9
+ declare const cacheOnSuccess: (ttl: Duration.Duration) => <A, E>(exit: Exit.Exit<A, E>) => Duration.Duration;
10
+ export { cacheOnSuccess, failureDetails };
11
+ //# sourceMappingURL=internal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAIxC,QAAA,MAAM,cAAc,UAAW,KAAK;;;;;;CAQnC,CAAC;AAGF,QAAA,MAAM,cAAc,QACZ,QAAQ,CAAC,QAAQ,MACtB,CAAC,EAAE,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,sBACgB,CAAC;AAE/C,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { Duration, Exit } from "effect";
2
+ import { HttpClientError } from "effect/http";
3
+ // Keep only the message and status: client errors retain the request headers.
4
+ const failureDetails = (cause) => {
5
+ const status = HttpClientError.isHttpClientError(cause)
6
+ ? cause.response?.status
7
+ : undefined;
8
+ return status === undefined
9
+ ? { message: cause.message }
10
+ : { message: cause.message, status };
11
+ };
12
+ // `Effect.cachedWithTTL` would also cache failures; IMDS failures are transient.
13
+ const cacheOnSuccess = (ttl) => (exit) => Exit.isSuccess(exit) ? ttl : Duration.zero;
14
+ export { cacheOnSuccess, failureDetails };
15
+ //# sourceMappingURL=internal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal.js","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,8EAA8E;AAC9E,MAAM,cAAc,GAAG,CAAC,KAAY,EAAE,EAAE;IACtC,MAAM,MAAM,GAAG,eAAe,CAAC,iBAAiB,CAAC,KAAK,CAAC;QACrD,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM;QACxB,CAAC,CAAC,SAAS,CAAC;IAEd,OAAO,MAAM,KAAK,SAAS;QACzB,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE;QAC5B,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;AACzC,CAAC,CAAC;AAEF,iFAAiF;AACjF,MAAM,cAAc,GAClB,CAAC,GAAsB,EAAE,EAAE,CAC3B,CAAO,IAAqB,EAAE,EAAE,CAC9B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;AAE/C,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { AwsImds } from "./aws-imds.ts";
3
+ import { InstanceIdentity } from "./instance-identity.ts";
4
+ import { AwsImdsToken } from "./token.ts";
5
+ /**
6
+ * Provides `InstanceIdentity`, `AwsImds`, and `AwsImdsToken` from the
7
+ * `HttpClient` in the environment. Settings come from `AwsImdsConfig`.
8
+ *
9
+ * @since 0.1.0
10
+ */
11
+ declare const layer: Layer.Layer<AwsImds | AwsImdsToken | InstanceIdentity, never, import("effect/http/HttpClient").HttpClient>;
12
+ /**
13
+ * `layer` backed by the platform `fetch` through `FetchHttpClient`.
14
+ *
15
+ * @since 0.1.0
16
+ */
17
+ declare const layerFetch: Layer.Layer<AwsImds | AwsImdsToken | InstanceIdentity, never, never>;
18
+ /**
19
+ * Discards the cached session token and identity document.
20
+ *
21
+ * @since 0.1.0
22
+ */
23
+ declare const clearCache: Effect.Effect<void, never, AwsImdsToken | InstanceIdentity>;
24
+ export { clearCache, layer, layerFetch };
25
+ //# sourceMappingURL=layer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layer.d.ts","sourceRoot":"","sources":["../src/layer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAGvC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C;;;;;GAKG;AACH,QAAA,MAAM,KAAK,4GAGV,CAAC;AAEF;;;;GAIG;AACH,QAAA,MAAM,UAAU,sEAAmD,CAAC;AAEpE;;;;GAIG;AACH,QAAA,MAAM,UAAU,6DAGf,CAAC;AAEF,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC"}
package/dist/layer.js ADDED
@@ -0,0 +1,26 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { FetchHttpClient } from "effect/http";
3
+ import { AwsImds } from "./aws-imds.js";
4
+ import { InstanceIdentity } from "./instance-identity.js";
5
+ import { AwsImdsToken } from "./token.js";
6
+ /**
7
+ * Provides `InstanceIdentity`, `AwsImds`, and `AwsImdsToken` from the
8
+ * `HttpClient` in the environment. Settings come from `AwsImdsConfig`.
9
+ *
10
+ * @since 0.1.0
11
+ */
12
+ const layer = InstanceIdentity.layer.pipe(Layer.provideMerge(AwsImds.layer), Layer.provideMerge(AwsImdsToken.layer));
13
+ /**
14
+ * `layer` backed by the platform `fetch` through `FetchHttpClient`.
15
+ *
16
+ * @since 0.1.0
17
+ */
18
+ const layerFetch = layer.pipe(Layer.provide(FetchHttpClient.layer));
19
+ /**
20
+ * Discards the cached session token and identity document.
21
+ *
22
+ * @since 0.1.0
23
+ */
24
+ const clearCache = Effect.andThen(AwsImdsToken.use((token) => token.clear), InstanceIdentity.use((identity) => identity.invalidate));
25
+ export { clearCache, layer, layerFetch };
26
+ //# sourceMappingURL=layer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layer.js","sourceRoot":"","sources":["../src/layer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C;;;;;GAKG;AACH,MAAM,KAAK,GAAG,gBAAgB,CAAC,KAAK,CAAC,IAAI,CACvC,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,EACjC,KAAK,CAAC,YAAY,CAAC,YAAY,CAAC,KAAK,CAAC,CACvC,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC;AAEpE;;;;GAIG;AACH,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAC/B,YAAY,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EACxC,gBAAgB,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CACxD,CAAC;AAEF,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { Schema } from "effect";
2
+ declare const AwsImdsMetadataError_base: Schema.Class<AwsImdsMetadataError, Schema.TaggedStruct<"AwsImdsMetadataError", {
3
+ readonly message: Schema.String;
4
+ readonly path: Schema.String;
5
+ readonly status: Schema.optional<Schema.Int>;
6
+ }>, import("effect/Cause").YieldableError & {
7
+ readonly brand: unique symbol;
8
+ }>;
9
+ /**
10
+ * Failure to read or decode the metadata at `path`.
11
+ *
12
+ * `status` is the HTTP status when IMDS answered with a non-2xx response, for
13
+ * example `404` for a path that does not exist on this instance. Transport,
14
+ * timeout, and decoding failures have no `status`. The underlying request is
15
+ * not retained because its headers carry the session token.
16
+ *
17
+ * @since 0.1.0
18
+ */
19
+ export declare class AwsImdsMetadataError extends AwsImdsMetadataError_base {
20
+ }
21
+ export {};
22
+ //# sourceMappingURL=metadata-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata-error.d.ts","sourceRoot":"","sources":["../src/metadata-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;;;;;;oBAeZ,OAAO,MAAM;;AAbjC;;;;;;;;;GASG;AAEH,qBAAa,oBAAqB,SAAQ,yBAOxC;CAAG"}
@@ -0,0 +1,19 @@
1
+ import { Schema } from "effect";
2
+ /**
3
+ * Failure to read or decode the metadata at `path`.
4
+ *
5
+ * `status` is the HTTP status when IMDS answered with a non-2xx response, for
6
+ * example `404` for a path that does not exist on this instance. Transport,
7
+ * timeout, and decoding failures have no `status`. The underlying request is
8
+ * not retained because its headers carry the session token.
9
+ *
10
+ * @since 0.1.0
11
+ */
12
+ // oxlint-disable-next-line unicorn/throw-new-error -- TaggedError is a class factory.
13
+ export class AwsImdsMetadataError extends Schema.TaggedError()("AwsImdsMetadataError", {
14
+ message: Schema.String,
15
+ path: Schema.String,
16
+ status: Schema.optional(Schema.Int),
17
+ }) {
18
+ }
19
+ //# sourceMappingURL=metadata-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata-error.js","sourceRoot":"","sources":["../src/metadata-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC;;;;;;;;;GASG;AACH,sFAAsF;AACtF,MAAM,OAAO,oBAAqB,SAAQ,MAAM,CAAC,WAAW,EAGzD,CAAC,sBAAsB,EAAE;IAC1B,OAAO,EAAE,MAAM,CAAC,MAAM;IACtB,IAAI,EAAE,MAAM,CAAC,MAAM;IACnB,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC;CACpC,CAAC;CAAG"}
@@ -0,0 +1,19 @@
1
+ import { Schema } from "effect";
2
+ declare const AwsImdsTokenError_base: Schema.Class<AwsImdsTokenError, Schema.TaggedStruct<"AwsImdsTokenError", {
3
+ readonly message: Schema.String;
4
+ readonly status: Schema.optional<Schema.Int>;
5
+ }>, import("effect/Cause").YieldableError & {
6
+ readonly brand: unique symbol;
7
+ }>;
8
+ /**
9
+ * Failure to obtain an IMDSv2 session token.
10
+ *
11
+ * `status` is the HTTP status when IMDS answered the token request with a
12
+ * non-2xx response. Transport, timeout, and decoding failures have no `status`.
13
+ *
14
+ * @since 0.1.0
15
+ */
16
+ export declare class AwsImdsTokenError extends AwsImdsTokenError_base {
17
+ }
18
+ export {};
19
+ //# sourceMappingURL=token-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-error.d.ts","sourceRoot":"","sources":["../src/token-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;;;;;oBAaZ,OAAO,MAAM;;AAXjC;;;;;;;GAOG;AAEH,qBAAa,iBAAkB,SAAQ,sBAMrC;CAAG"}
@@ -0,0 +1,16 @@
1
+ import { Schema } from "effect";
2
+ /**
3
+ * Failure to obtain an IMDSv2 session token.
4
+ *
5
+ * `status` is the HTTP status when IMDS answered the token request with a
6
+ * non-2xx response. Transport, timeout, and decoding failures have no `status`.
7
+ *
8
+ * @since 0.1.0
9
+ */
10
+ // oxlint-disable-next-line unicorn/throw-new-error -- TaggedError is a class factory.
11
+ export class AwsImdsTokenError extends Schema.TaggedError()("AwsImdsTokenError", {
12
+ message: Schema.String,
13
+ status: Schema.optional(Schema.Int),
14
+ }) {
15
+ }
16
+ //# sourceMappingURL=token-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-error.js","sourceRoot":"","sources":["../src/token-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC;;;;;;;GAOG;AACH,sFAAsF;AACtF,MAAM,OAAO,iBAAkB,SAAQ,MAAM,CAAC,WAAW,EAGtD,CAAC,mBAAmB,EAAE;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM;IACtB,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC;CACpC,CAAC;CAAG"}
@@ -0,0 +1,27 @@
1
+ import { Context, Effect, Layer, Redacted } from "effect";
2
+ import { HttpClient } from "effect/http";
3
+ import { AwsImdsTokenError } from "./token-error.ts";
4
+ declare const AwsImdsToken_base: Context.ServiceClass<AwsImdsToken, "@rc312/effect-aws-imds/AwsImdsToken", {
5
+ readonly get: Effect.Effect<Redacted.Redacted<string>, AwsImdsTokenError>;
6
+ readonly invalidate: (rejected: Redacted.Redacted<string>) => Effect.Effect<void>;
7
+ readonly clear: Effect.Effect<void>;
8
+ }>;
9
+ /**
10
+ * IMDSv2 session tokens.
11
+ *
12
+ * - `get` returns the cached token, requesting one with `PUT /latest/api/token`
13
+ * when none is cached or the cached one is close to expiry.
14
+ * - `invalidate(token)` discards `token` if it is still the cached one, so
15
+ * concurrent reads that were all rejected trigger a single refresh.
16
+ * - `clear` discards the cached token unconditionally.
17
+ *
18
+ * Provide your own implementation with `Layer.succeed` to use a fixed token.
19
+ *
20
+ * @since 0.1.0
21
+ */
22
+ declare class AwsImdsToken extends AwsImdsToken_base {
23
+ /** Requests tokens through the `HttpClient`, configured by `AwsImdsConfig`. */
24
+ static readonly layer: Layer.Layer<AwsImdsToken, never, HttpClient.HttpClient>;
25
+ }
26
+ export { AwsImdsToken };
27
+ //# sourceMappingURL=token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,OAAO,EAEP,MAAM,EACN,KAAK,EAEL,QAAQ,EAET,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAIzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;;kBA6CnC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,iBAAiB,CAAC;yBACpD,CACnB,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,KAChC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;oBACR,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;;AApBvC;;;;;;;;;;;;GAYG;AACH,cAAM,YAAa,SAAQ,iBASe;IACxC,+EAA+E;IAC/E,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAChC,YAAY,EACZ,KAAK,EACL,UAAU,CAAC,UAAU,CACtB,CAuCC;CACH;AAED,OAAO,EAAE,YAAY,EAAE,CAAC"}
package/dist/token.js ADDED
@@ -0,0 +1,53 @@
1
+ import { Cache, Context, Duration, Effect, Layer, Option, Redacted, Schema, } from "effect";
2
+ import { HttpClient } from "effect/http";
3
+ import { AwsImdsConfig } from "./config.js";
4
+ import { cacheOnSuccess, failureDetails } from "./internal.js";
5
+ import { AwsImdsTokenError } from "./token-error.js";
6
+ import { imdsTransport, TOKEN_HEADER } from "./transport.js";
7
+ const TOKEN_PATH = "/latest/api/token";
8
+ const TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds";
9
+ const MIN_TTL_SECONDS = 1;
10
+ const MAX_TTL_SECONDS = 21_600;
11
+ const REFRESH_MARGIN = Duration.seconds(30);
12
+ const decodeToken = Schema.decodeEffect(Schema.NonEmptyString.annotate({ description: "IMDSv2 session token" }));
13
+ const ttlSeconds = (ttl) => Math.min(MAX_TTL_SECONDS, Math.max(MIN_TTL_SECONDS, Math.floor(Duration.toSeconds(ttl))));
14
+ // Refresh 30 seconds early, or halfway through a short-lived token.
15
+ const refreshAfter = (ttl) => Duration.max(Duration.subtract(ttl, REFRESH_MARGIN), Duration.divideUnsafe(ttl, 2));
16
+ const toTokenError = (cause) => new AwsImdsTokenError(failureDetails(cause));
17
+ /**
18
+ * IMDSv2 session tokens.
19
+ *
20
+ * - `get` returns the cached token, requesting one with `PUT /latest/api/token`
21
+ * when none is cached or the cached one is close to expiry.
22
+ * - `invalidate(token)` discards `token` if it is still the cached one, so
23
+ * concurrent reads that were all rejected trigger a single refresh.
24
+ * - `clear` discards the cached token unconditionally.
25
+ *
26
+ * Provide your own implementation with `Layer.succeed` to use a fixed token.
27
+ *
28
+ * @since 0.1.0
29
+ */
30
+ class AwsImdsToken extends Context.Service()("@rc312/effect-aws-imds/AwsImdsToken") {
31
+ /** Requests tokens through the `HttpClient`, configured by `AwsImdsConfig`. */
32
+ static layer = Layer.effect(AwsImdsToken, Effect.gen(function* make() {
33
+ const settings = yield* AwsImdsConfig;
34
+ const seconds = ttlSeconds(settings.tokenTtl);
35
+ const client = (yield* HttpClient.HttpClient).pipe(imdsTransport(settings));
36
+ const fetchToken = client
37
+ .put(TOKEN_PATH, { headers: { [TOKEN_TTL_HEADER]: String(seconds) } })
38
+ .pipe(Effect.flatMap((response) => response.text), Effect.flatMap((text) => decodeToken(text)), Effect.map((token) => Redacted.make(token, { label: TOKEN_HEADER })), Effect.mapError(toTokenError), Effect.withSpan("AwsImdsToken.fetch"));
39
+ const tokens = yield* Cache.makeWith(() => fetchToken, {
40
+ capacity: 1,
41
+ timeToLive: cacheOnSuccess(refreshAfter(Duration.seconds(seconds))),
42
+ });
43
+ const current = Cache.get(tokens, TOKEN_PATH);
44
+ const clear = Cache.invalidate(tokens, TOKEN_PATH);
45
+ return AwsImdsToken.of({
46
+ clear,
47
+ get: current,
48
+ invalidate: (rejected) => Cache.getSuccess(tokens, TOKEN_PATH).pipe(Effect.flatMap((cached) => Option.contains(cached, rejected) ? clear : Effect.void)),
49
+ });
50
+ }));
51
+ }
52
+ export { AwsImdsToken };
53
+ //# sourceMappingURL=token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.js","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { HttpClient, HttpClientError } from "effect/http";
2
+ import type { AwsImdsSettings } from "./config.ts";
3
+ import type { AwsImdsToken } from "./token.ts";
4
+ declare const TOKEN_HEADER = "x-aws-ec2-metadata-token";
5
+ /**
6
+ * IMDS transport policy for any `HttpClient`: the endpoint as base URL, a
7
+ * timeout on each attempt including its text body, non-2xx statuses as errors,
8
+ * and transient retries on `settings.retrySchedule`.
9
+ *
10
+ * @since 0.1.0
11
+ */
12
+ declare const imdsTransport: (settings: AwsImdsSettings) => <E, R>(client: HttpClient.HttpClient.With<E, R>) => HttpClient.HttpClient.With<E | HttpClientError.HttpClientError | import("effect/Cause").TimeoutError, R>;
13
+ /**
14
+ * Wraps a client so every request carries a session token. The token is
15
+ * resolved before the request, outside its timeout and transient retries; a
16
+ * `401` invalidates that token and the request runs once more with a new one.
17
+ *
18
+ * Apply it after `imdsTransport`: the transport's timeout and retries then
19
+ * cover each metadata request, while time spent obtaining the token does not
20
+ * count against them.
21
+ *
22
+ * @since 0.1.0
23
+ */
24
+ declare const withSessionToken: (token: AwsImdsToken["Service"]) => <E, R>(client: HttpClient.HttpClient.With<E, R>) => HttpClient.HttpClient.With<E | import("./token-error.ts").AwsImdsTokenError, R>;
25
+ export { imdsTransport, TOKEN_HEADER, withSessionToken };
26
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,UAAU,EACV,eAAe,EAEhB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,QAAA,MAAM,YAAY,6BAA6B,CAAC;AAShD;;;;;;GAMG;AACH,QAAA,MAAM,aAAa,aACN,eAAe,MACzB,CAAC,EAAE,CAAC,UAAU,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,6GAgB5C,CAAC;AAMN;;;;;;;;;;GAUG;AACH,QAAA,MAAM,gBAAgB,UACZ,YAAY,CAAC,SAAS,CAAC,MAC9B,CAAC,EAAE,CAAC,UAAU,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,oFAkC9C,CAAC;AAEJ,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,EAAE,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { Effect, Redacted } from "effect";
2
+ import { Headers, HttpClient, HttpClientError, HttpClientRequest, } from "effect/http";
3
+ const TOKEN_HEADER = "x-aws-ec2-metadata-token";
4
+ const UNAUTHORIZED = 401;
5
+ // Redacts the session token from client spans that capture request headers.
6
+ const redactTokenHeader = Effect.updateService(Headers.CurrentRedactedNames, (names) => [...names, TOKEN_HEADER]);
7
+ /**
8
+ * IMDS transport policy for any `HttpClient`: the endpoint as base URL, a
9
+ * timeout on each attempt including its text body, non-2xx statuses as errors,
10
+ * and transient retries on `settings.retrySchedule`.
11
+ *
12
+ * @since 0.1.0
13
+ */
14
+ const imdsTransport = (settings) => (client) => client.pipe(HttpClient.mapRequest(HttpClientRequest.prependUrl(settings.endpoint)), HttpClient.filterStatusOk,
15
+ // HTTP responses memoize their text, so callers can read it again after
16
+ // the complete download has succeeded within the attempt's deadline.
17
+ HttpClient.tap((response) => response.text), HttpClient.transformResponse(Effect.timeout(settings.attemptTimeout)), HttpClient.retryTransient({
18
+ schedule: settings.retrySchedule,
19
+ // Reading the text body can fail after headers have already arrived.
20
+ // Schema/JSON decoding happens outside this transport policy.
21
+ while: (error) => HttpClientError.isHttpClientError(error) &&
22
+ error.reason instanceof HttpClientError.DecodeError,
23
+ }));
24
+ const isUnauthorized = (error) => HttpClientError.isHttpClientError(error) &&
25
+ error.response?.status === UNAUTHORIZED;
26
+ /**
27
+ * Wraps a client so every request carries a session token. The token is
28
+ * resolved before the request, outside its timeout and transient retries; a
29
+ * `401` invalidates that token and the request runs once more with a new one.
30
+ *
31
+ * Apply it after `imdsTransport`: the transport's timeout and retries then
32
+ * cover each metadata request, while time spent obtaining the token does not
33
+ * count against them.
34
+ *
35
+ * @since 0.1.0
36
+ */
37
+ const withSessionToken = (token) => (client) => {
38
+ const attempt = (request) => token.get.pipe(Effect.flatMap((sessionToken) => client
39
+ .execute(HttpClientRequest.setHeader(request, TOKEN_HEADER, Redacted.value(sessionToken)))
40
+ .pipe(redactTokenHeader, Effect.tapError((failure) => isUnauthorized(failure)
41
+ ? token.invalidate(sessionToken)
42
+ : Effect.void))));
43
+ return HttpClient.makeWith((request) => request.pipe(Effect.flatMap((prepared) => attempt(prepared).pipe(Effect.retry({ times: 1, while: isUnauthorized })))), Effect.succeed);
44
+ };
45
+ export { imdsTransport, TOKEN_HEADER, withSessionToken };
46
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAC1C,OAAO,EACL,OAAO,EACP,UAAU,EACV,eAAe,EACf,iBAAiB,GAClB,MAAM,aAAa,CAAC;AAKrB,MAAM,YAAY,GAAG,0BAA0B,CAAC;AAChD,MAAM,YAAY,GAAG,GAAG,CAAC;AAEzB,4EAA4E;AAC5E,MAAM,iBAAiB,GAAG,MAAM,CAAC,aAAa,CAC5C,OAAO,CAAC,oBAAoB,EAC5B,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,EAAE,YAAY,CAAC,CACpC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,aAAa,GACjB,CAAC,QAAyB,EAAE,EAAE,CAC9B,CAAO,MAAwC,EAAE,EAAE,CACjD,MAAM,CAAC,IAAI,CACT,UAAU,CAAC,UAAU,CAAC,iBAAiB,CAAC,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,EACtE,UAAU,CAAC,cAAc;AACzB,wEAAwE;AACxE,qEAAqE;AACrE,UAAU,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAC3C,UAAU,CAAC,iBAAiB,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,EACrE,UAAU,CAAC,cAAc,CAAC;IACxB,QAAQ,EAAE,QAAQ,CAAC,aAAa;IAChC,qEAAqE;IACrE,8DAA8D;IAC9D,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CACf,eAAe,CAAC,iBAAiB,CAAC,KAAK,CAAC;QACxC,KAAK,CAAC,MAAM,YAAY,eAAe,CAAC,WAAW;CACtD,CAAC,CACH,CAAC;AAEN,MAAM,cAAc,GAAG,CAAI,KAAQ,EAAE,EAAE,CACrC,eAAe,CAAC,iBAAiB,CAAC,KAAK,CAAC;IACxC,KAAK,CAAC,QAAQ,EAAE,MAAM,KAAK,YAAY,CAAC;AAE1C;;;;;;;;;;GAUG;AACH,MAAM,gBAAgB,GACpB,CAAC,KAA8B,EAAE,EAAE,CACnC,CAAO,MAAwC,EAAE,EAAE;IACjD,MAAM,OAAO,GAAG,CAAC,OAA4C,EAAE,EAAE,CAC/D,KAAK,CAAC,GAAG,CAAC,IAAI,CACZ,MAAM,CAAC,OAAO,CAAC,CAAC,YAAY,EAAE,EAAE,CAC9B,MAAM;SACH,OAAO,CACN,iBAAiB,CAAC,SAAS,CACzB,OAAO,EACP,YAAY,EACZ,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,CAC7B,CACF;SACA,IAAI,CACH,iBAAiB,EACjB,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE,EAAE,CAC1B,cAAc,CAAC,OAAO,CAAC;QACrB,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,YAAY,CAAC;QAChC,CAAC,CAAC,MAAM,CAAC,IAAI,CAChB,CACF,CACJ,CACF,CAAC;IAEJ,OAAO,UAAU,CAAC,QAAQ,CACxB,CAAC,OAA2D,EAAE,EAAE,CAC9D,OAAO,CAAC,IAAI,CACV,MAAM,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CAC1B,OAAO,CAAC,QAAQ,CAAC,CAAC,IAAI,CACpB,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAClD,CACF,CACF,EACH,MAAM,CAAC,OAAO,CACf,CAAC;AACJ,CAAC,CAAC;AAEJ,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "@rc312/effect-aws-imds",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Effect-native client for the AWS EC2 Instance Metadata Service (IMDSv2).",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/RyanCallahan312/effect-extensions.git",
9
+ "directory": "packages/effect-aws-imds"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "src"
14
+ ],
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "exports": {
18
+ ".": "./dist/index.js",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "registry": "https://registry.npmjs.org/"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -b tsconfig.json",
27
+ "dev": "tsc -b tsconfig.json --watch",
28
+ "typecheck": "tsc --noEmit -p tsconfig.json --tsBuildInfoFile .cache/typecheck.tsbuildinfo && tsc -p tsconfig.test.json",
29
+ "test": "vitest run && node --test test/*.test.mjs",
30
+ "prepack": "bun run build"
31
+ },
32
+ "devDependencies": {
33
+ "@effect/vitest": "^4.1.2",
34
+ "@types/node": "^26.6.5",
35
+ "effect": "^4.1.2",
36
+ "vitest": "^5.0.4"
37
+ },
38
+ "peerDependencies": {
39
+ "effect": "^4.1.2"
40
+ },
41
+ "engines": {
42
+ "node": ">=22.19.0"
43
+ }
44
+ }
@@ -0,0 +1,118 @@
1
+ import { Context, Effect, flow, Layer, Schema } from "effect";
2
+ import { HttpClient } from "effect/http";
3
+
4
+ import { AwsImdsConfig } from "./config.ts";
5
+ import { failureDetails } from "./internal.ts";
6
+ import { AwsImdsMetadataError } from "./metadata-error.ts";
7
+ import { AwsImdsTokenError } from "./token-error.ts";
8
+ import { AwsImdsToken } from "./token.ts";
9
+ import { imdsTransport, withSessionToken } from "./transport.ts";
10
+
11
+ const NOT_FOUND = 404;
12
+
13
+ /**
14
+ * Any failure produced by the IMDS services.
15
+ *
16
+ * @since 0.1.0
17
+ */
18
+ type AwsImdsError = AwsImdsMetadataError | AwsImdsTokenError;
19
+
20
+ const metadataError = (path: string) => (cause: Error) =>
21
+ new AwsImdsMetadataError({ ...failureDetails(cause), path });
22
+
23
+ // Token failures surface as themselves; everything else is about `path`.
24
+ const toImdsError = (path: string) => (cause: Error) =>
25
+ cause instanceof AwsImdsTokenError ? cause : metadataError(path)(cause);
26
+
27
+ /**
28
+ * Reads instance metadata through IMDSv2. Reads are not cached.
29
+ *
30
+ * `get(path)` returns the raw text at a path such as
31
+ * `/latest/meta-data/instance-id`.
32
+ *
33
+ * @since 0.1.0
34
+ */
35
+ class AwsImds extends Context.Service<
36
+ AwsImds,
37
+ {
38
+ readonly get: (path: string) => Effect.Effect<string, AwsImdsError>;
39
+ }
40
+ >()("@rc312/effect-aws-imds/AwsImds") {
41
+ /** Reads through the `HttpClient` with tokens from `AwsImdsToken`. */
42
+ static readonly layer: Layer.Layer<
43
+ AwsImds,
44
+ never,
45
+ AwsImdsToken | HttpClient.HttpClient
46
+ > = Layer.effect(
47
+ AwsImds,
48
+ Effect.gen(function* make() {
49
+ const settings = yield* AwsImdsConfig;
50
+ const token = yield* AwsImdsToken;
51
+
52
+ const client = (yield* HttpClient.HttpClient).pipe(
53
+ imdsTransport(settings),
54
+ withSessionToken(token)
55
+ );
56
+
57
+ return AwsImds.of({
58
+ get: (path) =>
59
+ client.get(path).pipe(
60
+ Effect.flatMap((response) => response.text),
61
+ Effect.mapError(toImdsError(path)),
62
+ Effect.withSpan("AwsImds.get", {
63
+ attributes: { "aws.imds.path": path },
64
+ })
65
+ ),
66
+ });
67
+ })
68
+ );
69
+ }
70
+
71
+ /** Decodes metadata text with `codec`, attributing failures to `path`. */
72
+ const decodeAt = <A, RD>(path: string, codec: Schema.Codec<A, string, RD>) =>
73
+ flow(
74
+ (text: string) => Schema.decodeEffect(codec)(text),
75
+ Effect.mapError(metadataError(path))
76
+ );
77
+
78
+ /**
79
+ * Reads the raw text at a metadata path.
80
+ *
81
+ * @since 0.1.0
82
+ */
83
+ const get = (path: string) => AwsImds.use((imds) => imds.get(path));
84
+
85
+ /**
86
+ * Reads a metadata path and decodes its text with `codec`, for example
87
+ * `Schema.fromJsonString(MySchema)` for a JSON document.
88
+ *
89
+ * @since 0.1.0
90
+ */
91
+ const getSchema = <A, RD>(path: string, codec: Schema.Codec<A, string, RD>) =>
92
+ get(path).pipe(Effect.flatMap(decodeAt(path, codec)));
93
+
94
+ const isNotFound = <E>(error: E) =>
95
+ error instanceof AwsImdsMetadataError && error.status === NOT_FOUND;
96
+
97
+ /**
98
+ * Turns a `404` from IMDS into `Option.none()` and a success into
99
+ * `Option.some`. Every other failure is kept.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * const action = AwsImds.get("/latest/meta-data/spot/instance-action").pipe(
104
+ * AwsImds.optional
105
+ * )
106
+ * ```
107
+ *
108
+ * @since 0.1.0
109
+ */
110
+ const optional = <A, E, R>(self: Effect.Effect<A, E, R>) =>
111
+ self.pipe(
112
+ Effect.asSome,
113
+ Effect.catchIf(isNotFound, () => Effect.succeedNone)
114
+ );
115
+
116
+ export { AwsImds, decodeAt, get, getSchema, optional };
117
+
118
+ export type { AwsImdsError };