@specific.dev/spectest 0.26.0 → 0.27.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 (74) hide show
  1. package/dist/aws-sigv4.d.ts +42 -0
  2. package/dist/aws-sigv4.js +166 -0
  3. package/dist/browser.d.ts +314 -0
  4. package/dist/browser.js +1320 -0
  5. package/dist/components/email.d.ts +135 -0
  6. package/dist/components/email.js +271 -0
  7. package/dist/components/expo.d.ts +69 -0
  8. package/dist/components/expo.js +125 -0
  9. package/dist/components/index.d.ts +8 -0
  10. package/dist/components/index.js +18 -0
  11. package/dist/components/k3s.d.ts +143 -0
  12. package/dist/components/k3s.js +1067 -0
  13. package/dist/components/postgres.d.ts +93 -0
  14. package/dist/components/postgres.js +58 -0
  15. package/dist/components/replayFake.d.ts +169 -0
  16. package/dist/components/replayFake.js +738 -0
  17. package/dist/components/s3.d.ts +99 -0
  18. package/dist/components/s3.js +81 -0
  19. package/dist/components/supabase.d.ts +197 -0
  20. package/dist/components/supabase.js +1003 -0
  21. package/dist/daemon.d.ts +1 -0
  22. package/dist/daemon.js +4223 -0
  23. package/dist/ids.d.ts +2 -0
  24. package/{src/ids.ts → dist/ids.js} +46 -50
  25. package/dist/index.d.ts +1183 -0
  26. package/dist/index.js +769 -0
  27. package/dist/ingress.d.ts +114 -0
  28. package/dist/ingress.js +210 -0
  29. package/dist/inspect.d.ts +228 -0
  30. package/dist/inspect.js +429 -0
  31. package/dist/locator.d.ts +260 -0
  32. package/dist/locator.js +293 -0
  33. package/dist/mobile.d.ts +71 -0
  34. package/dist/mobile.js +65 -0
  35. package/dist/record-secrets.d.ts +9 -0
  36. package/{src/record-secrets.ts → dist/record-secrets.js} +13 -15
  37. package/dist/recorder.d.ts +516 -0
  38. package/dist/recorder.js +219 -0
  39. package/dist/redis.d.ts +54 -0
  40. package/dist/redis.js +126 -0
  41. package/dist/replay-bundle.d.ts +38 -0
  42. package/{src/replay-bundle.ts → dist/replay-bundle.js} +29 -47
  43. package/dist/resolver.d.ts +1 -0
  44. package/dist/resolver.js +309 -0
  45. package/dist/s3.d.ts +89 -0
  46. package/dist/s3.js +198 -0
  47. package/dist/sql.d.ts +74 -0
  48. package/dist/sql.js +151 -0
  49. package/dist/terminal.d.ts +161 -0
  50. package/dist/terminal.js +538 -0
  51. package/package.json +24 -9
  52. package/src/browser.ts +0 -1819
  53. package/src/components/email.ts +0 -398
  54. package/src/components/expo.ts +0 -167
  55. package/src/components/index.ts +0 -63
  56. package/src/components/k3s.ts +0 -1312
  57. package/src/components/postgres.ts +0 -105
  58. package/src/components/replayFake.ts +0 -848
  59. package/src/components/s3.ts +0 -132
  60. package/src/components/supabase.ts +0 -1299
  61. package/src/daemon.ts +0 -4969
  62. package/src/index.ts +0 -2350
  63. package/src/ingress.ts +0 -288
  64. package/src/inspect.ts +0 -673
  65. package/src/locator.ts +0 -594
  66. package/src/mobile.ts +0 -133
  67. package/src/recorder.ts +0 -817
  68. package/src/redis.ts +0 -202
  69. package/src/resolver.ts +0 -351
  70. package/src/s3.ts +0 -333
  71. package/src/sql.ts +0 -243
  72. package/src/terminal.ts +0 -740
  73. package/src/vendor/rrweb-plugin-console-record.umd.js +0 -521
  74. package/src/vendor/rrweb-record.min.js +0 -5061
@@ -0,0 +1,93 @@
1
+ import { type SqlClient } from "../sql.js";
2
+ export interface PostgresOptions {
3
+ /** Image tag for the official `postgres` image. Default `"18-alpine"`.
4
+ * Ignored when {@link image} is set. */
5
+ version?: string;
6
+ /**
7
+ * Full image reference, overriding the default `postgres:<version>`. For a
8
+ * Bun-SQL-/wire-compatible image (timescaledb, postgis, pgvector, …) you
9
+ * still get the instrumented client on `ctx.svc.<name>.client`.
10
+ */
11
+ image?: string;
12
+ /** Database created on first boot. */
13
+ database: string;
14
+ /** Superuser name created on first boot. */
15
+ user: string;
16
+ /** Superuser password. */
17
+ password: string;
18
+ /**
19
+ * Whether postgres data persists across container restarts via a bind
20
+ * mount under `.spectest/volumes/<name>/`. Default `true`.
21
+ * Snapshots/forks always preserve state regardless of this flag — the
22
+ * volume just means a plain `docker rm`/`docker run` cycle keeps data.
23
+ */
24
+ persistent?: boolean;
25
+ /** TCP port the container listens on. Default `5432`. */
26
+ port?: number;
27
+ /**
28
+ * CMD args appended after the image entrypoint (keeps the entrypoint, unlike
29
+ * `command`). For extension tuning, e.g.
30
+ * `["postgres", "-c", "timescaledb.max_background_workers=0"]`.
31
+ */
32
+ args?: string[];
33
+ /** Extra environment variables forwarded to the container. */
34
+ env?: Record<string, string>;
35
+ }
36
+ /** Helpers a `postgres(...)` service exposes on `ctx.svc.<name>`. */
37
+ export interface PostgresHelpers {
38
+ /** An instrumented Bun SQL client — each query lands on the test event log
39
+ * alongside http/exec/assertion events, and its rows come back
40
+ * inspect-wrapped so `expect(...)` on them links to the query. See
41
+ * {@link SQL}. */
42
+ client: SqlClient;
43
+ }
44
+ /**
45
+ * A ready-to-use Postgres service. Drop into `environment.services`:
46
+ *
47
+ * ```ts
48
+ * services: {
49
+ * db: postgres({ database: "todos", user: "todos", password: "todos" }),
50
+ * ...
51
+ * }
52
+ * ```
53
+ *
54
+ * Peer services reach it at `<key>:<port>` (e.g. `db:5432` for the
55
+ * example above). Tests get a wired-up Bun SQL client at
56
+ * `ctx.svc.<key>.client` — `await ctx.svc.db.client\`SELECT 1\`` — with
57
+ * every query recorded on the test event log.
58
+ *
59
+ * Point it at a wire-compatible image with `image`/`args`:
60
+ *
61
+ * ```ts
62
+ * db: postgres({
63
+ * image: "timescale/timescaledb:latest-pg17",
64
+ * args: ["postgres", "-c", "timescaledb.max_background_workers=0"],
65
+ * database: "app", user: "app", password: "app",
66
+ * }),
67
+ * ```
68
+ */
69
+ export declare function postgres(opts: PostgresOptions): {
70
+ env: {
71
+ POSTGRES_DB: string;
72
+ POSTGRES_USER: string;
73
+ POSTGRES_PASSWORD: string;
74
+ PGDATA: string;
75
+ };
76
+ volumes: {
77
+ target: string;
78
+ }[];
79
+ ports: number[];
80
+ readyCheck: {
81
+ type: "tcp";
82
+ port: number;
83
+ timeoutSecs: number;
84
+ };
85
+ helpers: ({ name }: {
86
+ name: string;
87
+ }) => PostgresHelpers;
88
+ args?: string[] | undefined;
89
+ image: {
90
+ type: "registry";
91
+ reference: string;
92
+ };
93
+ };
@@ -0,0 +1,58 @@
1
+ import { SQL } from "../sql.js";
2
+ /**
3
+ * A ready-to-use Postgres service. Drop into `environment.services`:
4
+ *
5
+ * ```ts
6
+ * services: {
7
+ * db: postgres({ database: "todos", user: "todos", password: "todos" }),
8
+ * ...
9
+ * }
10
+ * ```
11
+ *
12
+ * Peer services reach it at `<key>:<port>` (e.g. `db:5432` for the
13
+ * example above). Tests get a wired-up Bun SQL client at
14
+ * `ctx.svc.<key>.client` — `await ctx.svc.db.client\`SELECT 1\`` — with
15
+ * every query recorded on the test event log.
16
+ *
17
+ * Point it at a wire-compatible image with `image`/`args`:
18
+ *
19
+ * ```ts
20
+ * db: postgres({
21
+ * image: "timescale/timescaledb:latest-pg17",
22
+ * args: ["postgres", "-c", "timescaledb.max_background_workers=0"],
23
+ * database: "app", user: "app", password: "app",
24
+ * }),
25
+ * ```
26
+ */
27
+ export function postgres(opts) {
28
+ const version = opts.version ?? "18-alpine";
29
+ const reference = opts.image ?? `postgres:${version}`;
30
+ const port = opts.port ?? 5432;
31
+ const persistent = opts.persistent ?? true;
32
+ // `satisfies` (instead of a return-type annotation) preserves the
33
+ // literal type — in particular, the `helpers` factory's return shape.
34
+ // The mapped type in `ServiceHandlesFor<S>` reads that to type
35
+ // `ctx.svc.<name>` as `{ client: SqlClient }`.
36
+ return {
37
+ image: { type: "registry", reference },
38
+ ...(opts.args ? { args: opts.args } : {}),
39
+ env: {
40
+ POSTGRES_DB: opts.database,
41
+ POSTGRES_USER: opts.user,
42
+ POSTGRES_PASSWORD: opts.password,
43
+ // PGDATA lives in a subdir so postgres can initialise inside a bind
44
+ // mount that may not be empty on first boot.
45
+ PGDATA: "/var/lib/postgresql/data/pgdata",
46
+ ...(opts.env ?? {}),
47
+ },
48
+ volumes: persistent ? [{ target: "/var/lib/postgresql/data" }] : [],
49
+ ports: [port],
50
+ readyCheck: { type: "tcp", port, timeoutSecs: 60 },
51
+ helpers: ({ name }) => {
52
+ const url = `postgres://${encodeURIComponent(opts.user)}` +
53
+ `:${encodeURIComponent(opts.password)}` +
54
+ `@${name}:${port}/${encodeURIComponent(opts.database)}`;
55
+ return { client: new SQL(url, { label: name }) };
56
+ },
57
+ };
58
+ }
@@ -0,0 +1,169 @@
1
+ import type { FakeDefinition } from "../index.js";
2
+ /** Which parts of a request define a match against the cassette. */
3
+ export interface ReplayMatch {
4
+ /** Match on HTTP method. Default `true`. */
5
+ method?: boolean;
6
+ /** Match on path (no query). Default `true`. */
7
+ path?: boolean;
8
+ /** Match on normalized query string. Default `true`. */
9
+ query?: boolean;
10
+ /** Match on body: `true` = exact decoded body, `"hash"` = sha256 only.
11
+ * Default off. */
12
+ body?: boolean | "hash";
13
+ /** Header names (lowercase) to include in the match. Default none. */
14
+ headers?: string[];
15
+ }
16
+ /** Match one request dimension. A bare string is an exact match; the
17
+ * object forms cover prefix and regex matching. */
18
+ export type StringMatch = string | {
19
+ exact: string;
20
+ } | {
21
+ startsWith: string;
22
+ } | {
23
+ regex: string;
24
+ };
25
+ /** Request matchers for a credential-injection rule. A rule applies only
26
+ * when EVERY specified dimension matches; omit `match` to always apply. */
27
+ export interface InjectMatch {
28
+ /** Match the request path (no query). */
29
+ path?: StringMatch;
30
+ /** HTTP method(s) — any one matching satisfies it. */
31
+ method?: string | string[];
32
+ /** Query-param matchers (ANDed); each key's value(s) must match. */
33
+ query?: Record<string, StringMatch>;
34
+ /** Header matchers (ANDed; header names case-insensitive). */
35
+ headers?: Record<string, StringMatch>;
36
+ }
37
+ /** One credential-brokering rule. When it matches a request, these headers
38
+ * are SET on the egress forward — overwriting whatever the app sent (so app
39
+ * code can't smuggle a different value past the broker). Header VALUES may
40
+ * embed `{{secret:REF}}` tokens; each `REF` is resolved server-side from the
41
+ * project's Secrets store and pushed eval-scoped — the real value never
42
+ * enters project code, the
43
+ * tarball, the warm-cache hash, or a cassette (it's redacted, fail-closed). */
44
+ export interface InjectRule {
45
+ match?: InjectMatch;
46
+ headers: Record<string, string>;
47
+ }
48
+ /** SigV4 request-signing broker. Unlike `inject` (which SETs a static header
49
+ * value), this RE-SIGNS the outbound request: the app signs with throwaway
50
+ * dummy credentials, we strip that signature and re-sign the exact outbound
51
+ * request with the real credentials — the only way to broker AWS auth, whose
52
+ * signature is a keyed HMAC over the whole request, not a static token.
53
+ *
54
+ * The three fields are **secret refs, resolved directly** from the project's
55
+ * Secrets store (NOT `{{secret:}}` templates) — same eval-scoped push and
56
+ * fail-closed redaction as `inject`. Region + service are inferred from the
57
+ * incoming request (its credential scope, else the host); the app never
58
+ * configures them. */
59
+ export interface AwsSigV4Sign {
60
+ type: "awsSigv4";
61
+ /** Secret ref for the AWS access key id. */
62
+ accessKeyId: string;
63
+ /** Secret ref for the AWS secret access key. */
64
+ secretAccessKey: string;
65
+ /** Optional secret ref for a session token (temporary credentials). */
66
+ sessionToken?: string;
67
+ }
68
+ export type SignConfig = AwsSigV4Sign;
69
+ export interface ReplayFakeOptions {
70
+ /** Stable name — the `ctx.fakes` key. */
71
+ name: string;
72
+ /** The REAL host to fake, e.g. `"api.stripe.com"`. The fake answers for
73
+ * this name on both `http://` (:80) and `https://` (:443), and the same
74
+ * name is forwarded to the real upstream in record mode (over HTTPS,
75
+ * resolved via an external DNS server — see the module header). Your
76
+ * app points at this real host directly; no separate stand-in. */
77
+ host: string;
78
+ /** What defines a request match. Default `{ method, path, query }`. */
79
+ match?: ReplayMatch;
80
+ /** Per-fake credential brokering. Rules are evaluated in
81
+ * order; the first whose `match` matches wins (a rule without `match`
82
+ * matches everything and shadows later rules). Applied at the
83
+ * record-mode egress forward; resolved secret values are redacted from
84
+ * the cassette (fail-closed). */
85
+ inject?: InjectRule[];
86
+ /** SigV4 request signing (AWS). When set, the record-mode forward strips the
87
+ * app's dummy signature and re-signs the outbound request with the brokered
88
+ * credentials. Point your AWS SDK at the real host with any dummy static
89
+ * creds (e.g. `AWS_ACCESS_KEY_ID=test`) — the dummies are stripped and the
90
+ * real ones live only on the outbound wire (redacted from the cassette). */
91
+ sign?: SignConfig;
92
+ /** Extra substrings/patterns to scrub from stored request/response
93
+ * bodies and queries (every injected secret value is always scrubbed). */
94
+ redactPatterns?: (string | RegExp)[];
95
+ /** `"auto"` (default) records under eval and replays under test;
96
+ * `"record"` / `"replay"` force a mode. */
97
+ mode?: "auto" | "record" | "replay";
98
+ }
99
+ export interface CassetteRequest {
100
+ method: string;
101
+ path: string;
102
+ /** Normalized: keys + repeated values sorted, so matching is order-free. */
103
+ query: Record<string, string[]>;
104
+ /** Matched/readable header subset only — never `authorization`/`cookie`. */
105
+ headers: Record<string, string>;
106
+ bodyHash?: string;
107
+ /** Request body (omitted when empty): decoded + redacted UTF-8 text, or
108
+ * base64 for binary payloads (S3 object PUTs, protobuf, …). */
109
+ body?: string;
110
+ /** Encoding of `body`. Absent = `"utf8"` (back-compat with old cassettes). */
111
+ bodyEncoding?: "utf8" | "base64";
112
+ }
113
+ export interface CassetteResponse {
114
+ status: number;
115
+ headers: Record<string, string>;
116
+ /** Decoded + redacted response body. */
117
+ body: string;
118
+ bodyEncoding: "utf8" | "base64";
119
+ }
120
+ export interface CassetteInteraction {
121
+ request: CassetteRequest;
122
+ response: CassetteResponse;
123
+ }
124
+ export interface Cassette {
125
+ version: 1;
126
+ fake: string;
127
+ /** The real host this cassette mirrors (e.g. `api.stripe.com`). */
128
+ host: string;
129
+ interactions: CassetteInteraction[];
130
+ }
131
+ /** Forked-per-test state for one replay fake. */
132
+ export interface CassetteState {
133
+ cassette: Cassette;
134
+ /** Per-match-signature replay cursor (call-order sequencing). */
135
+ cursor: Map<string, number>;
136
+ /** Interactions captured this record session. */
137
+ recorded: CassetteInteraction[];
138
+ dirty: boolean;
139
+ }
140
+ /** What `replay(...)` returns — a small summary that surfaces in the test
141
+ * timeline (recorded as a `fake` event) so the run shows exactly which
142
+ * cassette a test loaded and how many interactions it carried. */
143
+ export interface ReplaySummary {
144
+ /** Filename loaded (under `spectest/recordings/`) or `"(inline)"`. */
145
+ cassette: string;
146
+ /** The real host the cassette mirrors. */
147
+ host: string;
148
+ /** Number of interactions now available for replay. */
149
+ interactions: number;
150
+ }
151
+ export interface ReplayHelpers extends Record<string, unknown> {
152
+ /** Load a cassette into THIS fork's replay set and replay from it,
153
+ * replacing whatever was loaded before and resetting the replay cursor.
154
+ * Cassettes are NOT auto-loaded — call this at the top of a test. Pass a
155
+ * path relative to `spectest/tests/` (e.g. `"recordings/stripe.json"`; it
156
+ * must stay under `spectest/tests/`) or a cassette object; a missing file
157
+ * throws. The returned summary is recorded as a step, so the UI timeline
158
+ * shows what was loaded. */
159
+ replay(file: string | Cassette): ReplaySummary;
160
+ /** Full cassette JSON (loaded interactions + newly recorded, redacted)
161
+ * — the MVP delivery channel: `export default ctx.fakes.<name>.dump()`,
162
+ * then write the result to `spectest/recordings/<name>.json`. */
163
+ dump(): Cassette;
164
+ /** Interactions captured this record session. */
165
+ recorded(): CassetteInteraction[];
166
+ /** Count of interactions captured this record session. */
167
+ count(): number;
168
+ }
169
+ export declare function replayFake(opts: ReplayFakeOptions): FakeDefinition<CassetteState, ReplayHelpers>;