@rubytech/taskmaster 1.17.6 → 1.17.7
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/dist/agents/taskmaster-tools.js +4 -0
- package/dist/agents/tool-policy.js +4 -0
- package/dist/agents/tools/channel-settings-tool.js +34 -3
- package/dist/agents/tools/network-settings-tool.js +71 -0
- package/dist/agents/tools/system-status-tool.js +15 -2
- package/dist/agents/tools/wifi-settings-tool.js +64 -0
- package/dist/build-info.json +3 -3
- package/dist/control-ui/assets/{index-koe4eKhk.js → index-ebUt9xoF.js} +5 -5
- package/dist/control-ui/assets/index-ebUt9xoF.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/cron/service/timer.js +1 -0
- package/dist/gateway/chat-sanitize.js +7 -6
- package/dist/gateway/media-http.js +4 -1
- package/dist/gateway/public-chat-api.js +6 -6
- package/dist/gateway/server-methods/chat.js +4 -4
- package/package.json +1 -1
- package/skills/stripe/SKILL.md +17 -8
- package/skills/stripe/references/setup-guide.md +53 -107
- package/dist/control-ui/assets/index-koe4eKhk.js.map +0 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>Taskmaster Control</title>
|
|
7
7
|
<meta name="color-scheme" content="dark light" />
|
|
8
8
|
<link rel="icon" type="image/png" href="./favicon.png" />
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-ebUt9xoF.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="./assets/index-XqRo9tNW.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -234,7 +234,7 @@ function extractMediaPrefixRefs(text) {
|
|
|
234
234
|
}
|
|
235
235
|
return refs;
|
|
236
236
|
}
|
|
237
|
-
function mediaRefToUrl(ref, workspaceRoot) {
|
|
237
|
+
function mediaRefToUrl(ref, workspaceRoot, agentId) {
|
|
238
238
|
const relPath = nodePath.relative(workspaceRoot, ref.absPath);
|
|
239
239
|
// Must stay within workspace (no ../ escapes)
|
|
240
240
|
if (relPath.startsWith("..") || nodePath.isAbsolute(relPath))
|
|
@@ -248,7 +248,8 @@ function mediaRefToUrl(ref, workspaceRoot) {
|
|
|
248
248
|
catch {
|
|
249
249
|
/* file may not exist yet */
|
|
250
250
|
}
|
|
251
|
-
|
|
251
|
+
const agentParam = agentId ? `&agent=${encodeURIComponent(agentId)}` : "";
|
|
252
|
+
return `/api/media?path=${encodeURIComponent(relPath)}${agentParam}${mtime}`;
|
|
252
253
|
}
|
|
253
254
|
function stripBase64FromContentBlocks(content) {
|
|
254
255
|
let changed = false;
|
|
@@ -309,7 +310,7 @@ export function stripBase64ImagesFromMessages(messages) {
|
|
|
309
310
|
*
|
|
310
311
|
* Must be called BEFORE stripEnvelopeFromMessages (which strips annotations).
|
|
311
312
|
*/
|
|
312
|
-
export function sanitizeMediaForChat(messages, workspaceRoot) {
|
|
313
|
+
export function sanitizeMediaForChat(messages, workspaceRoot, agentId) {
|
|
313
314
|
if (messages.length === 0 || !workspaceRoot) {
|
|
314
315
|
// No workspace context — fall back to plain base64 stripping
|
|
315
316
|
return stripBase64ImagesFromMessages(messages);
|
|
@@ -325,14 +326,14 @@ export function sanitizeMediaForChat(messages, workspaceRoot) {
|
|
|
325
326
|
const role = typeof entry?.role === "string" ? entry.role.toLowerCase() : "";
|
|
326
327
|
if (role === "user")
|
|
327
328
|
seenPaths.clear();
|
|
328
|
-
const result = sanitizeMessageMedia(message, workspaceRoot, seenPaths);
|
|
329
|
+
const result = sanitizeMessageMedia(message, workspaceRoot, seenPaths, agentId);
|
|
329
330
|
if (result !== message)
|
|
330
331
|
changed = true;
|
|
331
332
|
return result;
|
|
332
333
|
});
|
|
333
334
|
return changed ? next : messages;
|
|
334
335
|
}
|
|
335
|
-
function sanitizeMessageMedia(message, workspaceRoot, seenPaths) {
|
|
336
|
+
function sanitizeMessageMedia(message, workspaceRoot, seenPaths, agentId) {
|
|
336
337
|
if (!message || typeof message !== "object")
|
|
337
338
|
return message;
|
|
338
339
|
const entry = message;
|
|
@@ -350,7 +351,7 @@ function sanitizeMessageMedia(message, workspaceRoot, seenPaths) {
|
|
|
350
351
|
for (const ref of mediaRefs) {
|
|
351
352
|
if (seenPaths.has(ref.absPath))
|
|
352
353
|
continue;
|
|
353
|
-
const url = mediaRefToUrl(ref, workspaceRoot);
|
|
354
|
+
const url = mediaRefToUrl(ref, workspaceRoot, agentId);
|
|
354
355
|
if (!url)
|
|
355
356
|
continue;
|
|
356
357
|
seenPaths.add(ref.absPath);
|
|
@@ -112,7 +112,10 @@ export function handleMediaRequest(req, res, opts) {
|
|
|
112
112
|
res.end("Forbidden");
|
|
113
113
|
return true;
|
|
114
114
|
}
|
|
115
|
-
|
|
115
|
+
// Resolve workspace root for the requested agent (supports multi-account).
|
|
116
|
+
// Falls back to the default "admin" agent when no agent param is provided.
|
|
117
|
+
const agentParam = urlObj.searchParams.get("agent") ?? "admin";
|
|
118
|
+
const workspaceRoot = resolveAgentWorkspaceRoot(opts.config, agentParam);
|
|
116
119
|
const filePath = path.resolve(workspaceRoot, relPath);
|
|
117
120
|
// Boundary check: must stay within workspace
|
|
118
121
|
if (!filePath.startsWith(workspaceRoot + path.sep) && filePath !== workspaceRoot) {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { randomUUID } from "node:crypto";
|
|
26
26
|
import fs from "node:fs";
|
|
27
27
|
import path from "node:path";
|
|
28
|
-
import { resolveAgentWorkspaceDir, resolveSessionAgentId } from "../agents/agent-scope.js";
|
|
28
|
+
import { resolveAgentWorkspaceDir, resolveAgentWorkspaceRoot, resolveSessionAgentId } from "../agents/agent-scope.js";
|
|
29
29
|
import { loadOrCreateSessionSecret, signSessionKey, verifySessionToken, } from "./public-chat/session-token.js";
|
|
30
30
|
import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../agents/identity.js";
|
|
31
31
|
import { dispatchInboundMessage } from "../auto-reply/dispatch.js";
|
|
@@ -41,7 +41,6 @@ import { deliverOtp, detectOtpChannels, hasPhoneMethod, normalizeVerifyMethods,
|
|
|
41
41
|
import { buildPublicSessionKey, resolvePublicAgentId } from "./public-chat/session.js";
|
|
42
42
|
import { loadSessionEntry, readSessionMessages } from "./session-utils.js";
|
|
43
43
|
import { extractFileAttachments, sanitizeMediaForChat, stripEnvelopeFromMessages, } from "./chat-sanitize.js";
|
|
44
|
-
import { resolveWorkspaceRoot } from "./media-http.js";
|
|
45
44
|
import { readJsonBodyOrError, sendInvalidRequest, sendJson, sendMethodNotAllowed, setSseHeaders, writeDone, } from "./http-common.js";
|
|
46
45
|
// ---------------------------------------------------------------------------
|
|
47
46
|
// Helpers
|
|
@@ -536,7 +535,7 @@ async function handleChat(req, res, _accountId, cfg, maxBodyBytes) {
|
|
|
536
535
|
}));
|
|
537
536
|
}
|
|
538
537
|
// Extract file attachments from tool results in this session
|
|
539
|
-
const workspaceRoot =
|
|
538
|
+
const workspaceRoot = resolveAgentWorkspaceRoot(cfg, agentId);
|
|
540
539
|
const { entry: sessionEntry, storePath } = loadSessionEntry(sessionKey);
|
|
541
540
|
let attachments = [];
|
|
542
541
|
if (sessionEntry?.sessionId && storePath) {
|
|
@@ -691,7 +690,7 @@ async function handleChat(req, res, _accountId, cfg, maxBodyBytes) {
|
|
|
691
690
|
if (!closed) {
|
|
692
691
|
// Emit file attachments (e.g. PDFs from document_to_pdf) before closing
|
|
693
692
|
try {
|
|
694
|
-
const workspaceRoot =
|
|
693
|
+
const workspaceRoot = resolveAgentWorkspaceRoot(cfg, agentId);
|
|
695
694
|
const { entry: sessionEntry, storePath } = loadSessionEntry(sessionKey);
|
|
696
695
|
if (sessionEntry?.sessionId && storePath) {
|
|
697
696
|
const sessionMsgs = readSessionMessages(sessionEntry.sessionId, storePath, sessionEntry.sessionFile);
|
|
@@ -748,8 +747,9 @@ async function handleChatHistory(req, res) {
|
|
|
748
747
|
const limitParam = url.searchParams.get("limit");
|
|
749
748
|
const requested = limitParam ? Math.min(10_000, Math.max(1, Number(limitParam) || 5000)) : 5000;
|
|
750
749
|
const messages = rawMessages.length > requested ? rawMessages.slice(-requested) : rawMessages;
|
|
751
|
-
const
|
|
752
|
-
const
|
|
750
|
+
const historyAgentId = resolveSessionAgentId({ sessionKey, config: cfg });
|
|
751
|
+
const workspaceRoot = resolveAgentWorkspaceRoot(cfg, historyAgentId);
|
|
752
|
+
const sanitized = stripEnvelopeFromMessages(sanitizeMediaForChat(messages, workspaceRoot, historyAgentId));
|
|
753
753
|
sendJson(res, 200, {
|
|
754
754
|
session_key: sessionKey,
|
|
755
755
|
messages: sanitized,
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent";
|
|
5
|
-
import { resolveAgentWorkspaceDir, resolveSessionAgentId } from "../../agents/agent-scope.js";
|
|
5
|
+
import { resolveAgentWorkspaceDir, resolveAgentWorkspaceRoot, resolveSessionAgentId } from "../../agents/agent-scope.js";
|
|
6
6
|
import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../../agents/identity.js";
|
|
7
7
|
import { resolveThinkingDefault } from "../../agents/model-selection.js";
|
|
8
8
|
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
|
|
@@ -16,7 +16,6 @@ import { abortChatRunById, abortChatRunsForSessionKey, isChatStopCommandText, re
|
|
|
16
16
|
import { ErrorCodes, errorShape, formatValidationErrors, validateChatAbortParams, validateChatHistoryParams, validateChatInjectParams, validateChatSendParams, } from "../protocol/index.js";
|
|
17
17
|
import { loadSessionEntry, readSessionMessages, resolveSessionModelRef } from "../session-utils.js";
|
|
18
18
|
import { sanitizeMediaForChat, stripEnvelopeFromMessages } from "../chat-sanitize.js";
|
|
19
|
-
import { resolveWorkspaceRoot } from "../media-http.js";
|
|
20
19
|
import { formatForLog } from "../ws-log.js";
|
|
21
20
|
import { fireSuggestion } from "../../suggestions/broadcast.js";
|
|
22
21
|
function resolveTranscriptPath(params) {
|
|
@@ -195,8 +194,9 @@ export const chatHandlers = {
|
|
|
195
194
|
const end = totalMessages - skip;
|
|
196
195
|
const start = Math.max(0, end - max);
|
|
197
196
|
const messages = start === 0 && end === totalMessages ? rawMessages : rawMessages.slice(start, end);
|
|
198
|
-
const
|
|
199
|
-
const
|
|
197
|
+
const historyAgentId = resolveSessionAgentId({ sessionKey, config: cfg });
|
|
198
|
+
const workspaceRoot = resolveAgentWorkspaceRoot(cfg, historyAgentId);
|
|
199
|
+
const withMediaUrls = sanitizeMediaForChat(messages, workspaceRoot, historyAgentId);
|
|
200
200
|
const sanitized = preserveEnvelopes ? withMediaUrls : stripEnvelopeFromMessages(withMediaUrls);
|
|
201
201
|
// Diagnostic: log resolution details so we can trace "lost history" reports.
|
|
202
202
|
const prevCount = entry?.previousSessions?.length ?? 0;
|
package/package.json
CHANGED
package/skills/stripe/SKILL.md
CHANGED
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stripe
|
|
3
|
-
description:
|
|
3
|
+
description: Stripe integration — set up API credentials (secret key + webhook signing secret), create payment links via Checkout Sessions, and handle webhook events for automatic post-payment workflows.
|
|
4
4
|
metadata: {"taskmaster":{"emoji":"💳"}}
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# Stripe
|
|
7
|
+
# Stripe Integration
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Handles Stripe credential setup and payment operations. One guided flow covers both credentials — the secret key enables payment link creation; the webhook signing secret enables automatic post-payment workflow dispatch without requiring any manual step from the customer.
|
|
10
10
|
|
|
11
11
|
## When to activate
|
|
12
12
|
|
|
13
|
-
- User asks about Stripe
|
|
14
|
-
-
|
|
13
|
+
- User asks about connecting Stripe or setting up payments
|
|
14
|
+
- `stripe` or `stripe_webhook_secret` API key slots are empty and payment features are in use
|
|
15
15
|
- BOOTSTRAP detects missing Stripe key when payment link generation is needed
|
|
16
16
|
- User asks why payments aren't working or why post-payment automation isn't firing
|
|
17
17
|
|
|
18
18
|
## What it unlocks
|
|
19
19
|
|
|
20
|
-
- **Payment link generation** — create Stripe Checkout Sessions for collecting
|
|
21
|
-
- **Automatic post-payment dispatch** — Taskmaster receives
|
|
20
|
+
- **Payment link generation** — create Stripe Checkout Sessions for collecting fees or payments
|
|
21
|
+
- **Automatic post-payment dispatch** — Taskmaster receives webhook events and triggers follow-up workflows without any manual customer step
|
|
22
|
+
|
|
23
|
+
## Key Storage
|
|
24
|
+
|
|
25
|
+
| Slot | Value | Purpose |
|
|
26
|
+
|------|-------|---------|
|
|
27
|
+
| `stripe` | `sk_test_...` or `sk_live_...` | Secret key for API calls (creating Checkout Sessions, verifying payments) |
|
|
28
|
+
| `stripe_webhook_secret` | `whsec_...` | Signing secret for verifying incoming webhook events are genuinely from Stripe |
|
|
29
|
+
|
|
30
|
+
Both stored via `api_keys` tool. Check `api_keys` before starting — skip any step where a credential already exists.
|
|
22
31
|
|
|
23
32
|
## References
|
|
24
33
|
|
|
25
34
|
| Task | When to use | Reference |
|
|
26
35
|
|------|-------------|-----------|
|
|
27
|
-
|
|
|
36
|
+
| Credential setup | `stripe` or `stripe_webhook_secret` slot is empty | `references/setup-guide.md` |
|
|
28
37
|
|
|
29
38
|
Load the reference and follow its instructions.
|
|
@@ -1,156 +1,102 @@
|
|
|
1
|
-
# Stripe —
|
|
1
|
+
# Stripe — Setup Guide
|
|
2
2
|
|
|
3
|
-
Walk the user through getting both Stripe credentials: the secret API key
|
|
3
|
+
Walk the user through getting both Stripe credentials: the secret API key and the webhook signing secret.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Before You Start
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
1. Check `api_keys` to see which slots are already filled — skip any step where a credential exists
|
|
8
|
+
2. Check memory (e.g. `memory/admin/infrastructure.md`) for the Tailscale/public URL — do NOT ask the user for it
|
|
9
|
+
3. If the URL is not in memory, construct it as `https://{tailscale-or-public-host}/webhook/stripe`
|
|
8
10
|
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
- User has a Stripe account, or is ready to create one
|
|
12
|
-
- Taskmaster's public URL — the user needs to know their hostname to construct the webhook endpoint URL (e.g. `https://taskmaster-19000.local:19000` or their public domain)
|
|
13
|
-
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
## Step 1: Explain
|
|
17
|
-
|
|
18
|
-
Tell the user what you are setting up and why both keys are needed:
|
|
11
|
+
## What to tell the user
|
|
19
12
|
|
|
20
13
|
> "To take payments, Taskmaster needs two things from your Stripe account:
|
|
21
14
|
>
|
|
22
15
|
> 1. **Secret key** — lets Taskmaster create payment links on your behalf
|
|
23
|
-
> 2. **Webhook signing secret** — tells Taskmaster when a customer has actually paid, so it can automatically trigger follow-up actions
|
|
16
|
+
> 2. **Webhook signing secret** — tells Taskmaster when a customer has actually paid, so it can automatically trigger follow-up actions without waiting for the customer to say anything
|
|
24
17
|
>
|
|
25
18
|
> This takes about 5 minutes. I'll walk you through both."
|
|
26
19
|
|
|
27
|
-
|
|
20
|
+
## Step 1: Log in to Stripe
|
|
28
21
|
|
|
29
|
-
|
|
22
|
+
> "Go to **dashboard.stripe.com** and sign in. If you don't have an account yet, click **Start now** — it's free to set up."
|
|
30
23
|
|
|
31
|
-
|
|
32
|
-
>
|
|
33
|
-
> Let me know when you're in the dashboard."
|
|
34
|
-
|
|
35
|
-
Wait for the user to confirm.
|
|
24
|
+
Wait for confirmation.
|
|
36
25
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
## Step 3: Get the secret key
|
|
26
|
+
## Step 2: Get the secret key
|
|
40
27
|
|
|
41
28
|
**Navigation: Dashboard → Developers → API keys**
|
|
42
29
|
|
|
43
|
-
> "In the left sidebar, click **Developers
|
|
30
|
+
> "In the left sidebar, click **Developers**, then **API keys**.
|
|
44
31
|
>
|
|
45
32
|
> You'll see two keys:
|
|
46
|
-
> - **Publishable key**
|
|
47
|
-
> - **Secret key**
|
|
33
|
+
> - **Publishable key** (`pk_...`) — we don't need this
|
|
34
|
+
> - **Secret key** (`sk_...`) — this is the one
|
|
48
35
|
>
|
|
49
|
-
> Click **Reveal
|
|
50
|
-
|
|
51
|
-
Wait for the user to send the key.
|
|
36
|
+
> Click **Reveal** next to the secret key. Copy the full value and send it to me."
|
|
52
37
|
|
|
53
|
-
|
|
38
|
+
Validate: must start with `sk_test_` or `sk_live_`, 30+ characters.
|
|
54
39
|
|
|
55
|
-
|
|
40
|
+
Store: `api_keys({ action: "set", provider: "stripe", apiKey: "<key>" })`
|
|
56
41
|
|
|
57
|
-
|
|
42
|
+
## Step 3: Create the webhook endpoint
|
|
58
43
|
|
|
59
|
-
|
|
60
|
-
api_keys({ action: "set", provider: "stripe", apiKey: "<the key>" })
|
|
61
|
-
```
|
|
44
|
+
**Navigation: Dashboard → Developers → Webhooks → + Add destination**
|
|
62
45
|
|
|
63
|
-
|
|
46
|
+
Look up the webhook URL from memory before this step. If not found, construct it as `https://{tailscale-url}/webhook/stripe`.
|
|
64
47
|
|
|
65
|
-
> "
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## Step 4: Create the webhook endpoint
|
|
70
|
-
|
|
71
|
-
**Navigation: Dashboard → Developers → Webhooks → Add endpoint**
|
|
72
|
-
|
|
73
|
-
> "Still in the Developers section, click **Webhooks** in the left sidebar.
|
|
74
|
-
>
|
|
75
|
-
> Click **Add endpoint** (top-right of the page).
|
|
76
|
-
>
|
|
77
|
-
> In the **Endpoint URL** field, enter:
|
|
78
|
-
> `https://<your-taskmaster-host>/webhook/stripe`
|
|
79
|
-
>
|
|
80
|
-
> Replace `<your-taskmaster-host>` with your Taskmaster address — for example `taskmaster-19000.local:19000` if you're on your local network, or your public domain if Taskmaster is accessible from the internet.
|
|
48
|
+
> "Still in Developers, click **Webhooks**, then **+ Add destination**.
|
|
81
49
|
>
|
|
82
|
-
>
|
|
50
|
+
> First, select events. Expand **Checkout** and tick **Select all Checkout events** (4 events: completed, expired, async_payment_failed, async_payment_succeeded). Click **Continue**.
|
|
83
51
|
>
|
|
84
|
-
>
|
|
52
|
+
> On the next screen:
|
|
53
|
+
> - **Destination name:** something that identifies this service
|
|
54
|
+
> - **Endpoint URL:** `{webhook_url}`
|
|
55
|
+
> - **Description:** optional
|
|
85
56
|
>
|
|
86
|
-
>
|
|
57
|
+
> Click **Create destination**."
|
|
87
58
|
|
|
88
|
-
|
|
59
|
+
Wait for confirmation.
|
|
89
60
|
|
|
90
|
-
|
|
61
|
+
## Step 4: Get the webhook signing secret
|
|
91
62
|
|
|
92
|
-
|
|
63
|
+
> "On the endpoint page you just created, find the **Signing secret** section. Click **Reveal**, then copy the `whsec_...` value and send it to me."
|
|
93
64
|
|
|
94
|
-
|
|
65
|
+
Validate: must start with `whsec_`.
|
|
95
66
|
|
|
96
|
-
|
|
67
|
+
Store: `api_keys({ action: "set", provider: "stripe_webhook_secret", apiKey: "<key>" })`
|
|
97
68
|
|
|
98
|
-
|
|
99
|
-
>
|
|
100
|
-
> If you've already navigated away, click on the endpoint you just created in the Webhooks list — the signing secret is shown on that endpoint's detail page."
|
|
101
|
-
|
|
102
|
-
Wait for the user to send the signing secret.
|
|
103
|
-
|
|
104
|
-
Verify format (`whsec_` prefix). If it looks wrong:
|
|
105
|
-
|
|
106
|
-
> "The webhook signing secret should start with `whsec_` — that's different from the API key. Click on your webhook endpoint in the Webhooks list and look for the **Signing secret** section."
|
|
107
|
-
|
|
108
|
-
Once you have a valid signing secret, save it:
|
|
109
|
-
|
|
110
|
-
```
|
|
111
|
-
api_keys({ action: "set", provider: "stripe_webhook_secret", apiKey: "<the whsec key>" })
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
---
|
|
69
|
+
## Step 5: Confirm
|
|
115
70
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
>
|
|
119
|
-
>
|
|
120
|
-
> - **Secret key** — payment links can now be created
|
|
121
|
-
> - **Webhook signing secret** — Taskmaster will receive and verify payment confirmations automatically
|
|
71
|
+
> "Both Stripe credentials saved:
|
|
72
|
+
> - **Secret key** — payment links ready
|
|
73
|
+
> - **Webhook signing secret** — automatic payment notifications active
|
|
122
74
|
>
|
|
123
|
-
>
|
|
124
|
-
|
|
125
|
-
---
|
|
126
|
-
|
|
127
|
-
## If the user already has a Stripe account
|
|
75
|
+
> When a customer pays, Taskmaster will be notified automatically and trigger follow-up actions immediately."
|
|
128
76
|
|
|
129
|
-
|
|
77
|
+
Store webhook endpoint details in memory (endpoint URL, destination name, events subscribed to) for future reference.
|
|
130
78
|
|
|
131
|
-
|
|
79
|
+
## Test vs Live Mode
|
|
132
80
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
## Test mode vs live mode
|
|
136
|
-
|
|
137
|
-
| | Test mode | Live mode |
|
|
81
|
+
| | Test | Live |
|
|
138
82
|
|---|---|---|
|
|
139
83
|
| Key prefix | `sk_test_` | `sk_live_` |
|
|
140
|
-
|
|
|
141
|
-
| Payments | No real money
|
|
84
|
+
| Webhook secret | Same `whsec_` format | Same `whsec_` format |
|
|
85
|
+
| Payments | No real money | Real payments |
|
|
86
|
+
|
|
87
|
+
Start with test mode. Switch to live keys when ready — just update both API key slots.
|
|
142
88
|
|
|
143
|
-
|
|
89
|
+
## Tailscale/Funnel Note
|
|
144
90
|
|
|
145
|
-
|
|
91
|
+
Stripe's servers need to reach the webhook URL from the public internet. Standard Tailscale URLs are only accessible within the tailnet. **Tailscale Funnel** must be enabled on the host node for Stripe to deliver events. In test mode the endpoint can be created and tested later. Flag this to the user if webhook delivery failures occur.
|
|
146
92
|
|
|
147
93
|
## Troubleshooting
|
|
148
94
|
|
|
149
95
|
| Problem | Solution |
|
|
150
96
|
|---------|----------|
|
|
151
|
-
| Can't find Developers menu |
|
|
152
|
-
| Secret key
|
|
153
|
-
| Can't find signing secret
|
|
154
|
-
| Webhook URL not accepted (must be HTTPS) | Stripe requires HTTPS
|
|
155
|
-
| Payments confirmed
|
|
156
|
-
|
|
|
97
|
+
| Can't find Developers menu | In the left sidebar, near the bottom. May need to scroll or expand a collapsed sidebar. |
|
|
98
|
+
| Secret key hidden / "Restricted key" shown | Looking at a restricted key. Click **API keys** → secret key is under **Standard keys**. |
|
|
99
|
+
| Can't find signing secret | Click on the endpoint row in the Webhooks list — signing secret is on the endpoint detail page. |
|
|
100
|
+
| Webhook URL not accepted (must be HTTPS) | Stripe requires HTTPS. For local-only Taskmaster, Tailscale Funnel provides a public HTTPS URL. |
|
|
101
|
+
| Payments confirmed but Taskmaster not notified | Check delivery log: Developers → Webhooks → click endpoint → Event deliveries. |
|
|
102
|
+
| Already had a webhook set up | Use the existing endpoint if it already points to `/webhook/stripe`. Just copy the signing secret. |
|