@vibes.diy/prompts 14.3.175 → 14.3.177
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/llms/backend.md +5 -1
- package/llms/spotify.md +149 -72
- package/llms/stripe-subscriptions.md +196 -117
- package/llms/stripe.md +130 -93
- package/llms/use-vibe.md +77 -0
- package/llms/youtube.md +147 -66
- package/package.json +5 -5
package/llms/backend.md
CHANGED
|
@@ -335,7 +335,11 @@ const routes = await ctx.github.routes(); // [{ repo, accountLogin, binderHandle
|
|
|
335
335
|
calls to a service the owner holds a key for (`Authorization: Bearer
|
|
336
336
|
${ctx.secrets.KEY}` to an admitted API). Never write a value anywhere users
|
|
337
337
|
can read it (a doc, a response body). Only the owner can set or rotate
|
|
338
|
-
secrets; handlers see the current values on every invocation.
|
|
338
|
+
secrets; handlers see the current values on every invocation. A feature
|
|
339
|
+
that cannot work until the owner has set one is a setup item: the `/setup`
|
|
340
|
+
verdict and the owner-only Setup surface are the use-vibe docs' *Some apps
|
|
341
|
+
need setup before they work*, and the skill that needs the secret
|
|
342
|
+
contributes the item and its linked steps. **A visitor's
|
|
339
343
|
own account (their GitHub, Threads, Instagram) is never a secret and never a
|
|
340
344
|
pasted token — it is a connection**: declare it in `config.connections`, let
|
|
341
345
|
the platform hold the credential, and read it through `@vibes.diy/social` or
|
package/llms/spotify.md
CHANGED
|
@@ -19,31 +19,38 @@ visitor to a developer console to mint one: that is the flow connections exists
|
|
|
19
19
|
replace. The console trip below is for the **owner** of this app, once, and for
|
|
20
20
|
nobody else.
|
|
21
21
|
|
|
22
|
-
## 1.
|
|
23
|
-
|
|
24
|
-
The owner registers an app with Spotify, once
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
the
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
`
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
22
|
+
## 1. The setup item: the owner's Spotify app credentials
|
|
23
|
+
|
|
24
|
+
The owner registers an app with Spotify, once. The Setup surface the item
|
|
25
|
+
lands on — the owner-only entry in the app's navigation, the verdict from
|
|
26
|
+
`/setup`, the re-read on focus that unlocks search in place — is the use-vibe
|
|
27
|
+
docs' *Some apps need setup before they work*; this recipe contributes one
|
|
28
|
+
item to it. The Spotify item is the credential pair: the backend answers missing-key while either ctx.secrets.SPOTIFY_CLIENT_ID or ctx.secrets.SPOTIFY_CLIENT_SECRET is absent, and its steps link the Spotify developer dashboard by URL and name both secrets in so many words.
|
|
29
|
+
The steps are written for someone who has never opened the dashboard, so each
|
|
30
|
+
one is a link to the exact page and uses the words that page uses:
|
|
31
|
+
|
|
32
|
+
1. Open the Spotify developer dashboard: <https://developer.spotify.com/dashboard>
|
|
33
|
+
(log in with a Spotify account). Press **Create app** — any name and
|
|
34
|
+
description; for **Redirect URIs** put the app's own address, since this
|
|
35
|
+
flow never uses one — and save.
|
|
36
|
+
2. Open the app you made and press **Settings**. Copy the **Client ID**, then
|
|
37
|
+
press **View client secret** and copy that too.
|
|
38
|
+
3. Open this vibe's **Settings** tab on vibes.diy:
|
|
39
|
+
`https://vibes.diy/vibes/mine/<ownerHandle>/<appSlug>/settings` (that is
|
|
40
|
+
**My Vibes** → this vibe → **Settings**). Under **Secrets**, enter
|
|
41
|
+
`SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET`, paste each value, and
|
|
42
|
+
save. From a terminal the same thing is
|
|
43
|
+
`npx vibes-diy secrets set SPOTIFY_CLIENT_ID --vibe <ownerHandle>/<appSlug>`
|
|
44
|
+
and the same for `SPOTIFY_CLIENT_SECRET`.
|
|
45
|
+
4. Come back to the app. Search works the moment both are saved; no redeploy.
|
|
46
|
+
|
|
47
|
+
Those steps are the item's copy on the Setup surface, with the vibe's real
|
|
48
|
+
handle and slug filled in from the `ownerHandle` and `appSlug` the `/setup`
|
|
49
|
+
route hands over. Two secrets means two ways to be half-configured: one item,
|
|
50
|
+
done only when both are present, so one set and one unset reads as *To do*
|
|
51
|
+
rather than as a broken API. In the consumer flow, a search whose pair is not
|
|
52
|
+
set reads *Search is not open yet*; the owner's Setup entry is where the
|
|
53
|
+
steps are, and later it is where a rotated secret goes.
|
|
47
54
|
|
|
48
55
|
Only the owner can set or rotate a secret, and handlers see the current value on
|
|
49
56
|
every invocation — so the page starts working the moment the pair lands, with no
|
|
@@ -141,9 +148,10 @@ API, and `{ "error", "error_description" }` on the token endpoint. Classify and
|
|
|
141
148
|
pass the reason through:
|
|
142
149
|
|
|
143
150
|
- `invalid_client` from the token endpoint — the id and secret do not match a real
|
|
144
|
-
app.
|
|
151
|
+
app. Say so, and point the owner at the Setup entry: the item reads *Done*
|
|
152
|
+
because both values are present, and one of them is wrong.
|
|
145
153
|
- `401` from the Web API — mint a fresh token and retry once; if the second try is
|
|
146
|
-
also 401,
|
|
154
|
+
also 401, say so and point the owner at the Setup entry the same way.
|
|
147
155
|
- `429` — Spotify is rate-limiting the owner's app. The `Retry-After` header is
|
|
148
156
|
seconds; say "Spotify asked us to wait N seconds" rather than "no results".
|
|
149
157
|
- `404` — no such artist, album or track. Keep the id on screen.
|
|
@@ -247,6 +255,16 @@ export async function fetch(request, ctx) {
|
|
|
247
255
|
|
|
248
256
|
// An uncaught throw reaches the app as a bare 500 naming only the error class, so every route answers with a reason.
|
|
249
257
|
try {
|
|
258
|
+
// The setup verdict: ready for everyone, the item (and the handle + slug the Setup screen
|
|
259
|
+
// links with) for the owner alone.
|
|
260
|
+
if (url.pathname === "/setup") {
|
|
261
|
+
const owner = ctx.userInfo?.userHandle === ctx.appInfo.ownerHandle;
|
|
262
|
+
const items = [{ id: "spotify-app", label: "Spotify app credentials", done: credentials(ctx) !== null }];
|
|
263
|
+
const ready = items.every((item) => item.done);
|
|
264
|
+
if (!owner) return json({ ok: true, ready, owner });
|
|
265
|
+
return json({ ok: true, ready, owner, items, ownerHandle: ctx.appInfo.ownerHandle, appSlug: ctx.appInfo.appSlug });
|
|
266
|
+
}
|
|
267
|
+
|
|
250
268
|
if (url.pathname === "/search") {
|
|
251
269
|
const q = (url.searchParams.get("q") ?? "").trim();
|
|
252
270
|
if (q === "") return json({ ok: true, tracks: [], artists: [] });
|
|
@@ -305,21 +323,71 @@ function artistRow(a) {
|
|
|
305
323
|
|
|
306
324
|
### `App.jsx`
|
|
307
325
|
```jsx app=spotify-browser
|
|
308
|
-
import React, { useState } from "react";
|
|
309
|
-
import {
|
|
326
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
327
|
+
import { apiFetch } from "use-vibes";
|
|
328
|
+
import { Badge, Button, Field, Heading, Input, Row, Stack, Text } from "@vibes.diy/pouf";
|
|
329
|
+
|
|
330
|
+
// The setup verdict, read on mount and again whenever the app comes back into view: the owner
|
|
331
|
+
// sets the pair in another tab and returns, and search unlocks without a reload.
|
|
332
|
+
function useSetup() {
|
|
333
|
+
const [setup, setSetup] = useState(null); // { ready, owner, items?, ownerHandle?, appSlug? }
|
|
334
|
+
const latest = useRef(0);
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
// Mount, focus and visibility reads overlap; only the latest one may land, or a slow stale
|
|
337
|
+
// answer overwrites a fresh one.
|
|
338
|
+
const load = () => {
|
|
339
|
+
const n = ++latest.current;
|
|
340
|
+
apiFetch("/setup").then((r) => r.json()).catch(() => ({ ready: false, owner: false })).then((s) => { if (n === latest.current) setSetup(s); });
|
|
341
|
+
};
|
|
342
|
+
load();
|
|
343
|
+
const onVisible = () => { if (document.visibilityState === "visible") load(); };
|
|
344
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
345
|
+
window.addEventListener("focus", load);
|
|
346
|
+
return () => { document.removeEventListener("visibilitychange", onVisible); window.removeEventListener("focus", load); };
|
|
347
|
+
}, []);
|
|
348
|
+
return setup;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// The item's steps: each one a link to the exact page, in that page's own words, with this
|
|
352
|
+
// vibe's real handle and slug filled in.
|
|
353
|
+
function setupSteps(vibe) {
|
|
354
|
+
return [
|
|
355
|
+
{ href: "https://developer.spotify.com/dashboard", link: "Spotify developer dashboard", text: ": log in, press Create app (any name; put this app's own address under Redirect URIs), and save." },
|
|
356
|
+
{ href: "https://developer.spotify.com/dashboard", link: "your app's Settings", text: ": copy the Client ID, then press View client secret and copy that too." },
|
|
357
|
+
{ href: `https://vibes.diy/vibes/mine/${vibe}/settings`, link: "this vibe's Settings tab", text: `: under Secrets, enter SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET, paste each value, and save — or run npx vibes-diy secrets set SPOTIFY_CLIENT_ID --vibe ${vibe} and the same for SPOTIFY_CLIENT_SECRET.` },
|
|
358
|
+
];
|
|
359
|
+
}
|
|
310
360
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
361
|
+
// The Setup surface, the owner's alone.
|
|
362
|
+
function SetupScreen({ setup }) {
|
|
363
|
+
const item = setup.items.find((i) => i.id === "spotify-app");
|
|
364
|
+
return (
|
|
365
|
+
<Stack>
|
|
366
|
+
<Heading>Setup</Heading>
|
|
367
|
+
<Row gap={2}>
|
|
368
|
+
<Text>{item.label}</Text>
|
|
369
|
+
<Badge tone={item.done ? "up" : "info"}>{item.done ? "Done" : "To do"}</Badge>
|
|
370
|
+
</Row>
|
|
371
|
+
<ol>
|
|
372
|
+
{setupSteps(`${setup.ownerHandle}/${setup.appSlug}`).map((step) => (
|
|
373
|
+
<li key={step.link}>
|
|
374
|
+
Open <a href={step.href} target="_blank" rel="noreferrer">{step.link}</a>
|
|
375
|
+
{step.text}
|
|
376
|
+
</li>
|
|
377
|
+
))}
|
|
378
|
+
</ol>
|
|
379
|
+
<Text muted>Both values stay on the server and are never sent to the browser. Search works the moment they are saved.</Text>
|
|
380
|
+
</Stack>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
318
383
|
|
|
319
384
|
export default function App() {
|
|
385
|
+
const setup = useSetup();
|
|
386
|
+
const [screen, setScreen] = useState("search");
|
|
320
387
|
const [query, setQuery] = useState("");
|
|
321
388
|
const [tracks, setTracks] = useState([]);
|
|
322
389
|
const [problem, setProblem] = useState(null);
|
|
390
|
+
const ready = setup?.ready === true;
|
|
323
391
|
|
|
324
392
|
const search = async () => {
|
|
325
393
|
const res = await fetch(`/_api/search?q=${encodeURIComponent(query)}`);
|
|
@@ -333,46 +401,55 @@ export default function App() {
|
|
|
333
401
|
};
|
|
334
402
|
|
|
335
403
|
return (
|
|
336
|
-
<
|
|
337
|
-
<Row gap={2}
|
|
338
|
-
<
|
|
339
|
-
|
|
404
|
+
<Stack>
|
|
405
|
+
<Row gap={2}>
|
|
406
|
+
<Button variant="quiet" onClick={() => setScreen("search")}>Search</Button>
|
|
407
|
+
{/* The Setup entry: the owner's alone, highlighted while the item is still to do, quiet once done. */}
|
|
408
|
+
{setup?.owner && (
|
|
409
|
+
<Button variant={ready ? "quiet" : "solid"} onClick={() => setScreen("setup")}>
|
|
410
|
+
Setup{ready ? "" : " · to do"}
|
|
411
|
+
</Button>
|
|
412
|
+
)}
|
|
340
413
|
</Row>
|
|
341
414
|
|
|
342
|
-
{
|
|
343
|
-
<
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
415
|
+
{screen === "setup" && setup?.owner ? (
|
|
416
|
+
<SetupScreen setup={setup} />
|
|
417
|
+
) : (
|
|
418
|
+
<Stack>
|
|
419
|
+
{ready ? (
|
|
420
|
+
<Row gap={2} align="top">
|
|
421
|
+
<Field label="Search">{(id, describedBy) => <Input id={id} describedBy={describedBy} value={query} onChange={setQuery} placeholder="Search Spotify" />}</Field>
|
|
422
|
+
<Button type="button" onClick={search}>Search</Button>
|
|
423
|
+
</Row>
|
|
424
|
+
) : (
|
|
425
|
+
<Text muted>Search is not open yet.</Text>
|
|
426
|
+
)}
|
|
427
|
+
|
|
428
|
+
{problem?.reason === "rate-limited" && <Text>Spotify asked us to wait {problem.retryAfter} seconds.</Text>}
|
|
429
|
+
{problem && problem.reason !== "rate-limited" && <Text role="alert">{problem.message}</Text>}
|
|
430
|
+
|
|
431
|
+
<ul>
|
|
432
|
+
{tracks.map((t) => (
|
|
433
|
+
<li key={t.id}>
|
|
434
|
+
{t.image && <img src={t.image} alt="" width={64} height={64} />}
|
|
435
|
+
<a href={t.url} target="_blank" rel="noreferrer">
|
|
436
|
+
{t.name}
|
|
437
|
+
</a>{" "}
|
|
438
|
+
— {t.artists}
|
|
439
|
+
</li>
|
|
348
440
|
))}
|
|
349
|
-
</
|
|
350
|
-
|
|
351
|
-
|
|
441
|
+
</ul>
|
|
442
|
+
|
|
443
|
+
<section>
|
|
444
|
+
<h3>What this page can see</h3>
|
|
445
|
+
<p>
|
|
446
|
+
Spotify's public catalog — search, artists, albums and tracks. Your own top tracks, playlists and saved songs
|
|
447
|
+
belong to your Spotify account and are not part of this.
|
|
448
|
+
</p>
|
|
449
|
+
</section>
|
|
450
|
+
</Stack>
|
|
352
451
|
)}
|
|
353
|
-
|
|
354
|
-
{problem && !["missing-key", "rate-limited"].includes(problem.reason) && <p role="alert">{problem.message}</p>}
|
|
355
|
-
|
|
356
|
-
<ul>
|
|
357
|
-
{tracks.map((t) => (
|
|
358
|
-
<li key={t.id}>
|
|
359
|
-
{t.image && <img src={t.image} alt="" width={64} height={64} />}
|
|
360
|
-
<a href={t.url} target="_blank" rel="noreferrer">
|
|
361
|
-
{t.name}
|
|
362
|
-
</a>{" "}
|
|
363
|
-
— {t.artists}
|
|
364
|
-
</li>
|
|
365
|
-
))}
|
|
366
|
-
</ul>
|
|
367
|
-
|
|
368
|
-
<section>
|
|
369
|
-
<h3>What this page can see</h3>
|
|
370
|
-
<p>
|
|
371
|
-
Spotify's public catalog — search, artists, albums and tracks. Your own top tracks, playlists and saved songs
|
|
372
|
-
belong to your Spotify account and are not part of this.
|
|
373
|
-
</p>
|
|
374
|
-
</section>
|
|
375
|
-
</div>
|
|
452
|
+
</Stack>
|
|
376
453
|
);
|
|
377
454
|
}
|
|
378
455
|
```
|