@ashlr/stack-core 0.1.1 → 0.2.2
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.
- package/LICENSE +1 -1
- package/README.md +26 -15
- package/package.json +1 -1
- package/src/__tests__/_harness.ts +67 -7
- package/src/__tests__/agent-onboard.test.ts +217 -0
- package/src/__tests__/api-key-providers.test.ts +6 -2
- package/src/__tests__/base-urls.test.ts +162 -0
- package/src/__tests__/catalog-index.test.ts +14 -20
- package/src/__tests__/cloudflare-provider.test.ts +9 -10
- package/src/__tests__/completion-ci.test.ts +239 -23
- package/src/__tests__/config-own-services.test.ts +183 -0
- package/src/__tests__/config-split.test.ts +206 -2
- package/src/__tests__/config.test.ts +2 -1
- package/src/__tests__/credential-staging.test.ts +659 -0
- package/src/__tests__/deps.test.ts +2 -1
- package/src/__tests__/detect-source.test.ts +1 -1
- package/src/__tests__/github-provider.test.ts +3 -2
- package/src/__tests__/helpers.test.ts +24 -4
- package/src/__tests__/http.test.ts +2 -2
- package/src/__tests__/inference.test.ts +19 -13
- package/src/__tests__/interactive-prompt.test.ts +106 -0
- package/src/__tests__/mcp-mirror.test.ts +116 -0
- package/src/__tests__/neon-provider.test.ts +136 -3
- package/src/__tests__/new-providers.test.ts +470 -0
- package/src/__tests__/oauth-port.test.ts +84 -0
- package/src/__tests__/openai-provider.test.ts +1 -1
- package/src/__tests__/phantom-contract.test.ts +177 -0
- package/src/__tests__/phantom-harness.test.ts +125 -8
- package/src/__tests__/phantom-project-preflight.test.ts +118 -0
- package/src/__tests__/phantom-wire.test.ts +31 -20
- package/src/__tests__/pipeline-integration.test.ts +1 -1
- package/src/__tests__/pipeline-recovery.test.ts +29 -28
- package/src/__tests__/pipeline-rollback.test.ts +509 -0
- package/src/__tests__/pipeline.test.ts +3 -1
- package/src/__tests__/pm-detect.test.ts +113 -0
- package/src/__tests__/provider-deprovision.test.ts +269 -0
- package/src/__tests__/provider-parity.test.ts +46 -0
- package/src/__tests__/publish-script.test.ts +659 -0
- package/src/__tests__/recipe.test.ts +4 -2
- package/src/__tests__/recommend-apply-e2e.test.ts +14 -27
- package/src/__tests__/registry.test.ts +11 -2
- package/src/__tests__/release-artifacts.test.ts +55 -0
- package/src/__tests__/secret-redaction.test.ts +10 -10
- package/src/__tests__/security.test.ts +9 -10
- package/src/__tests__/supabase-provider.test.ts +6 -4
- package/src/__tests__/swap.test.ts +114 -0
- package/src/__tests__/telemetry.test.ts +354 -0
- package/src/__tests__/template-litellm-gateway.test.ts +41 -0
- package/src/__tests__/template-temporal-worker-phantom.test.ts +31 -0
- package/src/__tests__/templates.test.ts +1 -1
- package/src/__tests__/turso-provider.test.ts +2 -5
- package/src/agent-onboard.ts +346 -0
- package/src/ai/catalog-index.ts +68 -10
- package/src/ai/circuit-breaker.ts +3 -6
- package/src/ai/cost-tracker.ts +6 -15
- package/src/ai/inference.ts +19 -11
- package/src/ai/phantom-wire.ts +29 -52
- package/src/ai/recipe.ts +69 -0
- package/src/base-urls.ts +71 -0
- package/src/catalog.ts +281 -2
- package/src/config.ts +31 -18
- package/src/detect-source.ts +36 -8
- package/src/detect.ts +13 -5
- package/src/http.ts +6 -5
- package/src/index.ts +15 -0
- package/src/mcp-writer.ts +97 -2
- package/src/oauth.ts +85 -53
- package/src/phantom.ts +193 -15
- package/src/pipeline.ts +177 -68
- package/src/pm-detect.ts +56 -0
- package/src/providers/_api-key.ts +18 -17
- package/src/providers/_base.ts +28 -1
- package/src/providers/_credential-stage.ts +237 -0
- package/src/providers/_helpers.ts +55 -9
- package/src/providers/anthropic.ts +2 -1
- package/src/providers/auth0.ts +27 -0
- package/src/providers/aws.ts +32 -33
- package/src/providers/cloudflare.ts +18 -37
- package/src/providers/convex.ts +1 -2
- package/src/providers/datadog.ts +29 -0
- package/src/providers/digitalocean.ts +30 -0
- package/src/providers/gcp.ts +36 -0
- package/src/providers/github.ts +37 -35
- package/src/providers/grafana.ts +29 -0
- package/src/providers/hetzner.ts +29 -0
- package/src/providers/index.ts +13 -1
- package/src/providers/launchdarkly.ts +33 -0
- package/src/providers/mixpanel.ts +37 -0
- package/src/providers/neon.ts +50 -26
- package/src/providers/openai.ts +2 -1
- package/src/providers/plausible.ts +26 -0
- package/src/providers/sentry.ts +17 -17
- package/src/providers/stripe.ts +1 -2
- package/src/providers/supabase.ts +66 -37
- package/src/providers/turso.ts +49 -38
- package/src/providers/vercel.ts +50 -20
- package/src/providers/workos.ts +27 -0
- package/src/registry.ts +7 -11
- package/src/swap.ts +108 -0
- package/src/telemetry.ts +267 -0
- package/src/templates.ts +6 -2
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ProviderContext } from "../providers/_base.ts";
|
|
3
|
+
import { type Harness, readVault, setupFakePhantom } from "./_harness.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests for the Wave 4 key-only provider adapters:
|
|
7
|
+
* auth0, workos, mixpanel, plausible, datadog, grafana, gcp,
|
|
8
|
+
* digitalocean, hetzner, launchdarkly.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
describe("Wave 4 providers", () => {
|
|
12
|
+
let h: Harness;
|
|
13
|
+
let realFetch: typeof fetch;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Provider logic after a token is read; real Phantom 0.7.9 refuses headless reveal.
|
|
17
|
+
h = setupFakePhantom({}, { legacyHeadlessReveal: true });
|
|
18
|
+
realFetch = globalThis.fetch;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
globalThis.fetch = realFetch;
|
|
23
|
+
h.cleanup();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// ── auth0 ────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
test("auth0: login prompts for AUTH0_DOMAIN", async () => {
|
|
29
|
+
const auth0 = (await import("../providers/auth0.ts")).default;
|
|
30
|
+
expect(auth0.name).toBe("auth0");
|
|
31
|
+
expect(auth0.authKind).toBe("api_key");
|
|
32
|
+
// secretName is AUTH0_DOMAIN — confirmed by the howTo message
|
|
33
|
+
expect(auth0.displayName).toBe("Auth0");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("auth0: materialize stores AUTH0_DOMAIN key", async () => {
|
|
37
|
+
globalThis.fetch = (async () =>
|
|
38
|
+
new Response(JSON.stringify({ issuer: "https://myapp.us.auth0.com/" }), {
|
|
39
|
+
status: 200,
|
|
40
|
+
})) as unknown as typeof fetch;
|
|
41
|
+
|
|
42
|
+
const auth0 = (await import("../providers/auth0.ts")).default;
|
|
43
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
44
|
+
const auth = {
|
|
45
|
+
token: "myapp.us.auth0.com",
|
|
46
|
+
identity: { issuer: "https://myapp.us.auth0.com/" },
|
|
47
|
+
};
|
|
48
|
+
const resource = await auth0.provision(ctx, auth, {});
|
|
49
|
+
const materialized = await auth0.materialize(ctx, resource, auth);
|
|
50
|
+
expect(materialized.secrets.AUTH0_DOMAIN).toBe("myapp.us.auth0.com");
|
|
51
|
+
expect((await readVault(h.dir)).AUTH0_DOMAIN).toBe("myapp.us.auth0.com");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("auth0: healthcheck returns error on 401", async () => {
|
|
55
|
+
globalThis.fetch = (async () =>
|
|
56
|
+
new Response("unauthorized", { status: 401 })) as unknown as typeof fetch;
|
|
57
|
+
|
|
58
|
+
const { addSecret } = await import("../phantom.ts");
|
|
59
|
+
await addSecret("AUTH0_DOMAIN", "bad.auth0.com");
|
|
60
|
+
|
|
61
|
+
const auth0 = (await import("../providers/auth0.ts")).default;
|
|
62
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
63
|
+
const status = await auth0.healthcheck!(ctx, {
|
|
64
|
+
provider: "auth0",
|
|
65
|
+
secrets: ["AUTH0_DOMAIN"],
|
|
66
|
+
created_at: new Date().toISOString(),
|
|
67
|
+
});
|
|
68
|
+
expect(status.kind).toBe("error");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ── workos ───────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
test("workos: materialize stores WORKOS_API_KEY", async () => {
|
|
74
|
+
globalThis.fetch = (async () =>
|
|
75
|
+
new Response(JSON.stringify({ data: [] }), { status: 200 })) as unknown as typeof fetch;
|
|
76
|
+
|
|
77
|
+
const workos = (await import("../providers/workos.ts")).default;
|
|
78
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
79
|
+
const auth = { token: "sk_workos_fake", identity: { organizations: "0" } };
|
|
80
|
+
const resource = await workos.provision(ctx, auth, {});
|
|
81
|
+
const materialized = await workos.materialize(ctx, resource, auth);
|
|
82
|
+
expect(materialized.secrets.WORKOS_API_KEY).toBe("sk_workos_fake");
|
|
83
|
+
const vault = await readVault(h.dir);
|
|
84
|
+
expect(vault.WORKOS_API_KEY).toBe("sk_workos_fake");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("workos: verify calls /organizations endpoint", async () => {
|
|
88
|
+
let capturedUrl = "";
|
|
89
|
+
globalThis.fetch = (async (url: string | URL | Request) => {
|
|
90
|
+
capturedUrl = String(url);
|
|
91
|
+
return new Response(JSON.stringify({ data: [] }), { status: 200 });
|
|
92
|
+
}) as unknown as typeof fetch;
|
|
93
|
+
|
|
94
|
+
const workos = (await import("../providers/workos.ts")).default;
|
|
95
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
96
|
+
const auth = { token: "sk_workos_fake", identity: {} as Record<string, string> };
|
|
97
|
+
await workos.provision(ctx, auth, {});
|
|
98
|
+
await workos.materialize(ctx, { id: "default", displayName: "WorkOS" }, auth);
|
|
99
|
+
// trigger verify via healthcheck
|
|
100
|
+
const { addSecret } = await import("../phantom.ts");
|
|
101
|
+
await expect(addSecret("WORKOS_API_KEY", "sk_workos_fake")).rejects.toMatchObject({
|
|
102
|
+
code: "PHANTOM_SECRET_EXISTS",
|
|
103
|
+
});
|
|
104
|
+
await workos.healthcheck!(ctx, {
|
|
105
|
+
provider: "workos",
|
|
106
|
+
secrets: ["WORKOS_API_KEY"],
|
|
107
|
+
created_at: new Date().toISOString(),
|
|
108
|
+
});
|
|
109
|
+
expect(capturedUrl).toContain("workos.com/organizations");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("workos: healthcheck returns error on 401", async () => {
|
|
113
|
+
globalThis.fetch = (async () =>
|
|
114
|
+
new Response("unauthorized", { status: 401 })) as unknown as typeof fetch;
|
|
115
|
+
|
|
116
|
+
const { addSecret } = await import("../phantom.ts");
|
|
117
|
+
await addSecret("WORKOS_API_KEY", "bad-key");
|
|
118
|
+
|
|
119
|
+
const workos = (await import("../providers/workos.ts")).default;
|
|
120
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
121
|
+
const status = await workos.healthcheck!(ctx, {
|
|
122
|
+
provider: "workos",
|
|
123
|
+
secrets: ["WORKOS_API_KEY"],
|
|
124
|
+
created_at: new Date().toISOString(),
|
|
125
|
+
});
|
|
126
|
+
expect(status.kind).toBe("error");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ── mixpanel ─────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
test("mixpanel: materialize stores MIXPANEL_PROJECT_TOKEN", async () => {
|
|
132
|
+
globalThis.fetch = (async () =>
|
|
133
|
+
new Response(JSON.stringify({ status: 1 }), { status: 200 })) as unknown as typeof fetch;
|
|
134
|
+
|
|
135
|
+
const mixpanel = (await import("../providers/mixpanel.ts")).default;
|
|
136
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
137
|
+
const auth = { token: "mp_fake_token", identity: {} as Record<string, string> };
|
|
138
|
+
const resource = await mixpanel.provision(ctx, auth, {});
|
|
139
|
+
const materialized = await mixpanel.materialize(ctx, resource, auth);
|
|
140
|
+
expect(materialized.secrets.MIXPANEL_PROJECT_TOKEN).toBe("mp_fake_token");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("mixpanel: healthcheck returns error on 401", async () => {
|
|
144
|
+
globalThis.fetch = (async () =>
|
|
145
|
+
new Response("unauthorized", { status: 401 })) as unknown as typeof fetch;
|
|
146
|
+
|
|
147
|
+
const { addSecret } = await import("../phantom.ts");
|
|
148
|
+
await addSecret("MIXPANEL_PROJECT_TOKEN", "bad-token");
|
|
149
|
+
|
|
150
|
+
const mixpanel = (await import("../providers/mixpanel.ts")).default;
|
|
151
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
152
|
+
const status = await mixpanel.healthcheck!(ctx, {
|
|
153
|
+
provider: "mixpanel",
|
|
154
|
+
secrets: ["MIXPANEL_PROJECT_TOKEN"],
|
|
155
|
+
created_at: new Date().toISOString(),
|
|
156
|
+
});
|
|
157
|
+
expect(status.kind).toBe("error");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ── plausible ─────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
test("plausible: verify calls /api/v1/sites", async () => {
|
|
163
|
+
let capturedUrl = "";
|
|
164
|
+
globalThis.fetch = (async (url: string | URL | Request) => {
|
|
165
|
+
capturedUrl = String(url);
|
|
166
|
+
return new Response(JSON.stringify({ sites: [{ domain: "example.com" }] }), { status: 200 });
|
|
167
|
+
}) as unknown as typeof fetch;
|
|
168
|
+
|
|
169
|
+
const plausible = (await import("../providers/plausible.ts")).default;
|
|
170
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
171
|
+
const auth = { token: "plausible_fake", identity: {} as Record<string, string> };
|
|
172
|
+
await plausible.provision(ctx, auth, {});
|
|
173
|
+
await plausible.materialize(ctx, { id: "default", displayName: "Plausible" }, auth);
|
|
174
|
+
const { addSecret } = await import("../phantom.ts");
|
|
175
|
+
await expect(addSecret("PLAUSIBLE_API_KEY", "plausible_fake")).rejects.toMatchObject({
|
|
176
|
+
code: "PHANTOM_SECRET_EXISTS",
|
|
177
|
+
});
|
|
178
|
+
await plausible.healthcheck!(ctx, {
|
|
179
|
+
provider: "plausible",
|
|
180
|
+
secrets: ["PLAUSIBLE_API_KEY"],
|
|
181
|
+
created_at: new Date().toISOString(),
|
|
182
|
+
});
|
|
183
|
+
expect(capturedUrl).toContain("plausible.io/api/v1/sites");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("plausible: healthcheck returns error on 401", async () => {
|
|
187
|
+
globalThis.fetch = (async () =>
|
|
188
|
+
new Response("unauthorized", { status: 401 })) as unknown as typeof fetch;
|
|
189
|
+
|
|
190
|
+
const { addSecret } = await import("../phantom.ts");
|
|
191
|
+
await addSecret("PLAUSIBLE_API_KEY", "bad-key");
|
|
192
|
+
|
|
193
|
+
const plausible = (await import("../providers/plausible.ts")).default;
|
|
194
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
195
|
+
const status = await plausible.healthcheck!(ctx, {
|
|
196
|
+
provider: "plausible",
|
|
197
|
+
secrets: ["PLAUSIBLE_API_KEY"],
|
|
198
|
+
created_at: new Date().toISOString(),
|
|
199
|
+
});
|
|
200
|
+
expect(status.kind).toBe("error");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ── datadog ───────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
test("datadog: verify calls /api/v1/validate with DD-API-KEY header", async () => {
|
|
206
|
+
let capturedHeaders: Record<string, string> = {};
|
|
207
|
+
globalThis.fetch = (async (_url: string | URL | Request, init?: RequestInit) => {
|
|
208
|
+
capturedHeaders = (init?.headers ?? {}) as Record<string, string>;
|
|
209
|
+
return new Response(JSON.stringify({ valid: true }), { status: 200 });
|
|
210
|
+
}) as unknown as typeof fetch;
|
|
211
|
+
|
|
212
|
+
const datadog = (await import("../providers/datadog.ts")).default;
|
|
213
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
214
|
+
const auth = { token: "dd_api_fake", identity: {} as Record<string, string> };
|
|
215
|
+
const resource = await datadog.provision(ctx, auth, {});
|
|
216
|
+
await datadog.materialize(ctx, resource, auth);
|
|
217
|
+
const { addSecret } = await import("../phantom.ts");
|
|
218
|
+
await expect(addSecret("DD_API_KEY", "dd_api_fake")).rejects.toMatchObject({
|
|
219
|
+
code: "PHANTOM_SECRET_EXISTS",
|
|
220
|
+
});
|
|
221
|
+
await datadog.healthcheck!(ctx, {
|
|
222
|
+
provider: "datadog",
|
|
223
|
+
secrets: ["DD_API_KEY"],
|
|
224
|
+
created_at: new Date().toISOString(),
|
|
225
|
+
});
|
|
226
|
+
expect(capturedHeaders["DD-API-KEY"]).toBe("dd_api_fake");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("datadog: healthcheck returns ok on valid key", async () => {
|
|
230
|
+
globalThis.fetch = (async () =>
|
|
231
|
+
new Response(JSON.stringify({ valid: true }), { status: 200 })) as unknown as typeof fetch;
|
|
232
|
+
|
|
233
|
+
const { addSecret } = await import("../phantom.ts");
|
|
234
|
+
await addSecret("DD_API_KEY", "dd_api_fake");
|
|
235
|
+
|
|
236
|
+
const datadog = (await import("../providers/datadog.ts")).default;
|
|
237
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
238
|
+
const status = await datadog.healthcheck!(ctx, {
|
|
239
|
+
provider: "datadog",
|
|
240
|
+
secrets: ["DD_API_KEY"],
|
|
241
|
+
created_at: new Date().toISOString(),
|
|
242
|
+
});
|
|
243
|
+
expect(status.kind).toBe("ok");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("datadog: healthcheck returns error on 403", async () => {
|
|
247
|
+
globalThis.fetch = (async () =>
|
|
248
|
+
new Response("forbidden", { status: 403 })) as unknown as typeof fetch;
|
|
249
|
+
|
|
250
|
+
const { addSecret } = await import("../phantom.ts");
|
|
251
|
+
await addSecret("DD_API_KEY", "bad-key");
|
|
252
|
+
|
|
253
|
+
const datadog = (await import("../providers/datadog.ts")).default;
|
|
254
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
255
|
+
const status = await datadog.healthcheck!(ctx, {
|
|
256
|
+
provider: "datadog",
|
|
257
|
+
secrets: ["DD_API_KEY"],
|
|
258
|
+
created_at: new Date().toISOString(),
|
|
259
|
+
});
|
|
260
|
+
expect(status.kind).toBe("error");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// ── grafana ───────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
test("grafana: verify accepts glsa_ prefixed token", async () => {
|
|
266
|
+
const grafana = (await import("../providers/grafana.ts")).default;
|
|
267
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
268
|
+
const auth = {
|
|
269
|
+
token: "glsa_validtoken1234567890abcdef",
|
|
270
|
+
identity: {} as Record<string, string>,
|
|
271
|
+
};
|
|
272
|
+
const resource = await grafana.provision(ctx, auth, {});
|
|
273
|
+
const materialized = await grafana.materialize(ctx, resource, auth);
|
|
274
|
+
expect(materialized.secrets.GRAFANA_API_KEY).toBe("glsa_validtoken1234567890abcdef");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("grafana: verify rejects short tokens", async () => {
|
|
278
|
+
const grafana = (await import("../providers/grafana.ts")).default;
|
|
279
|
+
// Healthcheck calls verify with the stored key. A short/invalid token should return error.
|
|
280
|
+
const { addSecret } = await import("../phantom.ts");
|
|
281
|
+
await addSecret("GRAFANA_API_KEY", "short");
|
|
282
|
+
|
|
283
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
284
|
+
const status = await grafana.healthcheck!(ctx, {
|
|
285
|
+
provider: "grafana",
|
|
286
|
+
secrets: ["GRAFANA_API_KEY"],
|
|
287
|
+
created_at: new Date().toISOString(),
|
|
288
|
+
});
|
|
289
|
+
expect(status.kind).toBe("error");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// ── gcp ────────────────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
test("gcp: verify accepts valid service-account JSON shape", async () => {
|
|
295
|
+
const gcp = (await import("../providers/gcp.ts")).default;
|
|
296
|
+
const validJson = JSON.stringify({
|
|
297
|
+
type: "service_account",
|
|
298
|
+
project_id: "my-project",
|
|
299
|
+
client_email: "sa@my-project.iam.gserviceaccount.com",
|
|
300
|
+
private_key: "-----BEGIN PRIVATE KEY-----...",
|
|
301
|
+
});
|
|
302
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
303
|
+
const auth = { token: validJson, identity: {} as Record<string, string> };
|
|
304
|
+
const resource = await gcp.provision(ctx, auth, {});
|
|
305
|
+
const materialized = await gcp.materialize(ctx, resource, auth);
|
|
306
|
+
expect(materialized.secrets.GCP_SERVICE_ACCOUNT_JSON).toBe(validJson);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("gcp: verify rejects invalid JSON", async () => {
|
|
310
|
+
const gcp = (await import("../providers/gcp.ts")).default;
|
|
311
|
+
const { addSecret } = await import("../phantom.ts");
|
|
312
|
+
await addSecret("GCP_SERVICE_ACCOUNT_JSON", "not-valid-json");
|
|
313
|
+
|
|
314
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
315
|
+
const status = await gcp.healthcheck!(ctx, {
|
|
316
|
+
provider: "gcp",
|
|
317
|
+
secrets: ["GCP_SERVICE_ACCOUNT_JSON"],
|
|
318
|
+
created_at: new Date().toISOString(),
|
|
319
|
+
});
|
|
320
|
+
expect(status.kind).toBe("error");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("gcp: verify rejects JSON missing required fields", async () => {
|
|
324
|
+
const gcp = (await import("../providers/gcp.ts")).default;
|
|
325
|
+
const { addSecret } = await import("../phantom.ts");
|
|
326
|
+
await addSecret("GCP_SERVICE_ACCOUNT_JSON", JSON.stringify({ type: "service_account" }));
|
|
327
|
+
|
|
328
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
329
|
+
const status = await gcp.healthcheck!(ctx, {
|
|
330
|
+
provider: "gcp",
|
|
331
|
+
secrets: ["GCP_SERVICE_ACCOUNT_JSON"],
|
|
332
|
+
created_at: new Date().toISOString(),
|
|
333
|
+
});
|
|
334
|
+
expect(status.kind).toBe("error");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ── digitalocean ─────────────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
test("digitalocean: verify calls /v2/account with Bearer header", async () => {
|
|
340
|
+
let capturedHeader = "";
|
|
341
|
+
globalThis.fetch = (async (_url: string | URL | Request, init?: RequestInit) => {
|
|
342
|
+
const headers = init?.headers as Record<string, string>;
|
|
343
|
+
capturedHeader = headers?.Authorization ?? "";
|
|
344
|
+
return new Response(JSON.stringify({ account: { email: "me@example.com", uuid: "u1" } }), {
|
|
345
|
+
status: 200,
|
|
346
|
+
});
|
|
347
|
+
}) as unknown as typeof fetch;
|
|
348
|
+
|
|
349
|
+
const { addSecret } = await import("../phantom.ts");
|
|
350
|
+
await addSecret("DIGITALOCEAN_TOKEN", "do_fake_token");
|
|
351
|
+
|
|
352
|
+
const digitalocean = (await import("../providers/digitalocean.ts")).default;
|
|
353
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
354
|
+
await digitalocean.healthcheck!(ctx, {
|
|
355
|
+
provider: "digitalocean",
|
|
356
|
+
secrets: ["DIGITALOCEAN_TOKEN"],
|
|
357
|
+
created_at: new Date().toISOString(),
|
|
358
|
+
});
|
|
359
|
+
expect(capturedHeader).toBe("Bearer do_fake_token");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("digitalocean: healthcheck returns error on 401", async () => {
|
|
363
|
+
globalThis.fetch = (async () =>
|
|
364
|
+
new Response("unauthorized", { status: 401 })) as unknown as typeof fetch;
|
|
365
|
+
|
|
366
|
+
const { addSecret } = await import("../phantom.ts");
|
|
367
|
+
await addSecret("DIGITALOCEAN_TOKEN", "bad-token");
|
|
368
|
+
|
|
369
|
+
const digitalocean = (await import("../providers/digitalocean.ts")).default;
|
|
370
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
371
|
+
const status = await digitalocean.healthcheck!(ctx, {
|
|
372
|
+
provider: "digitalocean",
|
|
373
|
+
secrets: ["DIGITALOCEAN_TOKEN"],
|
|
374
|
+
created_at: new Date().toISOString(),
|
|
375
|
+
});
|
|
376
|
+
expect(status.kind).toBe("error");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// ── hetzner ───────────────────────────────────────────────────────────────
|
|
380
|
+
|
|
381
|
+
test("hetzner: verify calls /v1/locations with Bearer header", async () => {
|
|
382
|
+
let capturedUrl = "";
|
|
383
|
+
globalThis.fetch = (async (url: string | URL | Request) => {
|
|
384
|
+
capturedUrl = String(url);
|
|
385
|
+
return new Response(JSON.stringify({ locations: [{ name: "fsn1" }, { name: "nbg1" }] }), {
|
|
386
|
+
status: 200,
|
|
387
|
+
});
|
|
388
|
+
}) as unknown as typeof fetch;
|
|
389
|
+
|
|
390
|
+
const { addSecret } = await import("../phantom.ts");
|
|
391
|
+
await addSecret("HETZNER_API_TOKEN", "hcloud_fake");
|
|
392
|
+
|
|
393
|
+
const hetzner = (await import("../providers/hetzner.ts")).default;
|
|
394
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
395
|
+
await hetzner.healthcheck!(ctx, {
|
|
396
|
+
provider: "hetzner",
|
|
397
|
+
secrets: ["HETZNER_API_TOKEN"],
|
|
398
|
+
created_at: new Date().toISOString(),
|
|
399
|
+
});
|
|
400
|
+
expect(capturedUrl).toContain("hetzner.cloud/v1/locations");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("hetzner: healthcheck returns error on 401", async () => {
|
|
404
|
+
globalThis.fetch = (async () =>
|
|
405
|
+
new Response("unauthorized", { status: 401 })) as unknown as typeof fetch;
|
|
406
|
+
|
|
407
|
+
const { addSecret } = await import("../phantom.ts");
|
|
408
|
+
await addSecret("HETZNER_API_TOKEN", "bad-token");
|
|
409
|
+
|
|
410
|
+
const hetzner = (await import("../providers/hetzner.ts")).default;
|
|
411
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
412
|
+
const status = await hetzner.healthcheck!(ctx, {
|
|
413
|
+
provider: "hetzner",
|
|
414
|
+
secrets: ["HETZNER_API_TOKEN"],
|
|
415
|
+
created_at: new Date().toISOString(),
|
|
416
|
+
});
|
|
417
|
+
expect(status.kind).toBe("error");
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// ── launchdarkly ─────────────────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
test("launchdarkly: verify calls /api/v2/caller-identity", async () => {
|
|
423
|
+
let capturedUrl = "";
|
|
424
|
+
globalThis.fetch = (async (url: string | URL | Request) => {
|
|
425
|
+
capturedUrl = String(url);
|
|
426
|
+
return new Response(
|
|
427
|
+
JSON.stringify({ accountId: "acct_1", tokenType: "personalAccessToken" }),
|
|
428
|
+
{ status: 200 },
|
|
429
|
+
);
|
|
430
|
+
}) as unknown as typeof fetch;
|
|
431
|
+
|
|
432
|
+
const launchdarkly = (await import("../providers/launchdarkly.ts")).default;
|
|
433
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
434
|
+
|
|
435
|
+
// Also confirm materialize stores the token
|
|
436
|
+
const auth = { token: "api-fake-ld-token", identity: {} as Record<string, string> };
|
|
437
|
+
const resource = await launchdarkly.provision(ctx, auth, {});
|
|
438
|
+
const materialized = await launchdarkly.materialize(ctx, resource, auth);
|
|
439
|
+
expect(materialized.secrets.LAUNCHDARKLY_API_TOKEN).toBe("api-fake-ld-token");
|
|
440
|
+
const { addSecret } = await import("../phantom.ts");
|
|
441
|
+
await expect(addSecret("LAUNCHDARKLY_API_TOKEN", "api-fake-ld-token")).rejects.toMatchObject({
|
|
442
|
+
code: "PHANTOM_SECRET_EXISTS",
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Trigger verify via healthcheck to capture the URL
|
|
446
|
+
await launchdarkly.healthcheck!(ctx, {
|
|
447
|
+
provider: "launchdarkly",
|
|
448
|
+
secrets: ["LAUNCHDARKLY_API_TOKEN"],
|
|
449
|
+
created_at: new Date().toISOString(),
|
|
450
|
+
});
|
|
451
|
+
expect(capturedUrl).toContain("caller-identity");
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test("launchdarkly: healthcheck returns error on 401", async () => {
|
|
455
|
+
globalThis.fetch = (async () =>
|
|
456
|
+
new Response("unauthorized", { status: 401 })) as unknown as typeof fetch;
|
|
457
|
+
|
|
458
|
+
const { addSecret } = await import("../phantom.ts");
|
|
459
|
+
await addSecret("LAUNCHDARKLY_API_TOKEN", "bad-token");
|
|
460
|
+
|
|
461
|
+
const launchdarkly = (await import("../providers/launchdarkly.ts")).default;
|
|
462
|
+
const ctx: ProviderContext = { cwd: process.cwd(), interactive: false, log: () => {} };
|
|
463
|
+
const status = await launchdarkly.healthcheck!(ctx, {
|
|
464
|
+
provider: "launchdarkly",
|
|
465
|
+
secrets: ["LAUNCHDARKLY_API_TOKEN"],
|
|
466
|
+
created_at: new Date().toISOString(),
|
|
467
|
+
});
|
|
468
|
+
expect(status.kind).toBe("error");
|
|
469
|
+
});
|
|
470
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oauth-port tests
|
|
3
|
+
*
|
|
4
|
+
* Verifies that captureCallback (via runPkceFlow) uses STACK_OAUTH_PORT (8787)
|
|
5
|
+
* by default and gracefully falls back to an OS-assigned port when 8787 is
|
|
6
|
+
* already occupied.
|
|
7
|
+
*/
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
9
|
+
import { createServer } from "node:http";
|
|
10
|
+
import type { Server } from "node:http";
|
|
11
|
+
import { STACK_OAUTH_PORT } from "../oauth.ts";
|
|
12
|
+
|
|
13
|
+
describe("STACK_OAUTH_PORT", () => {
|
|
14
|
+
test("is exported as 8787", () => {
|
|
15
|
+
expect(STACK_OAUTH_PORT).toBe(8787);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("OAuth loopback port selection", () => {
|
|
20
|
+
let blocker: Server | null = null;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
blocker = null;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
if (blocker) {
|
|
28
|
+
await new Promise<void>((res) => (blocker as Server).close(() => res()));
|
|
29
|
+
blocker = null;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("STACK_OAUTH_PORT constant is 8787", () => {
|
|
34
|
+
expect(STACK_OAUTH_PORT).toBe(8787);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("fallback activates when preferred port is occupied", async () => {
|
|
38
|
+
// Occupy port 8787 so captureCallback must fall back.
|
|
39
|
+
blocker = createServer();
|
|
40
|
+
await new Promise<void>((res, rej) => {
|
|
41
|
+
(blocker as Server).once("error", rej);
|
|
42
|
+
(blocker as Server).listen(STACK_OAUTH_PORT, "127.0.0.1", () => res());
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Spin up a second server using port 0 — mimics what the fallback does.
|
|
46
|
+
const fallback = createServer();
|
|
47
|
+
const port = await new Promise<number>((res, rej) => {
|
|
48
|
+
fallback.once("error", rej);
|
|
49
|
+
fallback.listen(0, "127.0.0.1", () => {
|
|
50
|
+
const addr = fallback.address() as import("node:net").AddressInfo;
|
|
51
|
+
res(addr.port);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// The fallback port must be non-zero and different from the blocked port.
|
|
57
|
+
expect(port).toBeGreaterThan(0);
|
|
58
|
+
expect(port).not.toBe(STACK_OAUTH_PORT);
|
|
59
|
+
} finally {
|
|
60
|
+
await new Promise<void>((res) => fallback.close(() => res()));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("preferred port is available when not blocked", async () => {
|
|
65
|
+
// Confirm 8787 can be bound when nothing else holds it.
|
|
66
|
+
const srv = createServer();
|
|
67
|
+
const port = await new Promise<number>((res, rej) => {
|
|
68
|
+
srv.once("error", (err: NodeJS.ErrnoException) => {
|
|
69
|
+
// If 8787 happens to be in use in the test environment, skip gracefully.
|
|
70
|
+
if (err.code === "EADDRINUSE") res(-1);
|
|
71
|
+
else rej(err);
|
|
72
|
+
});
|
|
73
|
+
srv.listen(STACK_OAUTH_PORT, "127.0.0.1", () => {
|
|
74
|
+
const addr = srv.address() as import("node:net").AddressInfo;
|
|
75
|
+
res(addr.port);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (port !== -1) {
|
|
80
|
+
expect(port).toBe(STACK_OAUTH_PORT);
|
|
81
|
+
await new Promise<void>((res) => srv.close(() => res()));
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import openai from "../providers/openai.ts";
|
|
3
2
|
import type { ProviderContext } from "../providers/_base.ts";
|
|
3
|
+
import openai from "../providers/openai.ts";
|
|
4
4
|
import { type Harness, readVault, setupFakePhantom } from "./_harness.ts";
|
|
5
5
|
|
|
6
6
|
describe("openai provider (mocked fetch)", () => {
|