1/**
2 * Raw (non-distilled) HTTP helpers against the floci emulator gateway —
3 * out-of-band proof that a resource exists IN THE EMULATOR, not the real
4 * cloud. The dummy SigV4 Authorization header is never verified by the
5 * emulator; it only carries the service/region/account scoping the gateway
6 * routes by.
7 */
8import * as Effect from "effect/Effect";
9import * as HttpBody from "effect/http/HttpBody";
10import * as HttpClient from "effect/http/HttpClient";
11import * as HttpClientRequest from "effect/http/HttpClientRequest";
12import { spawnSync } from "node:child_process";
13
14export const FLOCI_ENDPOINT = "http://localhost:4566";
15
16/**
17 * skipIf gate: floci runs as a Docker container. `docker info` proves the
18 * daemon is reachable (a mere `docker --version` would pass with the daemon
19 * down). Sync probe at collection time — skipIf needs a plain boolean.
20 */
21export const dockerAvailable = (() => {
22 try {
23 return (
24 spawnSync("docker", ["info"], { stdio: "ignore", timeout: 15_000 })
25 .status === 0
26 );
27 } catch {
28 return false;
29 }
30})();
31
32/** POST an AWS-JSON operation straight at the emulator gateway. */
33export const rawAwsJson = Effect.fn(function* (options: {
34 service: string;
35 region: string;
36 target: string;
37 contentType: string;
38 body: Record<string, unknown>;
39}) {
40 const client = yield* HttpClient.HttpClient;
41 return yield* client.execute(
42 HttpClientRequest.post(`${FLOCI_ENDPOINT}/`).pipe(
43 HttpClientRequest.setHeaders({
44 "content-type": options.contentType,
45 "x-amz-target": options.target,
46 "x-amz-date": "20260101T000000Z",
47 authorization: `AWS4-HMAC-SHA256 Credential=test/20260101/${options.region}/${options.service}/aws4_request, SignedHeaders=host;x-amz-date, Signature=dummy`,
48 }),
49 HttpClientRequest.setBody(
50 HttpBody.text(JSON.stringify(options.body), options.contentType),
51 ),
52 ),
53 );
54});
55
56/** POST an AWS query-protocol operation (SNS, …) straight at the gateway. */
57export const rawAwsQuery = Effect.fn(function* (options: {
58 service: string;
59 region: string;
60 params: Record<string, string>;
61}) {
62 const client = yield* HttpClient.HttpClient;
63 return yield* client.execute(
64 HttpClientRequest.post(`${FLOCI_ENDPOINT}/`).pipe(
65 HttpClientRequest.setHeaders({
66 "content-type": "application/x-www-form-urlencoded",
67 "x-amz-date": "20260101T000000Z",
68 authorization: `AWS4-HMAC-SHA256 Credential=test/20260101/${options.region}/${options.service}/aws4_request, SignedHeaders=host;x-amz-date, Signature=dummy`,
69 }),
70 HttpClientRequest.setBody(
71 HttpBody.text(
72 new URLSearchParams(options.params).toString(),
73 "application/x-www-form-urlencoded",
74 ),
75 ),
76 ),
77 );
78});
79
80/** Path-style S3 GET against the gateway (list the bucket's objects). */
81export const rawS3GetBucket = Effect.fn(function* (bucketName: string) {
82 const client = yield* HttpClient.HttpClient;
83 return yield* client.execute(
84 HttpClientRequest.get(`${FLOCI_ENDPOINT}/${bucketName}`).pipe(
85 HttpClientRequest.setHeaders({
86 "x-amz-date": "20260101T000000Z",
87 authorization:
88 "AWS4-HMAC-SHA256 Credential=test/20260101/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=dummy",
89 }),
90 ),
91 );
92});
93
94/** Path-style S3 object GET against the gateway. */
95export const rawS3GetObject = Effect.fn(function* (
96 bucketName: string,
97 key: string,
98) {
99 const client = yield* HttpClient.HttpClient;
100 return yield* client.execute(
101 HttpClientRequest.get(`${FLOCI_ENDPOINT}/${bucketName}/${key}`).pipe(
102 HttpClientRequest.setHeaders({
103 "x-amz-date": "20260101T000000Z",
104 authorization:
105 "AWS4-HMAC-SHA256 Credential=test/20260101/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=dummy",
106 }),
107 ),
108 );
109});
110
111/** Region segment of an ARN (`arn:aws:sqs:REGION:ACCOUNT:name`). */
112export const regionOfArn = (arn: string) => arn.split(":")[3]!;