@kutur-kit/assets 0.0.0-stage → 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -2
  3. package/dist/Asset.d.ts +174 -0
  4. package/dist/Asset.d.ts.map +1 -0
  5. package/dist/Asset.js +156 -0
  6. package/dist/Asset.js.map +1 -0
  7. package/dist/AssetBucket.d.ts +73 -0
  8. package/dist/AssetBucket.d.ts.map +1 -0
  9. package/dist/AssetBucket.js +19 -0
  10. package/dist/AssetBucket.js.map +1 -0
  11. package/dist/AssetDelivery.d.ts +23 -0
  12. package/dist/AssetDelivery.d.ts.map +1 -0
  13. package/dist/AssetDelivery.js +12 -0
  14. package/dist/AssetDelivery.js.map +1 -0
  15. package/dist/AssetSigning.d.ts +109 -0
  16. package/dist/AssetSigning.d.ts.map +1 -0
  17. package/dist/AssetSigning.js +93 -0
  18. package/dist/AssetSigning.js.map +1 -0
  19. package/dist/alchemy/r2.d.ts +40 -0
  20. package/dist/alchemy/r2.d.ts.map +1 -0
  21. package/dist/alchemy/r2.js +83 -0
  22. package/dist/alchemy/r2.js.map +1 -0
  23. package/dist/delivery/local.d.ts +17 -0
  24. package/dist/delivery/local.d.ts.map +1 -0
  25. package/dist/delivery/local.js +20 -0
  26. package/dist/delivery/local.js.map +1 -0
  27. package/dist/filesystem.d.ts +15 -0
  28. package/dist/filesystem.d.ts.map +1 -0
  29. package/dist/filesystem.js +90 -0
  30. package/dist/filesystem.js.map +1 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +3 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/internal/bytes.d.ts +3 -0
  36. package/dist/internal/bytes.d.ts.map +1 -0
  37. package/dist/internal/bytes.js +11 -0
  38. package/dist/internal/bytes.js.map +1 -0
  39. package/dist/internal/http.d.ts +17 -0
  40. package/dist/internal/http.d.ts.map +1 -0
  41. package/dist/internal/http.js +38 -0
  42. package/dist/internal/http.js.map +1 -0
  43. package/dist/internal/sigv4.d.ts +15 -0
  44. package/dist/internal/sigv4.d.ts.map +1 -0
  45. package/dist/internal/sigv4.js +52 -0
  46. package/dist/internal/sigv4.js.map +1 -0
  47. package/dist/internal/url.d.ts +3 -0
  48. package/dist/internal/url.d.ts.map +1 -0
  49. package/dist/internal/url.js +3 -0
  50. package/dist/internal/url.js.map +1 -0
  51. package/dist/memory.d.ts +11 -0
  52. package/dist/memory.d.ts.map +1 -0
  53. package/dist/memory.js +34 -0
  54. package/dist/memory.js.map +1 -0
  55. package/dist/s3.d.ts +42 -0
  56. package/dist/s3.d.ts.map +1 -0
  57. package/dist/s3.js +67 -0
  58. package/dist/s3.js.map +1 -0
  59. package/dist/signing/local.d.ts +27 -0
  60. package/dist/signing/local.d.ts.map +1 -0
  61. package/dist/signing/local.js +128 -0
  62. package/dist/signing/local.js.map +1 -0
  63. package/dist/testing/AssetBucketContract.d.ts +18 -0
  64. package/dist/testing/AssetBucketContract.d.ts.map +1 -0
  65. package/dist/testing/AssetBucketContract.js +124 -0
  66. package/dist/testing/AssetBucketContract.js.map +1 -0
  67. package/dist/testing/AssetSigningContract.d.ts +18 -0
  68. package/dist/testing/AssetSigningContract.d.ts.map +1 -0
  69. package/dist/testing/AssetSigningContract.js +132 -0
  70. package/dist/testing/AssetSigningContract.js.map +1 -0
  71. package/dist/testing/index.d.ts +3 -0
  72. package/dist/testing/index.d.ts.map +1 -0
  73. package/dist/testing/index.js +3 -0
  74. package/dist/testing/index.js.map +1 -0
  75. package/package.json +109 -3
  76. package/src/Asset.ts +443 -0
  77. package/src/AssetBucket.ts +82 -0
  78. package/src/AssetDelivery.ts +41 -0
  79. package/src/AssetSigning.ts +206 -0
  80. package/src/alchemy/r2.ts +149 -0
  81. package/src/delivery/local.ts +41 -0
  82. package/src/filesystem.ts +166 -0
  83. package/src/index.ts +3 -0
  84. package/src/internal/bytes.ts +12 -0
  85. package/src/internal/http.ts +54 -0
  86. package/src/internal/sigv4.ts +84 -0
  87. package/src/internal/url.ts +2 -0
  88. package/src/memory.ts +65 -0
  89. package/src/s3.ts +138 -0
  90. package/src/signing/local.ts +210 -0
  91. package/src/testing/AssetBucketContract.ts +215 -0
  92. package/src/testing/AssetSigningContract.ts +254 -0
  93. package/src/testing/index.ts +3 -0
@@ -0,0 +1,17 @@
1
+ import type * as AssetBucket from "../AssetBucket.ts";
2
+ import { Effect, Option } from "effect";
3
+ import { HttpServerRequest, HttpServerResponse } from "effect/http";
4
+ /** The routes' prefix and wildcard path under a base URL, absolute or not. */
5
+ export declare const mount: (baseUrl: string) => {
6
+ prefix: string;
7
+ path: `/${string}/*`;
8
+ };
9
+ /** The current request's key under the prefix, and its query; no key for a malformed path. */
10
+ export declare const target: (prefix: string) => Effect.Effect<{
11
+ request: HttpServerRequest.HttpServerRequest;
12
+ key: Option.Option<string>;
13
+ query: import("node:url").URLSearchParams;
14
+ }, never, HttpServerRequest.HttpServerRequest>;
15
+ /** Serves a stored object with its type, length and cache control, or 404. */
16
+ export declare const respond: (bucket: AssetBucket.Bucket, key: string) => Effect.Effect<HttpServerResponse.HttpServerResponse, AssetBucket.AssetBucketError, never>;
17
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sources":["../../src/internal/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,WAAW,MAAM,mBAAmB,CAAC;AAEtD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEpE,8EAA8E;AAC9E,eAAO,MAAM,KAAK,YAAa,MAAM;;;CAIpC,CAAC;AAEF,8FAA8F;AAC9F,eAAO,MAAM,MAAM,WAAY,MAAM;;;;8CAajC,CAAC;AASL,8EAA8E;AAC9E,eAAO,MAAM,OAAO,WAAY,WAAW,CAAC,MAAM,OAAO,MAAM,8FAiB5D,CAAC"}
@@ -0,0 +1,38 @@
1
+ import { Effect, Option } from "effect";
2
+ import { HttpServerRequest, HttpServerResponse } from "effect/http";
3
+ /** The routes' prefix and wildcard path under a base URL, absolute or not. */
4
+ export const mount = (baseUrl) => {
5
+ const prefix = new URL(baseUrl, "http://localhost").pathname.replace(/\/$/u, "");
6
+ return { prefix, path: `/${prefix.slice(1)}/*` };
7
+ };
8
+ /** The current request's key under the prefix, and its query; no key for a malformed path. */
9
+ export const target = (prefix) => Effect.map(HttpServerRequest.HttpServerRequest, (request) => {
10
+ const url = new URL(request.url, "http://localhost");
11
+ const key = Option.liftThrowable(() => url.pathname
12
+ .slice(prefix.length + 1)
13
+ .split("/")
14
+ .map(decodeURIComponent)
15
+ .join("/"))();
16
+ return { request, key, query: url.searchParams };
17
+ });
18
+ // Objects are served from the app's origin in development. Even a file a
19
+ // browser would read as something else must not be sniffed or run.
20
+ const inert = {
21
+ "x-content-type-options": "nosniff",
22
+ "content-security-policy": "default-src 'none'; sandbox",
23
+ };
24
+ /** Serves a stored object with its type, length and cache control, or 404. */
25
+ export const respond = (bucket, key) => Effect.map(bucket.get(key), Option.match({
26
+ onNone: () => HttpServerResponse.empty({ status: 404 }),
27
+ onSome: ({ info, body }) => {
28
+ const response = HttpServerResponse.stream(body, {
29
+ contentType: info.contentType,
30
+ contentLength: Number(info.size),
31
+ headers: inert,
32
+ });
33
+ return info.cacheControl === undefined
34
+ ? response
35
+ : HttpServerResponse.setHeader(response, "cache-control", info.cacheControl);
36
+ },
37
+ }));
38
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sources":["../../src/internal/http.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEpE,8EAA8E;AAC9E,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,OAAe,EAAE,EAAE;IACvC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,EAAE,kBAAkB,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAEjF,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAa,EAAE,CAAC;AAC5D,CAAC,CAAC;AAEF,8FAA8F;AAC9F,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,MAAc,EAAE,EAAE,CACvC,MAAM,CAAC,GAAG,CAAC,iBAAiB,CAAC,iBAAiB,EAAE,CAAC,OAAO,EAAE,EAAE;IAC1D,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;IAErD,MAAM,GAAG,GAAG,MAAM,CAAC,aAAa,CAAC,GAAG,EAAE,CACpC,GAAG,CAAC,QAAQ;SACT,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;SACxB,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,kBAAkB,CAAC;SACvB,IAAI,CAAC,GAAG,CAAC,CACb,EAAE,CAAC;IAEJ,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,YAAY,EAAE,CAAC;AACnD,CAAC,CAAC,CAAC;AAEL,yEAAyE;AACzE,mEAAmE;AACnE,MAAM,KAAK,GAAG;IACZ,wBAAwB,EAAE,SAAS;IACnC,yBAAyB,EAAE,6BAA6B;CACzD,CAAC;AAEF,8EAA8E;AAC9E,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,MAA0B,EAAE,GAAW,EAAE,EAAE,CACjE,MAAM,CAAC,GAAG,CACR,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EACf,MAAM,CAAC,KAAK,CAAC;IACX,MAAM,EAAE,GAAG,EAAE,CAAC,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IACvD,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE;QACzB,MAAM,QAAQ,GAAG,kBAAkB,CAAC,MAAM,CAAC,IAAI,EAAE;YAC/C,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,aAAa,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;YAChC,OAAO,EAAE,KAAK;SACf,CAAC,CAAC;QAEH,OAAO,IAAI,CAAC,YAAY,KAAK,SAAS;YACpC,CAAC,CAAC,QAAQ;YACV,CAAC,CAAC,kBAAkB,CAAC,SAAS,CAAC,QAAQ,EAAE,eAAe,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IACjF,CAAC;CACF,CAAC,CACH,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type * as AssetSigning from "../AssetSigning.ts";
2
+ import * as Credentials from "@distilled.cloud/aws/Credentials";
3
+ import { Effect } from "effect";
4
+ /** Where signed URLs point: path-style, `${endpoint}/${bucket}/${key}`. */
5
+ export interface Target {
6
+ readonly endpoint: string;
7
+ readonly bucket: string;
8
+ }
9
+ /**
10
+ * Signs with SigV4 for the S3 API. The target and the `Credentials` are
11
+ * resolved for each request, so stores that deliver them at runtime, such as
12
+ * alchemy's R2 bindings, can sign too.
13
+ */
14
+ export declare const make: <R>(bucketId: string, target: Effect.Effect<Target, never, R>) => Effect.Effect<AssetSigning.Signing, never, Credentials.Credentials | R>;
15
+ //# sourceMappingURL=sigv4.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sigv4.d.ts","sources":["../../src/internal/sigv4.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,YAAY,MAAM,oBAAoB,CAAC;AAExD,OAAO,KAAK,WAAW,MAAM,kCAAkC,CAAC;AAGhE,OAAO,EAA6B,MAAM,EAAE,MAAM,QAAQ,CAAC;AAK3D,2EAA2E;AAC3E,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAKD;;;;GAIG;AACH,eAAO,MAAM,IAAI,GAAI,CAAC,YACV,MAAM,UACR,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,KACtC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,EAAE,WAAW,CAAC,WAAW,GAAG,CAAC,CAwDrE,CAAC"}
@@ -0,0 +1,52 @@
1
+ import * as Credentials from "@distilled.cloud/aws/Credentials";
2
+ import * as Presign from "@distilled.cloud/aws/Presign";
3
+ import * as Region from "@distilled.cloud/aws/Region";
4
+ import { Clock, DateTime, Duration, Effect } from "effect";
5
+ import * as AssetBucket from "../AssetBucket.js";
6
+ import { encodeKey } from "./url.js";
7
+ // The longest S3 allows for a presigned URL.
8
+ const maxExpiry = Duration.days(7);
9
+ /**
10
+ * Signs with SigV4 for the S3 API. The target and the `Credentials` are
11
+ * resolved for each request, so stores that deliver them at runtime, such as
12
+ * alchemy's R2 bindings, can sign too.
13
+ */
14
+ export const make = (bucketId, target) => Effect.map(Effect.context(), (context) => {
15
+ const sign = (method, key, expiresIn, signed) => Effect.gen(function* () {
16
+ const { endpoint, bucket } = yield* target;
17
+ const credentials = yield* yield* Credentials.Credentials;
18
+ const now = yield* Clock.currentTimeMillis;
19
+ const seconds = Math.ceil(Duration.toMillis(Duration.min(expiresIn, maxExpiry)) / 1000);
20
+ const url = yield* Presign.presignUrl({
21
+ method,
22
+ url: `${endpoint.replace(/\/+$/u, "")}/${bucket}/${encodeKey(key)}`,
23
+ service: "s3",
24
+ region: credentials.region,
25
+ expiresIn: seconds,
26
+ headers: signed,
27
+ // The signing time comes from Effect's clock, as the expiry does.
28
+ datetime: new Date(now).toISOString().replace(/[-:]|\.\d{3}/gu, ""),
29
+ }).pipe(Effect.provideService(Region.Region, Effect.succeed(credentials.region)));
30
+ return {
31
+ method,
32
+ url,
33
+ // Browsers set Content-Length from the body; the rest is sent as given.
34
+ headers: Object.fromEntries(Object.entries(signed).filter(([name]) => name !== "content-length")),
35
+ expiresAt: DateTime.makeUnsafe(now + seconds * 1000),
36
+ };
37
+ }).pipe(Effect.provideContext(context), Effect.mapError((cause) => new AssetBucket.AssetBucketError({ operation: "sign", bucket: bucketId, key, cause })));
38
+ return {
39
+ // Content-Length is signed: without it, the storage accepts any size.
40
+ // `If-None-Match: *` makes the URL write-once: replayed after its
41
+ // upload, even with other bytes of the same length, it is refused.
42
+ signPut: ({ key, contentType, contentLength, cacheControl, expiresIn }) => sign("PUT", key, expiresIn, {
43
+ "content-type": contentType,
44
+ "content-length": String(contentLength),
45
+ "if-none-match": "*",
46
+ // Signed, or browsers' `no-cache` for conditional requests is stored.
47
+ "cache-control": cacheControl,
48
+ }),
49
+ signGet: ({ key, expiresIn }) => sign("GET", key, expiresIn, {}),
50
+ };
51
+ });
52
+ //# sourceMappingURL=sigv4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sigv4.js","sources":["../../src/internal/sigv4.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ /** A key in a URL path: segments encoded, slashes kept. */
2
+ export declare const encodeKey: (key: string) => string;
3
+ //# sourceMappingURL=url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url.d.ts","sources":["../../src/internal/url.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,eAAO,MAAM,SAAS,QAAS,MAAM,WAAqD,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** A key in a URL path: segments encoded, slashes kept. */
2
+ export const encodeKey = (key) => key.split("/").map(encodeURIComponent).join("/");
3
+ //# sourceMappingURL=url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url.js","sources":["../../src/internal/url.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type * as Asset from "./Asset.ts";
2
+ import type * as AssetBucket from "./AssetBucket.ts";
3
+ import { Layer } from "effect";
4
+ /**
5
+ * Backs a bucket with memory for the lifetime of the layer. Bodies are copied
6
+ * in and out, so callers cannot change stored bytes.
7
+ */
8
+ export declare const layer: <Self, Id extends string>(bucket: AssetBucket.Tag<Self, Id>) => Layer.Layer<Self>;
9
+ /** Backs the buckets of the given assets with memory, one store per bucket. */
10
+ export declare const toLayer: <const Assets extends ReadonlyArray<Asset.Any>>(assets: Assets) => Layer.Layer<Asset.BucketOf<Assets[number]>>;
11
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sources":["../src/memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,KAAK,WAAW,MAAM,kBAAkB,CAAC;AAErD,OAAO,EAAoB,KAAK,EAAkB,MAAM,QAAQ,CAAC;AASjE;;;GAGG;AACH,eAAO,MAAM,KAAK,GAAI,IAAI,EAAE,EAAE,SAAS,MAAM,UACnC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,KAChC,KAAK,CAAC,KAAK,CAAC,IAAI,CA8Bf,CAAC;AAEL,+EAA+E;AAC/E,eAAO,MAAM,OAAO,GAAI,KAAK,CAAC,MAAM,SAAS,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,UAC3D,MAAM,KACb,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAW5C,CAAC"}
package/dist/memory.js ADDED
@@ -0,0 +1,34 @@
1
+ import { ByteSize, Effect, Layer, Option, Stream } from "effect";
2
+ import { concat } from "./internal/bytes.js";
3
+ /**
4
+ * Backs a bucket with memory for the lifetime of the layer. Bodies are copied
5
+ * in and out, so callers cannot change stored bytes.
6
+ */
7
+ export const layer = (bucket) => Layer.sync(bucket, () => {
8
+ const objects = new Map();
9
+ return bucket.of({
10
+ put: ({ key, contentType, cacheControl, body }) => Stream.runCollect(body).pipe(Effect.map((chunks) => {
11
+ const bytes = concat(chunks);
12
+ const info = { size: ByteSize.bytes(bytes.length), contentType, cacheControl };
13
+ objects.set(key, { info, bytes });
14
+ return info;
15
+ })),
16
+ get: (key) => Effect.sync(() => Option.map(Option.fromUndefinedOr(objects.get(key)), ({ info, bytes }) => ({
17
+ info,
18
+ body: Stream.succeed(bytes.slice()),
19
+ }))),
20
+ head: (key) => Effect.sync(() => Option.map(Option.fromUndefinedOr(objects.get(key)), ({ info }) => info)),
21
+ delete: (key) => Effect.sync(() => {
22
+ objects.delete(key);
23
+ }),
24
+ });
25
+ });
26
+ /** Backs the buckets of the given assets with memory, one store per bucket. */
27
+ export const toLayer = (assets) => {
28
+ // SAFETY: asset buckets are tags from AssetBucket.Service.
29
+ const buckets = new Set(assets.map((asset) => asset.bucket));
30
+ const merged = [...buckets].reduce((layers, bucket) => Layer.merge(layers, layer(bucket)), Layer.empty);
31
+ // SAFETY: the merged layers provide exactly the buckets of the listed assets.
32
+ return merged;
33
+ };
34
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.js","sources":["../src/memory.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEjE,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAO7C;;;GAGG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CACnB,MAAiC,EACd,EAAE,CACrB,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE;IACtB,MAAM,OAAO,GAAG,IAAI,GAAG,EAAiB,CAAC;IAEzC,OAAO,MAAM,CAAC,EAAE,CAAC;QACf,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE,EAAE,CAChD,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,CAC1B,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;YACpB,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;YAC7B,MAAM,IAAI,GAAG,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC;YAE/E,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YAElC,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CACH;QACH,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CACX,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CACf,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;YACzE,IAAI;YACJ,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;SACpC,CAAC,CAAC,CACJ;QACH,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CACZ,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;QAC7F,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CACd,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;YACf,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACtB,CAAC,CAAC;KACL,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEL,+EAA+E;AAC/E,MAAM,CAAC,MAAM,OAAO,GAAG,CACrB,MAAc,EAC+B,EAAE;IAC/C,2DAA2D;IAC3D,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAkC,CAAC,CAAC,CAAC;IAEzF,MAAM,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,MAAM,CAChC,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,EACtD,KAAK,CAAC,KAAK,CACZ,CAAC;IAEF,8EAA8E;IAC9E,OAAO,MAAqD,CAAC;AAC/D,CAAC,CAAC"}
package/dist/s3.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ import type * as AssetSigning from "./AssetSigning.ts";
2
+ import type * as Credentials from "@distilled.cloud/aws/Credentials";
3
+ import type * as HttpClient from "effect/http/HttpClient";
4
+ import { Layer } from "effect";
5
+ import * as AssetBucket from "./AssetBucket.ts";
6
+ export interface Options {
7
+ /** The bucket's name in the storage. */
8
+ readonly bucket: string;
9
+ }
10
+ export interface SigningOptions extends Options {
11
+ /**
12
+ * Where clients reach the storage, such as
13
+ * `https://<account>.r2.cloudflarestorage.com` or `http://localhost:9000`.
14
+ * Signed URLs are path-style under it: `${endpoint}/${bucket}/${key}`.
15
+ */
16
+ readonly endpoint: string;
17
+ }
18
+ type Requirements = Credentials.Credentials | HttpClient.HttpClient;
19
+ /**
20
+ * Backs a bucket with S3 or a store with its API, such as R2 or RustFS,
21
+ * through `@distilled.cloud/aws`. Provide its `Credentials` and an
22
+ * `HttpClient`, and an `Endpoint` for stores other than AWS.
23
+ *
24
+ * Bodies are collected before they are sent, so a failing body sends nothing.
25
+ * Uploads through the app are small by `maxSize`; larger files go to storage
26
+ * directly, through `signing`.
27
+ */
28
+ export declare const layer: <Self, Id extends string>(bucket: AssetBucket.Tag<Self, Id>, options: Options) => Layer.Layer<Self, never, Requirements>;
29
+ /**
30
+ * Signs URLs for a bucket with SigV4, for clients to upload to and download
31
+ * from the storage directly. Uploads are signed with their content type and
32
+ * length, the cache control to store, and to create an object only, which
33
+ * the storage enforces.
34
+ */
35
+ export declare const signing: <Self, Id extends string>(bucket: AssetBucket.Tag<Self, Id>, options: SigningOptions) => Layer.Layer<AssetSigning.SigningOf<Self>, never, Credentials.Credentials>;
36
+ /**
37
+ * R2's S3 endpoint for an account, for `signing` and an S3 `Endpoint`.
38
+ * Requests to it are signed for the region `auto`.
39
+ */
40
+ export declare const r2Endpoint: (accountId: string, jurisdiction?: "eu" | "fedramp") => string;
41
+ export {};
42
+ //# sourceMappingURL=s3.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3.d.ts","sources":["../src/s3.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,YAAY,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,KAAK,WAAW,MAAM,kCAAkC,CAAC;AACrE,OAAO,KAAK,KAAK,UAAU,MAAM,wBAAwB,CAAC;AAG1D,OAAO,EAAoB,KAAK,EAAkB,MAAM,QAAQ,CAAC;AAEjE,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAC;AAKhD,MAAM,WAAW,OAAO;IACtB,wCAAwC;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAe,SAAQ,OAAO;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,KAAK,YAAY,GAAG,WAAW,CAAC,WAAW,GAAG,UAAU,CAAC,UAAU,CAAC;AAOpE;;;;;;;;GAQG;AACH,eAAO,MAAM,KAAK,GAAI,IAAI,EAAE,EAAE,SAAS,MAAM,UACnC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,WACxB,OAAO,KACf,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,YAAY,CAyErC,CAAC;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,OAAO,GAAI,IAAI,EAAE,EAAE,SAAS,MAAM,UACrC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,WACxB,cAAc,KACtB,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,CAAC,WAAW,CACkB,CAAC;AAE9F;;;GAGG;AACH,eAAO,MAAM,UAAU,cAAe,MAAM,iBAAiB,IAAI,GAAG,SAAS,WAC2B,CAAC"}
package/dist/s3.js ADDED
@@ -0,0 +1,67 @@
1
+ import * as S3 from "@distilled.cloud/aws/s3";
2
+ import { ByteSize, Effect, Layer, Option, Stream } from "effect";
3
+ import * as AssetBucket from "./AssetBucket.js";
4
+ import * as AssetSigningModule from "./AssetSigning.js";
5
+ import { concat } from "./internal/bytes.js";
6
+ import * as SigV4 from "./internal/sigv4.js";
7
+ const failure = (bucket, operation, key) => (cause) => new AssetBucket.AssetBucketError({ operation, bucket, key, cause });
8
+ /**
9
+ * Backs a bucket with S3 or a store with its API, such as R2 or RustFS,
10
+ * through `@distilled.cloud/aws`. Provide its `Credentials` and an
11
+ * `HttpClient`, and an `Endpoint` for stores other than AWS.
12
+ *
13
+ * Bodies are collected before they are sent, so a failing body sends nothing.
14
+ * Uploads through the app are small by `maxSize`; larger files go to storage
15
+ * directly, through `signing`.
16
+ */
17
+ export const layer = (bucket, options) => Layer.effect(bucket, Effect.gen(function* () {
18
+ // S3 reads the optional `Endpoint` and `Region` when called, so the
19
+ // whole context is kept, not just the required services.
20
+ const context = yield* Effect.context();
21
+ const Bucket = options.bucket;
22
+ const fail = (operation, key) => failure(bucket.id, operation, key);
23
+ return bucket.of({
24
+ put: ({ key, contentType, cacheControl, body }) => Effect.gen(function* () {
25
+ const bytes = concat(yield* Stream.runCollect(body));
26
+ const request = {
27
+ Bucket,
28
+ Key: key,
29
+ Body: bytes,
30
+ ContentType: contentType,
31
+ };
32
+ if (cacheControl !== undefined)
33
+ request.CacheControl = cacheControl;
34
+ yield* S3.putObject(request).pipe(Effect.provideContext(context), Effect.mapError(fail("put", key)));
35
+ return { size: ByteSize.bytes(bytes.length), contentType, cacheControl };
36
+ }),
37
+ get: (key) => S3.getObject({ Bucket, Key: key }).pipe(Effect.map((object) => Option.some({
38
+ info: {
39
+ size: ByteSize.bytes(object.ContentLength ?? 0),
40
+ contentType: object.ContentType ?? "application/octet-stream",
41
+ cacheControl: object.CacheControl,
42
+ },
43
+ body: object.Body === undefined
44
+ ? Stream.empty
45
+ : Stream.mapError(object.Body, fail("get", key)),
46
+ })), Effect.catchTag("NoSuchKey", () => Effect.succeedNone), Effect.provideContext(context), Effect.mapError(fail("get", key))),
47
+ head: (key) => S3.headObject({ Bucket, Key: key }).pipe(Effect.map((object) => Option.some({
48
+ size: ByteSize.bytes(object.ContentLength ?? 0),
49
+ contentType: object.ContentType ?? "application/octet-stream",
50
+ cacheControl: object.CacheControl,
51
+ })), Effect.catchTag("NotFound", () => Effect.succeedNone), Effect.provideContext(context), Effect.mapError(fail("head", key))),
52
+ delete: (key) => S3.deleteObject({ Bucket, Key: key }).pipe(Effect.asVoid, Effect.provideContext(context), Effect.mapError(fail("delete", key))),
53
+ });
54
+ }));
55
+ /**
56
+ * Signs URLs for a bucket with SigV4, for clients to upload to and download
57
+ * from the storage directly. Uploads are signed with their content type and
58
+ * length, the cache control to store, and to create an object only, which
59
+ * the storage enforces.
60
+ */
61
+ export const signing = (bucket, options) => Layer.effect(AssetSigningModule.of(bucket), SigV4.make(bucket.id, Effect.succeed(options)));
62
+ /**
63
+ * R2's S3 endpoint for an account, for `signing` and an S3 `Endpoint`.
64
+ * Requests to it are signed for the region `auto`.
65
+ */
66
+ export const r2Endpoint = (accountId, jurisdiction) => `https://${accountId}.${jurisdiction === undefined ? "" : `${jurisdiction}.`}r2.cloudflarestorage.com`;
67
+ //# sourceMappingURL=s3.js.map
package/dist/s3.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3.js","sources":["../src/s3.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type * as AssetBucket from "../AssetBucket.ts";
2
+ import { Layer, Redacted } from "effect";
3
+ import { HttpRouter } from "effect/http";
4
+ import * as AssetSigning from "../AssetSigning.ts";
5
+ export interface Options {
6
+ /**
7
+ * Where `routes` are mounted, such as `/storage/private` or
8
+ * `http://localhost:3000/storage/private`. Signed URLs start with it.
9
+ */
10
+ readonly baseUrl: string;
11
+ /** Signs URLs. Changing it invalidates URLs already handed out. */
12
+ readonly secret: Redacted.Redacted<string>;
13
+ }
14
+ /**
15
+ * Signs URLs for a bucket with an HMAC, for development, tests or a single
16
+ * self-hosted server. `routes` verifies them, so storage needs no signing.
17
+ */
18
+ export declare const layer: <Self, Id extends string>(bucket: AssetBucket.Tag<Self, Id>, options: Options) => Layer.Layer<AssetSigning.SigningOf<Self>>;
19
+ /**
20
+ * Accepts signed uploads and serves signed downloads of a bucket under
21
+ * `baseUrl`. Refuses a request that differs from what was signed: another
22
+ * method, key, content type, length or cache control, or one sent after it
23
+ * expires. An upload to a key that already holds an object is refused with
24
+ * 412.
25
+ */
26
+ export declare const routes: <Self, Id extends string>(bucket: AssetBucket.Tag<Self, Id>, options: Options) => Layer.Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", Exclude<Self, HttpRouter.Provided>>>;
27
+ //# sourceMappingURL=local.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local.d.ts","sources":["../../src/signing/local.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,WAAW,MAAM,mBAAmB,CAAC;AAEtD,OAAO,EAML,KAAK,EAEL,QAAQ,EAET,MAAM,QAAQ,CAAC;AAChB,OAAO,EAAE,UAAU,EAAsB,MAAM,aAAa,CAAC;AAE7D,OAAO,KAAK,YAAY,MAAM,oBAAoB,CAAC;AAInD,MAAM,WAAW,OAAO;IACtB;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;CAC5C;AAkDD;;;GAGG;AACH,eAAO,MAAM,KAAK,GAAI,IAAI,EAAE,EAAE,SAAS,MAAM,UACnC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,WACxB,OAAO,KACf,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,CAAC,IAAI,CAAC,CAsC1C,CAAC;AAIF;;;;;;GAMG;AACH,eAAO,MAAM,MAAM,GAAI,IAAI,EAAE,EAAE,SAAS,MAAM,UACpC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,WACxB,OAAO,+HA0EjB,CAAC"}
@@ -0,0 +1,128 @@
1
+ import { ByteSize, Clock, DateTime, Duration, Effect, Layer, Option, Redacted, Stream, } from "effect";
2
+ import { HttpRouter, HttpServerResponse } from "effect/http";
3
+ import * as AssetSigning from "../AssetSigning.js";
4
+ import { mount, respond, target } from "../internal/http.js";
5
+ import { encodeKey } from "../internal/url.js";
6
+ const hex = (bytes) => Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
7
+ // The signature covers everything a client must not change.
8
+ const sign = (secret, signed) => Effect.promise(async () => {
9
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(Redacted.value(secret)), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
10
+ const message = [
11
+ signed.method,
12
+ signed.key,
13
+ signed.expires,
14
+ signed.contentType ?? "",
15
+ signed.contentLength ?? "",
16
+ signed.ifNoneMatch ?? "",
17
+ signed.cacheControl ?? "",
18
+ ].join("\n");
19
+ return hex(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message)));
20
+ });
21
+ // Compares in time independent of where the strings differ.
22
+ const equal = (a, b) => {
23
+ let difference = a.length ^ b.length;
24
+ for (let index = 0; index < Math.max(a.length, b.length); index++)
25
+ difference |= (a.charCodeAt(index) || 0) ^ (b.charCodeAt(index) || 0);
26
+ return difference === 0;
27
+ };
28
+ /**
29
+ * Signs URLs for a bucket with an HMAC, for development, tests or a single
30
+ * self-hosted server. `routes` verifies them, so storage needs no signing.
31
+ */
32
+ export const layer = (bucket, options) => {
33
+ const request = (signed, expiresIn) => Effect.gen(function* () {
34
+ const now = yield* Clock.currentTimeMillis;
35
+ const expires = Math.ceil((now + Duration.toMillis(expiresIn)) / 1000);
36
+ const signature = yield* sign(options.secret, { ...signed, expires });
37
+ const query = new URLSearchParams({ expires: String(expires), signature });
38
+ const headers = {};
39
+ if (signed.contentType !== undefined)
40
+ headers["content-type"] = signed.contentType;
41
+ if (signed.ifNoneMatch !== undefined)
42
+ headers["if-none-match"] = signed.ifNoneMatch;
43
+ if (signed.cacheControl !== undefined)
44
+ headers["cache-control"] = signed.cacheControl;
45
+ return {
46
+ method: signed.method,
47
+ url: `${options.baseUrl}/${encodeKey(signed.key)}?${query.toString()}`,
48
+ headers,
49
+ expiresAt: DateTime.makeUnsafe(expires * 1000),
50
+ };
51
+ });
52
+ return Layer.succeed(AssetSigning.of(bucket), {
53
+ signPut: ({ key, contentType, contentLength, cacheControl, expiresIn }) => request({
54
+ method: "PUT",
55
+ key,
56
+ contentType,
57
+ contentLength: String(contentLength),
58
+ ifNoneMatch: "*",
59
+ cacheControl,
60
+ }, expiresIn),
61
+ signGet: ({ key, expiresIn }) => request({ method: "GET", key }, expiresIn),
62
+ });
63
+ };
64
+ const refused = HttpServerResponse.text("Forbidden", { status: 403 });
65
+ /**
66
+ * Accepts signed uploads and serves signed downloads of a bucket under
67
+ * `baseUrl`. Refuses a request that differs from what was signed: another
68
+ * method, key, content type, length or cache control, or one sent after it
69
+ * expires. An upload to a key that already holds an object is refused with
70
+ * 412.
71
+ */
72
+ export const routes = (bucket, options) => {
73
+ const { prefix, path } = mount(options.baseUrl);
74
+ const verify = (query, signed) => Effect.gen(function* () {
75
+ const expires = Number(query.get("expires"));
76
+ const signature = query.get("signature") ?? "";
77
+ const now = yield* Clock.currentTimeMillis;
78
+ const expected = yield* sign(options.secret, { ...signed, expires });
79
+ return now <= expires * 1000 && equal(signature, expected);
80
+ });
81
+ const upload = Effect.gen(function* () {
82
+ const { request, key, query } = yield* target(prefix);
83
+ const contentType = request.headers["content-type"];
84
+ const contentLength = request.headers["content-length"];
85
+ const ifNoneMatch = request.headers["if-none-match"];
86
+ const cacheControl = request.headers["cache-control"];
87
+ // The signed length is required, so the body's is checked against it.
88
+ const length = Option.flatMap(Option.fromUndefinedOr(contentLength), (header) => ByteSize.fromInput(Number(header)));
89
+ const allowed = Option.isSome(key) &&
90
+ Option.isSome(length) &&
91
+ (yield* verify(query, {
92
+ method: "PUT",
93
+ key: key.value,
94
+ contentType,
95
+ contentLength,
96
+ ifNoneMatch,
97
+ cacheControl,
98
+ }));
99
+ if (!allowed || Option.isNone(key) || Option.isNone(length))
100
+ return refused;
101
+ const store = yield* bucket;
102
+ // A signed upload writes once, as S3 answers `If-None-Match: *`. Checked
103
+ // before writing, so two uploads racing to one key may both succeed: these
104
+ // routes serve development and tests.
105
+ const existing = yield* store.head(key.value);
106
+ if (Option.isSome(existing))
107
+ return HttpServerResponse.text("Precondition Failed", { status: 412 });
108
+ // Node ends the body at its Content-Length; a client that sends less
109
+ // fails the stream, so the bucket keeps what it had.
110
+ yield* store.put({
111
+ key: key.value,
112
+ contentType: contentType ?? "",
113
+ cacheControl,
114
+ body: Stream.orDie(request.stream),
115
+ contentLength: length.value,
116
+ });
117
+ return HttpServerResponse.empty({ status: 200 });
118
+ }).pipe(Effect.orDie);
119
+ const download = Effect.gen(function* () {
120
+ const { key, query } = yield* target(prefix);
121
+ const allowed = Option.isSome(key) && (yield* verify(query, { method: "GET", key: key.value }));
122
+ if (!allowed || Option.isNone(key))
123
+ return refused;
124
+ return yield* respond(yield* bucket, key.value);
125
+ }).pipe(Effect.orDie);
126
+ return Layer.mergeAll(HttpRouter.add("PUT", path, upload), HttpRouter.add("GET", path, download));
127
+ };
128
+ //# sourceMappingURL=local.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local.js","sources":["../../src/signing/local.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { Effect } from "effect";
2
+ import * as AssetBucket from "../AssetBucket.ts";
3
+ declare const ContractBucket_base: AssetBucket.Tag<ContractBucket, "assets-contract">;
4
+ /** The bucket the cases use. Back it with the implementation under test. */
5
+ export declare class ContractBucket extends ContractBucket_base {
6
+ }
7
+ /**
8
+ * Checks an `AssetBucket.Bucket` implementation against the guarantees it
9
+ * documents. Cases overwrite or delete the keys they use, under `contract/`.
10
+ * Run each case with any test runner.
11
+ */
12
+ export interface Case {
13
+ readonly name: string;
14
+ readonly run: Effect.Effect<void, never, ContractBucket>;
15
+ }
16
+ export declare const cases: ReadonlyArray<Case>;
17
+ export {};
18
+ //# sourceMappingURL=AssetBucketContract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AssetBucketContract.d.ts","sources":["../../src/testing/AssetBucketContract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,MAAM,EAAkC,MAAM,QAAQ,CAAC;AAE1E,OAAO,KAAK,WAAW,MAAM,mBAAmB,CAAC;;AAEjD,4EAA4E;AAC5E,qBAAa,cAAe,SAAQ,mBAAwD;CAAG;AAE/F;;;;GAIG;AACH,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC;CAC1D;AAwFD,eAAO,MAAM,KAAK,EAAE,aAAa,CAAC,IAAI,CA+GrC,CAAC"}