@specific.dev/spectest 0.39.0 → 0.41.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 (100) hide show
  1. package/dist/components/supabase.d.ts +87 -27
  2. package/dist/components/supabase.js +352 -69
  3. package/dist/daemon.d.ts +38 -0
  4. package/dist/daemon.js +388 -941
  5. package/dist/harness/build-context.d.ts +82 -0
  6. package/dist/harness/build-context.js +113 -0
  7. package/dist/harness/buildkit-progress.d.ts +37 -0
  8. package/dist/harness/buildkit-progress.js +66 -0
  9. package/dist/harness/container-run.d.ts +89 -0
  10. package/dist/harness/container-run.js +118 -0
  11. package/dist/harness/file-mounts.d.ts +91 -0
  12. package/dist/harness/file-mounts.js +119 -0
  13. package/dist/harness/hostmatch.d.ts +65 -0
  14. package/dist/harness/hostmatch.js +108 -0
  15. package/dist/harness/http-proxy.d.ts +62 -0
  16. package/dist/harness/http-proxy.js +104 -0
  17. package/dist/harness/ingress-table.d.ts +148 -0
  18. package/dist/harness/ingress-table.js +129 -0
  19. package/dist/harness/log-delta.d.ts +54 -0
  20. package/dist/harness/log-delta.js +83 -0
  21. package/dist/harness/main.d.ts +47 -0
  22. package/dist/harness/main.js +164 -0
  23. package/dist/harness/methods.d.ts +54 -0
  24. package/dist/harness/methods.js +65 -0
  25. package/dist/harness/names-registry.d.ts +63 -0
  26. package/dist/harness/names-registry.js +90 -0
  27. package/dist/harness/protocol.d.ts +88 -0
  28. package/dist/harness/protocol.js +96 -0
  29. package/dist/harness/ready-poll.d.ts +47 -0
  30. package/dist/harness/ready-poll.js +67 -0
  31. package/dist/harness/service-graph.d.ts +29 -0
  32. package/dist/harness/service-graph.js +92 -0
  33. package/dist/harness/volume-paths.d.ts +70 -0
  34. package/dist/harness/volume-paths.js +81 -0
  35. package/dist/index.d.ts +3 -3
  36. package/dist/ingress.d.ts +1 -1
  37. package/dist/resolver.js +5 -8
  38. package/dist/vendor/rrweb-plugin-console-record.umd.js +521 -0
  39. package/dist/vendor/rrweb-record.min.js +5061 -0
  40. package/package.json +7 -1
  41. package/src/aws-sigv4.ts +218 -0
  42. package/src/browser.ts +2040 -0
  43. package/src/components/aws.ts +554 -0
  44. package/src/components/email.ts +398 -0
  45. package/src/components/expo.ts +167 -0
  46. package/src/components/index.ts +81 -0
  47. package/src/components/k3s.ts +2061 -0
  48. package/src/components/postgres.ts +132 -0
  49. package/src/components/replayFake.ts +1015 -0
  50. package/src/components/s3.ts +132 -0
  51. package/src/components/supabase.ts +1699 -0
  52. package/src/daemon.ts +5489 -0
  53. package/src/harness/build-context.test.ts +0 -0
  54. package/src/harness/build-context.ts +146 -0
  55. package/src/harness/buildkit-progress.test.ts +98 -0
  56. package/src/harness/buildkit-progress.ts +74 -0
  57. package/src/harness/container-run.test.ts +209 -0
  58. package/src/harness/container-run.ts +158 -0
  59. package/src/harness/file-mounts.test.ts +185 -0
  60. package/src/harness/file-mounts.ts +145 -0
  61. package/src/harness/hostmatch.test.ts +148 -0
  62. package/src/harness/hostmatch.ts +109 -0
  63. package/src/harness/http-proxy.test.ts +156 -0
  64. package/src/harness/http-proxy.ts +119 -0
  65. package/src/harness/ingress-rebind.test.ts +125 -0
  66. package/src/harness/ingress-table.test.ts +172 -0
  67. package/src/harness/ingress-table.ts +186 -0
  68. package/src/harness/log-delta.test.ts +125 -0
  69. package/src/harness/log-delta.ts +100 -0
  70. package/src/harness/main.test.ts +211 -0
  71. package/src/harness/main.ts +196 -0
  72. package/src/harness/methods.test.ts +63 -0
  73. package/src/harness/methods.ts +92 -0
  74. package/src/harness/names-registry.test.ts +137 -0
  75. package/src/harness/names-registry.ts +108 -0
  76. package/src/harness/protocol.test.ts +148 -0
  77. package/src/harness/protocol.ts +163 -0
  78. package/src/harness/ready-poll.test.ts +172 -0
  79. package/src/harness/ready-poll.ts +93 -0
  80. package/src/harness/service-graph.test.ts +97 -0
  81. package/src/harness/service-graph.ts +97 -0
  82. package/src/harness/volume-paths.test.ts +102 -0
  83. package/src/harness/volume-paths.ts +112 -0
  84. package/src/ids.ts +89 -0
  85. package/src/index.ts +2725 -0
  86. package/src/ingress.ts +305 -0
  87. package/src/inspect.ts +739 -0
  88. package/src/locator.ts +716 -0
  89. package/src/mobile.ts +133 -0
  90. package/src/record-secrets.ts +41 -0
  91. package/src/recorder.ts +846 -0
  92. package/src/redis.ts +202 -0
  93. package/src/replay-bundle.ts +108 -0
  94. package/src/resolver.ts +348 -0
  95. package/src/s3.ts +333 -0
  96. package/src/sql.ts +243 -0
  97. package/src/terminal.ts +740 -0
  98. package/src/url-match.ts +67 -0
  99. package/src/vendor/rrweb-plugin-console-record.umd.js +521 -0
  100. package/src/vendor/rrweb-record.min.js +5061 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/spectest",
3
- "version": "0.39.0",
3
+ "version": "0.41.0",
4
4
  "description": "Spectest SDK for defining test environments in TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -8,27 +8,33 @@
8
8
  "exports": {
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
+ "bun": "./src/index.ts",
11
12
  "default": "./dist/index.js"
12
13
  },
13
14
  "./components": {
14
15
  "types": "./dist/components/index.d.ts",
16
+ "bun": "./src/components/index.ts",
15
17
  "default": "./dist/components/index.js"
16
18
  },
17
19
  "./browser": {
18
20
  "types": "./dist/browser.d.ts",
21
+ "bun": "./src/browser.ts",
19
22
  "default": "./dist/browser.js"
20
23
  },
21
24
  "./daemon": {
22
25
  "types": "./dist/daemon.d.ts",
26
+ "bun": "./src/daemon.ts",
23
27
  "default": "./dist/daemon.js"
24
28
  },
25
29
  "./resolver": {
26
30
  "types": "./dist/resolver.d.ts",
31
+ "bun": "./src/resolver.ts",
27
32
  "default": "./dist/resolver.js"
28
33
  }
29
34
  },
30
35
  "files": [
31
36
  "dist",
37
+ "src",
32
38
  "README.md"
33
39
  ],
34
40
  "repository": {
@@ -0,0 +1,218 @@
1
+ // AWS Signature Version 4 signer — dependency-free, over `node:crypto`.
2
+ //
3
+ // Used by `components/replayFake.ts` to RE-SIGN a request on the record-mode
4
+ // egress forward. The app under test signs with throwaway dummy credentials
5
+ // (any AWS SDK refuses to build a request without *some* credential); the
6
+ // broker strips that dummy signature and re-signs the exact outbound request
7
+ // with the real credentials brokered eval-scoped from the platform Secrets
8
+ // store. Because signing happens at forward time over the real outbound
9
+ // canonical request, `x-amz-date` / `x-amz-content-sha256` / `authorization`
10
+ // are always internally consistent — which is exactly what static header
11
+ // injection cannot achieve for SigV4 (the signature is a keyed HMAC over the
12
+ // whole request, not a static token).
13
+ //
14
+ // Scope note: this implements the standard non-S3 canonicalization (path
15
+ // URI-encoded once more on top of the wire path; query canonicalized with
16
+ // %20). That is correct for the single-host JSON services this targets
17
+ // (DynamoDB, SQS, STS, …). S3 (single-encoded path, virtual-hosted style)
18
+ // and streaming/chunked signatures are out of scope.
19
+
20
+ import { createHash, createHmac } from "node:crypto";
21
+
22
+ export interface SigV4Credentials {
23
+ accessKeyId: string;
24
+ secretAccessKey: string;
25
+ /** Session token for temporary credentials (STS/assume-role). */
26
+ sessionToken?: string;
27
+ }
28
+
29
+ export interface SigV4Scope {
30
+ region: string;
31
+ service: string;
32
+ }
33
+
34
+ function hmac(key: Buffer | string, data: string): Buffer {
35
+ return createHmac("sha256", key).update(data, "utf8").digest();
36
+ }
37
+
38
+ function sha256Hex(data: Uint8Array | string): string {
39
+ const buf = typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data);
40
+ return createHash("sha256").update(buf).digest("hex");
41
+ }
42
+
43
+ /** RFC 3986 URI-encode per the SigV4 rules: unreserved chars pass through,
44
+ * everything else is `%XX` over the UTF-8 bytes. `/` is encoded only when
45
+ * `encodeSlash` (true for query components, false for path segments). */
46
+ function awsUriEncode(input: string, encodeSlash: boolean): string {
47
+ let out = "";
48
+ for (const byte of Buffer.from(input, "utf8")) {
49
+ const isUnreserved =
50
+ (byte >= 0x41 && byte <= 0x5a) || // A-Z
51
+ (byte >= 0x61 && byte <= 0x7a) || // a-z
52
+ (byte >= 0x30 && byte <= 0x39) || // 0-9
53
+ byte === 0x2d || // -
54
+ byte === 0x2e || // .
55
+ byte === 0x5f || // _
56
+ byte === 0x7e; // ~
57
+ if (isUnreserved) {
58
+ out += String.fromCharCode(byte);
59
+ } else if (byte === 0x2f && !encodeSlash) {
60
+ out += "/";
61
+ } else {
62
+ out += "%" + byte.toString(16).toUpperCase().padStart(2, "0");
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+
68
+ /** Canonical URI = the absolute path, URI-encoded once more (non-S3 rule).
69
+ * For the near-universal `/` path of JSON services this is just `/`. */
70
+ function canonicalUri(path: string): string {
71
+ if (!path || path === "/") return "/";
72
+ return path
73
+ .split("/")
74
+ .map((seg) => awsUriEncode(seg, false))
75
+ .join("/");
76
+ }
77
+
78
+ /** Canonical query string: each key/value AWS-URI-encoded, sorted by encoded
79
+ * key then value, joined by `&`. Also the exact string to put on the wire so
80
+ * the received query re-canonicalizes to what we signed (URLSearchParams
81
+ * would encode spaces as `+`, diverging from the signed `%20`). */
82
+ function canonicalQuery(params: URLSearchParams): string {
83
+ const pairs: [string, string][] = [];
84
+ for (const [k, v] of params) pairs.push([awsUriEncode(k, true), awsUriEncode(v, true)]);
85
+ pairs.sort((a, b) =>
86
+ a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0,
87
+ );
88
+ return pairs.map(([k, v]) => `${k}=${v}`).join("&");
89
+ }
90
+
91
+ /** `20260722T101112Z` (amzDate) + `20260722` (dateStamp) from a Date. */
92
+ function amzDates(now: Date): { amzDate: string; dateStamp: string } {
93
+ const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
94
+ return { amzDate, dateStamp: amzDate.slice(0, 8) };
95
+ }
96
+
97
+ /** Parse the `<AKID>/<date>/<region>/<service>/aws4_request` credential scope
98
+ * out of an `Authorization` header or an `X-Amz-Credential` query value. */
99
+ function scopeFromCredential(cred: string | null | undefined): SigV4Scope | undefined {
100
+ if (!cred) return undefined;
101
+ const m = /Credential=([^,\s]+)/.exec(cred);
102
+ const scope = m ? m[1] : cred;
103
+ const parts = scope.split("/");
104
+ if (parts.length >= 5 && parts[4] === "aws4_request" && parts[2] && parts[3]) {
105
+ return { region: parts[2], service: parts[3] };
106
+ }
107
+ return undefined;
108
+ }
109
+
110
+ /** Fallback: derive scope from a `<service>.<region>.amazonaws.com` host. */
111
+ function scopeFromHost(host: string): SigV4Scope | undefined {
112
+ if (!host.endsWith(".amazonaws.com")) return undefined;
113
+ const label = host.slice(0, -".amazonaws.com".length);
114
+ const parts = label.split(".");
115
+ if (parts.length === 1 && parts[0]) return { service: parts[0], region: "us-east-1" };
116
+ if (parts.length === 2 && parts[0] && parts[1]) return { service: parts[0], region: parts[1] };
117
+ // `bucket.s3.region` (S3 virtual-host) is out of scope, but recover what we can.
118
+ const s3i = parts.indexOf("s3");
119
+ if (s3i >= 0 && parts[s3i + 1]) return { service: "s3", region: parts[s3i + 1] };
120
+ return undefined;
121
+ }
122
+
123
+ /** Infer the region + service to sign for. The app's own (dummy-credentialed)
124
+ * request still carries a real credential SCOPE — that's authoritative; the
125
+ * host is a fallback. Returns `undefined` when nothing is derivable. */
126
+ export function inferSigV4Scope(args: {
127
+ authorization?: string | null;
128
+ credentialParam?: string | null;
129
+ host: string;
130
+ }): SigV4Scope | undefined {
131
+ return (
132
+ scopeFromCredential(args.authorization) ??
133
+ scopeFromCredential(args.credentialParam) ??
134
+ scopeFromHost(args.host)
135
+ );
136
+ }
137
+
138
+ export interface SignedRequest {
139
+ /** Headers to SET on the outbound request (lowercased). Includes
140
+ * `authorization`, `x-amz-date`, `x-amz-content-sha256`, and
141
+ * `x-amz-security-token` when session credentials are used. */
142
+ headers: Record<string, string>;
143
+ /** Canonical query string to place on the wire (matches the signature). */
144
+ canonicalQuery: string;
145
+ }
146
+
147
+ /** Sign an outbound request with SigV4 (header authorization). `additionalHeaders`
148
+ * (e.g. `x-amz-target`, `content-type`) are folded into the signed set — they
149
+ * must already be present on the outbound request with these exact values. */
150
+ export function signAwsV4Request(args: {
151
+ method: string;
152
+ /** Wire path (as the app sent it, already once-encoded). */
153
+ path: string;
154
+ /** Query params to send (SigV4 `x-amz-*` params already stripped). */
155
+ query: URLSearchParams;
156
+ host: string;
157
+ body: Uint8Array;
158
+ credentials: SigV4Credentials;
159
+ scope: SigV4Scope;
160
+ additionalHeaders?: Record<string, string>;
161
+ now?: Date;
162
+ }): SignedRequest {
163
+ const { amzDate, dateStamp } = amzDates(args.now ?? new Date());
164
+ const payloadHash = sha256Hex(args.body);
165
+
166
+ const signedHeaders: Record<string, string> = {};
167
+ for (const [k, v] of Object.entries(args.additionalHeaders ?? {})) {
168
+ signedHeaders[k.toLowerCase()] = v.trim();
169
+ }
170
+ signedHeaders["host"] = args.host;
171
+ signedHeaders["x-amz-content-sha256"] = payloadHash;
172
+ signedHeaders["x-amz-date"] = amzDate;
173
+ if (args.credentials.sessionToken) {
174
+ signedHeaders["x-amz-security-token"] = args.credentials.sessionToken;
175
+ }
176
+
177
+ const names = Object.keys(signedHeaders).sort();
178
+ const canonicalHeaders = names.map((n) => `${n}:${signedHeaders[n]}\n`).join("");
179
+ const signedHeaderList = names.join(";");
180
+
181
+ const cq = canonicalQuery(args.query);
182
+ const canonicalRequest = [
183
+ args.method.toUpperCase(),
184
+ canonicalUri(args.path),
185
+ cq,
186
+ canonicalHeaders,
187
+ signedHeaderList,
188
+ payloadHash,
189
+ ].join("\n");
190
+
191
+ const credentialScope = `${dateStamp}/${args.scope.region}/${args.scope.service}/aws4_request`;
192
+ const stringToSign = [
193
+ "AWS4-HMAC-SHA256",
194
+ amzDate,
195
+ credentialScope,
196
+ sha256Hex(canonicalRequest),
197
+ ].join("\n");
198
+
199
+ const kDate = hmac("AWS4" + args.credentials.secretAccessKey, dateStamp);
200
+ const kRegion = hmac(kDate, args.scope.region);
201
+ const kService = hmac(kRegion, args.scope.service);
202
+ const kSigning = hmac(kService, "aws4_request");
203
+ const signature = createHmac("sha256", kSigning).update(stringToSign, "utf8").digest("hex");
204
+
205
+ const authorization =
206
+ `AWS4-HMAC-SHA256 Credential=${args.credentials.accessKeyId}/${credentialScope}, ` +
207
+ `SignedHeaders=${signedHeaderList}, Signature=${signature}`;
208
+
209
+ const headers: Record<string, string> = {
210
+ authorization,
211
+ "x-amz-date": amzDate,
212
+ "x-amz-content-sha256": payloadHash,
213
+ };
214
+ if (args.credentials.sessionToken) {
215
+ headers["x-amz-security-token"] = args.credentials.sessionToken;
216
+ }
217
+ return { headers, canonicalQuery: cq };
218
+ }