@m-kopa/launchpad-cli 0.23.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.
- package/CHANGELOG.md +854 -0
- package/README.md +109 -0
- package/dist/auth/browser.d.ts +18 -0
- package/dist/auth/browser.d.ts.map +1 -0
- package/dist/auth/callback-server.d.ts +24 -0
- package/dist/auth/callback-server.d.ts.map +1 -0
- package/dist/auth/discovery.d.ts +25 -0
- package/dist/auth/discovery.d.ts.map +1 -0
- package/dist/auth/flow.d.ts +39 -0
- package/dist/auth/flow.d.ts.map +1 -0
- package/dist/auth/jwt.d.ts +27 -0
- package/dist/auth/jwt.d.ts.map +1 -0
- package/dist/auth/pkce.d.ts +26 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/registration.d.ts +8 -0
- package/dist/auth/registration.d.ts.map +1 -0
- package/dist/auth/session.d.ts +54 -0
- package/dist/auth/session.d.ts.map +1 -0
- package/dist/auth/token.d.ts +37 -0
- package/dist/auth/token.d.ts.map +1 -0
- package/dist/bundle/cron-bundle.d.ts +77 -0
- package/dist/bundle/cron-bundle.d.ts.map +1 -0
- package/dist/bundle/cwd-walker.d.ts +43 -0
- package/dist/bundle/cwd-walker.d.ts.map +1 -0
- package/dist/bundle/orchestrate.d.ts +51 -0
- package/dist/bundle/orchestrate.d.ts.map +1 -0
- package/dist/bundle/upload.d.ts +66 -0
- package/dist/bundle/upload.d.ts.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +9757 -0
- package/dist/clone/git-init.d.ts +18 -0
- package/dist/clone/git-init.d.ts.map +1 -0
- package/dist/clone/tar-extract.d.ts +59 -0
- package/dist/clone/tar-extract.d.ts.map +1 -0
- package/dist/commands/apps.d.ts +14 -0
- package/dist/commands/apps.d.ts.map +1 -0
- package/dist/commands/channel-auth.d.ts +31 -0
- package/dist/commands/channel-auth.d.ts.map +1 -0
- package/dist/commands/clone.d.ts +3 -0
- package/dist/commands/clone.d.ts.map +1 -0
- package/dist/commands/create.d.ts +27 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/deploy-flags.d.ts +75 -0
- package/dist/commands/deploy-flags.d.ts.map +1 -0
- package/dist/commands/deploy-modes.d.ts +59 -0
- package/dist/commands/deploy-modes.d.ts.map +1 -0
- package/dist/commands/deploy.d.ts +29 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/destroy.d.ts +14 -0
- package/dist/commands/destroy.d.ts.map +1 -0
- package/dist/commands/envvars.d.ts +28 -0
- package/dist/commands/envvars.d.ts.map +1 -0
- package/dist/commands/generate.d.ts +3 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/groups-whoami.d.ts +3 -0
- package/dist/commands/groups-whoami.d.ts.map +1 -0
- package/dist/commands/groups.d.ts +3 -0
- package/dist/commands/groups.d.ts.map +1 -0
- package/dist/commands/init.d.ts +44 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logs.d.ts +16 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/merge.d.ts +29 -0
- package/dist/commands/merge.d.ts.map +1 -0
- package/dist/commands/plan.d.ts +3 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/pull.d.ts +12 -0
- package/dist/commands/pull.d.ts.map +1 -0
- package/dist/commands/review.d.ts +22 -0
- package/dist/commands/review.d.ts.map +1 -0
- package/dist/commands/rollback.d.ts +3 -0
- package/dist/commands/rollback.d.ts.map +1 -0
- package/dist/commands/secrets-template.d.ts +3 -0
- package/dist/commands/secrets-template.d.ts.map +1 -0
- package/dist/commands/secrets.d.ts +3 -0
- package/dist/commands/secrets.d.ts.map +1 -0
- package/dist/commands/skills.d.ts +13 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/status.d.ts +54 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/update.d.ts +114 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/validate.d.ts +3 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/whoami.d.ts +3 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/deploy/apply.d.ts +29 -0
- package/dist/deploy/apply.d.ts.map +1 -0
- package/dist/deploy/dry-run.d.ts +13 -0
- package/dist/deploy/dry-run.d.ts.map +1 -0
- package/dist/deploy/git-files.d.ts +33 -0
- package/dist/deploy/git-files.d.ts.map +1 -0
- package/dist/deploy/group-pin.d.ts +66 -0
- package/dist/deploy/group-pin.d.ts.map +1 -0
- package/dist/deploy/manifest-state.d.ts +20 -0
- package/dist/deploy/manifest-state.d.ts.map +1 -0
- package/dist/deploy/manifest-status.d.ts +11 -0
- package/dist/deploy/manifest-status.d.ts.map +1 -0
- package/dist/deploy/resolve.d.ts +53 -0
- package/dist/deploy/resolve.d.ts.map +1 -0
- package/dist/deploy/rollback.d.ts +23 -0
- package/dist/deploy/rollback.d.ts.map +1 -0
- package/dist/deploy/runner.d.ts +29 -0
- package/dist/deploy/runner.d.ts.map +1 -0
- package/dist/deploy/stage-exit-codes.d.ts +41 -0
- package/dist/deploy/stage-exit-codes.d.ts.map +1 -0
- package/dist/deploy/status-polling.d.ts +37 -0
- package/dist/deploy/status-polling.d.ts.map +1 -0
- package/dist/deploy/tar-pack.d.ts +22 -0
- package/dist/deploy/tar-pack.d.ts.map +1 -0
- package/dist/detect/index.d.ts +53 -0
- package/dist/detect/index.d.ts.map +1 -0
- package/dist/dispatcher.d.ts +30 -0
- package/dist/dispatcher.d.ts.map +1 -0
- package/dist/groups/client.d.ts +62 -0
- package/dist/groups/client.d.ts.map +1 -0
- package/dist/http/api-client.d.ts +33 -0
- package/dist/http/api-client.d.ts.map +1 -0
- package/dist/http/errors.d.ts +31 -0
- package/dist/http/errors.d.ts.map +1 -0
- package/dist/manifest/load.d.ts +38 -0
- package/dist/manifest/load.d.ts.map +1 -0
- package/dist/manifest/schema.d.ts +3 -0
- package/dist/manifest/schema.d.ts.map +1 -0
- package/dist/postinstall.d.ts +3 -0
- package/dist/postinstall.d.ts.map +1 -0
- package/dist/postinstall.js +37 -0
- package/dist/secrets/env-parse.d.ts +19 -0
- package/dist/secrets/env-parse.d.ts.map +1 -0
- package/dist/secrets/push.d.ts +13 -0
- package/dist/secrets/push.d.ts.map +1 -0
- package/dist/secrets/set.d.ts +19 -0
- package/dist/secrets/set.d.ts.map +1 -0
- package/dist/secrets/status.d.ts +19 -0
- package/dist/secrets/status.d.ts.map +1 -0
- package/dist/types/api.d.ts +112 -0
- package/dist/types/api.d.ts.map +1 -0
- package/dist/update-notifier.d.ts +69 -0
- package/dist/update-notifier.d.ts.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +62 -0
- package/skills/README.md +100 -0
- package/skills/_partials/shell-contract.md +42 -0
- package/skills/launchpad-content-pr/SKILL.md +255 -0
- package/skills/launchpad-deploy/SKILL.md +415 -0
- package/skills/launchpad-deploy-status/SKILL.md +231 -0
- package/skills/launchpad-destroy/SKILL.md +317 -0
- package/skills/launchpad-onboard/SKILL.md +179 -0
- package/skills/launchpad-status/SKILL.md +263 -0
- package/skills/marquee-share/README.md +155 -0
- package/skills/marquee-share/SKILL.md +94 -0
- package/skills/marquee-share/SYNC.md +27 -0
- package/skills/marquee-share/dist/cli.js +896 -0
- package/skills/marquee-share/eslint.config.mjs +71 -0
- package/skills/marquee-share/install.sh +103 -0
- package/skills/marquee-share/package-lock.json +3946 -0
- package/skills/marquee-share/package.json +30 -0
- package/skills/marquee-share/src/auth/PROVENANCE.md +103 -0
- package/skills/marquee-share/src/auth/browser.ts +75 -0
- package/skills/marquee-share/src/auth/callback-server.ts +171 -0
- package/skills/marquee-share/src/auth/discovery.ts +171 -0
- package/skills/marquee-share/src/auth/flow.ts +262 -0
- package/skills/marquee-share/src/auth/index.ts +171 -0
- package/skills/marquee-share/src/auth/jwt.ts +77 -0
- package/skills/marquee-share/src/auth/pkce.ts +79 -0
- package/skills/marquee-share/src/auth/registration.ts +87 -0
- package/skills/marquee-share/src/auth/session.ts +205 -0
- package/skills/marquee-share/src/auth/token.ts +162 -0
- package/skills/marquee-share/src/cli.ts +246 -0
- package/skills/marquee-share/src/config.ts +101 -0
- package/skills/marquee-share/src/render/template.ts +171 -0
- package/skills/marquee-share/src/upload/index.ts +11 -0
- package/skills/marquee-share/src/upload/upload.ts +191 -0
- package/skills/marquee-share/tests/cli.test.ts +281 -0
- package/skills/marquee-share/tests/config.test.ts +119 -0
- package/skills/marquee-share/tests/flow.test.ts +356 -0
- package/skills/marquee-share/tests/no-token-leak.test.ts +240 -0
- package/skills/marquee-share/tests/pkce.test.ts +121 -0
- package/skills/marquee-share/tests/session.test.ts +173 -0
- package/skills/marquee-share/tests/template.test.ts +170 -0
- package/skills/marquee-share/tests/upload.test.ts +311 -0
- package/skills/marquee-share/tsconfig.json +23 -0
- package/skills/marquee-share/vitest.config.ts +15 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// Tests for the wrap + upload layer of the Marquee share skill.
|
|
2
|
+
//
|
|
3
|
+
// `shareToMarquee` is the public surface of Task 4: it wraps
|
|
4
|
+
// already-rendered content HTML into the branded template, POSTs the
|
|
5
|
+
// self-contained document to `${resourceUrl}/api/uploads`, and returns
|
|
6
|
+
// the parsed `view_url`.
|
|
7
|
+
//
|
|
8
|
+
// Like the auth `flow.test.ts`, every external dependency is injected:
|
|
9
|
+
// * `fetcher` — a fake server standing in for the Marquee API.
|
|
10
|
+
// * `tokenProvider` — a fake `getValidToken`, so these tests never
|
|
11
|
+
// touch the real auth layer / browser / session file.
|
|
12
|
+
//
|
|
13
|
+
// What is pinned: the request method + URL, the `Content-Type:
|
|
14
|
+
// text/html` header, the `Authorization: Bearer <token>` header, the
|
|
15
|
+
// body being the WRAPPED branded document (not the raw content), and
|
|
16
|
+
// `view_url` being parsed back out of the JSON response.
|
|
17
|
+
|
|
18
|
+
import { describe, expect, test, vi } from "vitest";
|
|
19
|
+
import { shareToMarquee, MarqueeUploadError } from "../src/upload/index.js";
|
|
20
|
+
import type { SkillConfig } from "../src/config.js";
|
|
21
|
+
|
|
22
|
+
const RESOURCE_URL = "https://marquee.launchpad.m-kopa.us";
|
|
23
|
+
const UPLOADS_ENDPOINT = `${RESOURCE_URL}/api/uploads`;
|
|
24
|
+
const FAKE_TOKEN = "fake-access-token-for-tests";
|
|
25
|
+
|
|
26
|
+
function cfg(): SkillConfig {
|
|
27
|
+
return { resourceUrl: RESOURCE_URL, sessionPath: "/tmp/unused-session.json" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CapturedRequest {
|
|
31
|
+
url: string;
|
|
32
|
+
method: string;
|
|
33
|
+
headers: Headers;
|
|
34
|
+
body: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A fake Marquee `/api/uploads` server. Captures the request it
|
|
39
|
+
* received and answers with a configurable response. Defaults to a
|
|
40
|
+
* 200 with a realistic upload-response body.
|
|
41
|
+
*/
|
|
42
|
+
function makeFakeServer(
|
|
43
|
+
cap: { request?: CapturedRequest },
|
|
44
|
+
response?: { status: number; body: unknown },
|
|
45
|
+
): typeof fetch {
|
|
46
|
+
return vi.fn(
|
|
47
|
+
async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
|
48
|
+
// `input` can be a string, a URL, or a Request. `Request` does
|
|
49
|
+
// not override `toString()` (it would yield "[object Request]"),
|
|
50
|
+
// so read its `url` property; a URL stringifies correctly.
|
|
51
|
+
const url =
|
|
52
|
+
input instanceof Request ? input.url : String(input);
|
|
53
|
+
cap.request = {
|
|
54
|
+
url,
|
|
55
|
+
method: init?.method ?? "GET",
|
|
56
|
+
headers: new Headers(init?.headers),
|
|
57
|
+
body: typeof init?.body === "string" ? init.body : "",
|
|
58
|
+
};
|
|
59
|
+
if (response) {
|
|
60
|
+
return new Response(JSON.stringify(response.body), {
|
|
61
|
+
status: response.status,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return new Response(
|
|
65
|
+
JSON.stringify({
|
|
66
|
+
id: "abc123def456ghi7",
|
|
67
|
+
view_url: `${RESOURCE_URL}/v/abc123def456ghi7`,
|
|
68
|
+
owner_sub: "sub-123",
|
|
69
|
+
created_at: "2026-05-17T00:00:00.000Z",
|
|
70
|
+
size_bytes: 42,
|
|
71
|
+
content_sha256: "0".repeat(64),
|
|
72
|
+
}),
|
|
73
|
+
{ status: 200 },
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
) as unknown as typeof fetch;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A fake `getValidToken` — never touches the real auth layer. */
|
|
80
|
+
function fakeTokenProvider(token = FAKE_TOKEN): () => Promise<string> {
|
|
81
|
+
return vi.fn(async () => token);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe("shareToMarquee — request shape", () => {
|
|
85
|
+
test("POSTs to the resource's /api/uploads endpoint", async () => {
|
|
86
|
+
const cap: { request?: CapturedRequest } = {};
|
|
87
|
+
await shareToMarquee(
|
|
88
|
+
{ title: "T", contentHtml: "<p>hi</p>" },
|
|
89
|
+
{
|
|
90
|
+
config: cfg(),
|
|
91
|
+
fetcher: makeFakeServer(cap),
|
|
92
|
+
tokenProvider: fakeTokenProvider(),
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
expect(cap.request?.url).toBe(UPLOADS_ENDPOINT);
|
|
96
|
+
expect(cap.request?.method).toBe("POST");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("sends Content-Type: text/html", async () => {
|
|
100
|
+
const cap: { request?: CapturedRequest } = {};
|
|
101
|
+
await shareToMarquee(
|
|
102
|
+
{ title: "T", contentHtml: "<p>hi</p>" },
|
|
103
|
+
{
|
|
104
|
+
config: cfg(),
|
|
105
|
+
fetcher: makeFakeServer(cap),
|
|
106
|
+
tokenProvider: fakeTokenProvider(),
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
expect(cap.request?.headers.get("content-type")).toMatch(/^text\/html/);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("sends Authorization: Bearer <token> from the token provider", async () => {
|
|
113
|
+
const cap: { request?: CapturedRequest } = {};
|
|
114
|
+
await shareToMarquee(
|
|
115
|
+
{ title: "T", contentHtml: "<p>hi</p>" },
|
|
116
|
+
{
|
|
117
|
+
config: cfg(),
|
|
118
|
+
fetcher: makeFakeServer(cap),
|
|
119
|
+
tokenProvider: fakeTokenProvider("a-specific-token-value"),
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
expect(cap.request?.headers.get("authorization")).toBe(
|
|
123
|
+
"Bearer a-specific-token-value",
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("the request body is the WRAPPED branded document, not the raw content", async () => {
|
|
128
|
+
const cap: { request?: CapturedRequest } = {};
|
|
129
|
+
await shareToMarquee(
|
|
130
|
+
{ title: "Report", contentHtml: "<p>raw-inner-content</p>" },
|
|
131
|
+
{
|
|
132
|
+
config: cfg(),
|
|
133
|
+
fetcher: makeFakeServer(cap),
|
|
134
|
+
tokenProvider: fakeTokenProvider(),
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
const body = cap.request?.body ?? "";
|
|
138
|
+
// The branded shell wrapped it.
|
|
139
|
+
expect(body.startsWith("<!DOCTYPE html>")).toBe(true);
|
|
140
|
+
// The caller's content is inside the shell.
|
|
141
|
+
expect(body).toContain("<p>raw-inner-content</p>");
|
|
142
|
+
// The escaped title made it into the shell.
|
|
143
|
+
expect(body).toContain("<title>Report</title>");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("respects a custom resourceUrl from config", async () => {
|
|
147
|
+
const cap: { request?: CapturedRequest } = {};
|
|
148
|
+
const preview = "https://marquee-preview.example.com";
|
|
149
|
+
await shareToMarquee(
|
|
150
|
+
{ title: "T", contentHtml: "<p>x</p>" },
|
|
151
|
+
{
|
|
152
|
+
config: { resourceUrl: preview, sessionPath: "/tmp/s.json" },
|
|
153
|
+
fetcher: makeFakeServer(cap),
|
|
154
|
+
tokenProvider: fakeTokenProvider(),
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
expect(cap.request?.url).toBe(`${preview}/api/uploads`);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("shareToMarquee — response parsing", () => {
|
|
162
|
+
test("returns the parsed view_url from a 200 response", async () => {
|
|
163
|
+
const cap: { request?: CapturedRequest } = {};
|
|
164
|
+
const result = await shareToMarquee(
|
|
165
|
+
{ title: "T", contentHtml: "<p>x</p>" },
|
|
166
|
+
{
|
|
167
|
+
config: cfg(),
|
|
168
|
+
fetcher: makeFakeServer(cap, {
|
|
169
|
+
status: 200,
|
|
170
|
+
body: {
|
|
171
|
+
id: "zzz999",
|
|
172
|
+
view_url: "https://marquee.launchpad.m-kopa.us/v/zzz999",
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
tokenProvider: fakeTokenProvider(),
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
expect(result.viewUrl).toBe(
|
|
179
|
+
"https://marquee.launchpad.m-kopa.us/v/zzz999",
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("returns the id alongside the view_url when present", async () => {
|
|
184
|
+
const result = await shareToMarquee(
|
|
185
|
+
{ title: "T", contentHtml: "<p>x</p>" },
|
|
186
|
+
{
|
|
187
|
+
config: cfg(),
|
|
188
|
+
fetcher: makeFakeServer(
|
|
189
|
+
{},
|
|
190
|
+
{
|
|
191
|
+
status: 200,
|
|
192
|
+
body: { id: "the-id-16chars77", view_url: `${RESOURCE_URL}/v/the-id-16chars77` },
|
|
193
|
+
},
|
|
194
|
+
),
|
|
195
|
+
tokenProvider: fakeTokenProvider(),
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
expect(result.id).toBe("the-id-16chars77");
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("shareToMarquee — error handling", () => {
|
|
203
|
+
test("throws MarqueeUploadError on a non-2xx response", async () => {
|
|
204
|
+
await expect(
|
|
205
|
+
shareToMarquee(
|
|
206
|
+
{ title: "T", contentHtml: "<p>x</p>" },
|
|
207
|
+
{
|
|
208
|
+
config: cfg(),
|
|
209
|
+
fetcher: makeFakeServer(
|
|
210
|
+
{},
|
|
211
|
+
{ status: 401, body: { error: "unauthenticated" } },
|
|
212
|
+
),
|
|
213
|
+
tokenProvider: fakeTokenProvider(),
|
|
214
|
+
},
|
|
215
|
+
),
|
|
216
|
+
).rejects.toBeInstanceOf(MarqueeUploadError);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("the upload error carries the HTTP status", async () => {
|
|
220
|
+
await shareToMarquee(
|
|
221
|
+
{ title: "T", contentHtml: "<p>x</p>" },
|
|
222
|
+
{
|
|
223
|
+
config: cfg(),
|
|
224
|
+
fetcher: makeFakeServer(
|
|
225
|
+
{},
|
|
226
|
+
{ status: 413, body: { error: "payload_too_large" } },
|
|
227
|
+
),
|
|
228
|
+
tokenProvider: fakeTokenProvider(),
|
|
229
|
+
},
|
|
230
|
+
).catch((e: unknown) => {
|
|
231
|
+
expect(e).toBeInstanceOf(MarqueeUploadError);
|
|
232
|
+
expect((e as MarqueeUploadError).status).toBe(413);
|
|
233
|
+
});
|
|
234
|
+
expect.assertions(2);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("throws MarqueeUploadError when a 200 response lacks view_url", async () => {
|
|
238
|
+
await expect(
|
|
239
|
+
shareToMarquee(
|
|
240
|
+
{ title: "T", contentHtml: "<p>x</p>" },
|
|
241
|
+
{
|
|
242
|
+
config: cfg(),
|
|
243
|
+
fetcher: makeFakeServer({}, { status: 200, body: { id: "x" } }),
|
|
244
|
+
tokenProvider: fakeTokenProvider(),
|
|
245
|
+
},
|
|
246
|
+
),
|
|
247
|
+
).rejects.toBeInstanceOf(MarqueeUploadError);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("throws MarqueeUploadError on a non-JSON 200 response", async () => {
|
|
251
|
+
const fetcher = vi.fn(
|
|
252
|
+
async () => new Response("not json", { status: 200 }),
|
|
253
|
+
) as unknown as typeof fetch;
|
|
254
|
+
await expect(
|
|
255
|
+
shareToMarquee(
|
|
256
|
+
{ title: "T", contentHtml: "<p>x</p>" },
|
|
257
|
+
{ config: cfg(), fetcher, tokenProvider: fakeTokenProvider() },
|
|
258
|
+
),
|
|
259
|
+
).rejects.toBeInstanceOf(MarqueeUploadError);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// `JSON.parse` happily yields `null`, an array, or a bare primitive
|
|
263
|
+
// for valid-but-wrong-shape JSON. Reading `.view_url` off `null`
|
|
264
|
+
// would throw a raw `TypeError`; these cases must instead surface as
|
|
265
|
+
// a typed `MarqueeUploadError`.
|
|
266
|
+
test.each([
|
|
267
|
+
["a literal `null` body", null],
|
|
268
|
+
["a JSON array body", ["not", "an", "object"]],
|
|
269
|
+
["a bare-number body", 42],
|
|
270
|
+
["a bare-string body", "ok"],
|
|
271
|
+
])(
|
|
272
|
+
"throws MarqueeUploadError on a 200 response with %s",
|
|
273
|
+
async (_label, body) => {
|
|
274
|
+
await expect(
|
|
275
|
+
shareToMarquee(
|
|
276
|
+
{ title: "T", contentHtml: "<p>x</p>" },
|
|
277
|
+
{
|
|
278
|
+
config: cfg(),
|
|
279
|
+
fetcher: makeFakeServer({}, { status: 200, body }),
|
|
280
|
+
tokenProvider: fakeTokenProvider(),
|
|
281
|
+
},
|
|
282
|
+
),
|
|
283
|
+
).rejects.toBeInstanceOf(MarqueeUploadError);
|
|
284
|
+
},
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("shareToMarquee — token-leak safety", () => {
|
|
289
|
+
test("a thrown upload error does not embed the bearer token", async () => {
|
|
290
|
+
const secretToken = "SENTINEL-UPLOAD-TOKEN-deadbeef";
|
|
291
|
+
let caught: unknown;
|
|
292
|
+
try {
|
|
293
|
+
await shareToMarquee(
|
|
294
|
+
{ title: "T", contentHtml: "<p>x</p>" },
|
|
295
|
+
{
|
|
296
|
+
config: cfg(),
|
|
297
|
+
fetcher: makeFakeServer({}, { status: 500, body: { error: "boom" } }),
|
|
298
|
+
tokenProvider: fakeTokenProvider(secretToken),
|
|
299
|
+
},
|
|
300
|
+
);
|
|
301
|
+
} catch (e) {
|
|
302
|
+
caught = e;
|
|
303
|
+
}
|
|
304
|
+
expect(caught).toBeInstanceOf(MarqueeUploadError);
|
|
305
|
+
const serialised = `${String(caught)} ${JSON.stringify(
|
|
306
|
+
caught,
|
|
307
|
+
Object.getOwnPropertyNames(caught),
|
|
308
|
+
)}`;
|
|
309
|
+
expect(serialised).not.toContain(secretToken);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2022",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noUncheckedIndexedAccess": true,
|
|
8
|
+
"exactOptionalPropertyTypes": true,
|
|
9
|
+
"noImplicitOverride": true,
|
|
10
|
+
"noUnusedLocals": true,
|
|
11
|
+
"noUnusedParameters": true,
|
|
12
|
+
"noFallthroughCasesInSwitch": true,
|
|
13
|
+
"lib": ["es2022"],
|
|
14
|
+
"types": ["node"],
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"verbatimModuleSyntax": true,
|
|
18
|
+
"resolveJsonModule": true,
|
|
19
|
+
"noEmit": true
|
|
20
|
+
},
|
|
21
|
+
"include": ["src/**/*", "tests/**/*"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// vitest.config.ts — plain Node pool.
|
|
2
|
+
//
|
|
3
|
+
// The skill runs as a Node/bun program on a developer's machine. It
|
|
4
|
+
// legitimately uses `node:fs`, `node:http`, `node:crypto`,
|
|
5
|
+
// `node:child_process` — unlike the marquee Pages app (which targets
|
|
6
|
+
// workerd). Vitest's default Node pool is the right harness, scoped
|
|
7
|
+
// to this package's own `tests/` directory only.
|
|
8
|
+
|
|
9
|
+
import { defineConfig } from "vitest/config";
|
|
10
|
+
|
|
11
|
+
export default defineConfig({
|
|
12
|
+
test: {
|
|
13
|
+
include: ["tests/**/*.test.ts"],
|
|
14
|
+
},
|
|
15
|
+
});
|