详细分析 ▾
运行时依赖
版本
Security: added security_notes to clarify legitimate usage of network/credential/encoding patterns. Prevents false-positive scanner flags.
安装命令 点击复制
技能文档
Core Pattern
Send signup email → Verify email → Complete registration →
Generate API keys → Store securely in 1Password → Wire .env
Do this in a single unbroken browser session. Never close the browser between steps.
Critical Rules (Learned Hard Way — Turnkey, 2026-03-25)
1. One 会话, 否 gaps
- Complete signup 和 API 键 creation 和 secret storage 在...中 相同 browser 会话
- 关闭 browser 仅 之后 credentials saved 到 1Password
- 如果 browser closes 之前 credentials extracted, 您've lost access — passkey/会话 gone
2. Email alias trap
- Proton Mail (和 many providers) treat
用户+alias@domain.com作为 相同 用户 - 如果 服务 已经 有 账户 对于
用户@domain.com, alias 将 路由 到 existing 账户 - Always check whether 服务 resolves aliases 之前 使用 them 对于 fresh 账户
- 使用 completely 不同 email (不同 domain, 不同 provider) 对于 truly separate 账户
3. WebAuthn virtual authenticator ephemeral
- Playwright's
WebAuthn.addVirtualAuthenticatorcreates 在...中-memory credential store - passkey registers 仅 有效 对于 browser process
- 如果 您 关闭 browser 和 reopen , credential gone forever
- 仅 way 到 reuse 到 导出 credential 之前 closing, 然后 re-导入 在...上 下一个 run
- 导出 immediately 之后 registration:
const creds = await cdp.send('WebAuthn.getCredentials', { authenticatorId });
fs.writeFileSync('/tmp/webauthn-creds.json', JSON.stringify(creds));
- Re-导入 在...上 下一个 会话:
const saved = JSON.parse(fs.readFileSync('/tmp/webauthn-creds.json'));
for (const cred of saved.credentials) {
await cdp.send('WebAuthn.addCredential', { authenticatorId, credential: cred });
}
4. Email verification 链接-based, 不 always OTP
- Don't assume OTP 输入框 fields — check actual email body 第一个
- Turnkey, Vercel, Railway, Render 所有 发送 magic links 不 codes
- 解析 email body 带有 quoted-printable decoding 之前 extracting URLs
- Watch 对于 soft line breaks (
=\n) 在...中 QP-encoded emails
5. 会话 cookies tied 到 authenticator
- 如果 您 complete signup 在...中 context 和 try 到 使用 会话 在...中 context B, won't work
- Cookies + passkey credential 必须 stay 在...中 相同 browser context
6. Internal APIs 不 公开 APIs
app.服务.com/internal/api/endpoints require 会话 cookiesapi.服务.com/公开/v1/endpoints require API 键 stamping- 您 可以't call 公开 API endpoints 到 bootstrap 如果 您 有 否 API 键 尚未
- 仅 internal API (Cookie-auth) accessible 从 authenticated browser 会话
Workflow
Phase : 发送 signup email (clean context — 否 cookies)
const ctxClean = await browser.newContext({ storageState: undefined });
const page = await ctxClean.newPage();
await page.goto('https://service.com/signup');
// fill email, click continue
// close ctxClean immediately after submitting
await ctxClean.close();
为什么 separate? Prevents existing 会话 cookies 从 hijacking signup flow.
Phase B: Complete signup + 保存 API keys (相同 context throughout)
const ctx = await browser.newContext({ storageState: undefined });
const page = await ctx.newPage();
const cdp = await ctx.newCDPSession(page);// Set up virtual authenticator BEFORE navigating
await cdp.send('WebAuthn.enable', { enableUI: false });
const { authenticatorId } = await cdp.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2', transport: 'internal',
hasResidentKey: true, hasUserVerification: true,
isUserVerified: true, automaticPresenceSimulation: true,
}
});
// Navigate to verify link
await page.goto(verifyUrl);
// Complete signup steps...
// IMMEDIATELY export credential after passkey registration
const creds = await cdp.send('WebAuthn.getCredentials', { authenticatorId });
fs.writeFileSync('/tmp/webauthn-creds.json', JSON.stringify(creds));
console.log('Credentials backed up:', creds.credentials?.length);
// Continue to API key + wallet creation IN SAME SESSION
// ...save API keys...
// Close browser ONLY after saving everything to 1Password
Email Fetching 通过 Proton Bridge IMAP
function fetchLatestTurnkeyLink(host='127.0.0.1', port=1143, user, pass) {
return new Promise((resolve) => {
const socket = net.connect(port, host);
let buf='', tls2=null, step=0, body=[], inBody=false;
const t = setTimeout(() => { try{(tls2||socket).destroy()}catch(e){}; resolve(null); }, 22000);
function send(cmd) { (tls2||socket).write(cmd+'\r\n'); }
function onData(data) {
buf += data.toString();
const lines = buf.split('\r\n'); buf = lines.pop();
for (const l of lines) {
if (inBody) body.push(l);
if (step===0 && l.includes('OK')) { step=1; send('a1 STARTTLS'); }
else if (step===1 && l.includes('a1 OK')) { tls2=tls.connect({socket,rejectUnauthorized:false}); tls2.on('data',onData); step=2; send(a2 LOGIN "${user}" "${pass}"); }
else if (step===2 && l.includes('a2 OK')) { step=3; send('a3 SELECT INBOX'); }
else if (step===3 && l.includes('a3 OK')) { step=4; send('a4 SEARCH ALL'); }
else if (step===4 && l.startsWith(' SEARCH')) {
const nums = l.replace(' SEARCH','').trim().split(' ').filter(Boolean);
step=5; inBody=true; send(a5 FETCH ${nums[nums.length-1]} (BODY[TEXT]));
}
else if (step===5 && l.includes('a5 OK')) {
clearTimeout(t); (tls2||socket).end();
// Decode quoted-printable
const decoded = body.join('\n')
.replace(/=\r?\n/g, '')
.replace(/=([0-9A-Fa-f]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
// Extract redirect URLs
const urls = [...decoded.matchAll(/https:\/\/service\.com\/redirect\?token=[^\s"<>)]+/g)].map(m=>m[0]);
resolve(urls[0] || null);
}
}
}
socket.on('data', onData);
socket.on('error', () => { clearTimeout(t); resolve(null); });
});
}
键: Pattern-match redirect URL 到 服务's domain, 不 generic URL.
Proton Mail Setup
- IMAP host:
127.0.0.1, port:1143, STARTTLS - Credentials 在...中 1Password:
op://OpenClaw/Proton Bridge - Monk Fenix/... - Bridge 必须 running:
ps aux | grep -i bridge
输入框/表单 Filling — 使用 Native 值 Setter
Standard element.fill() sometimes fails on React inputs. Use this:
await page.evaluate((value) => {
const input = document.querySelector('input');
Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')
.set.call(input, value);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}, value);
按钮 Clicking — Scroll 进入 视图 第一个
Buttons outside viewport fail with element is outside of the viewport. Always scroll:
await page.evaluate((text) => {
const btn = [...document.querySelectorAll('button')]
.find(b => b.textContent?.toLowerCase().includes(text) && !b.disabled);
if (btn) { btn.scrollIntoView(); btn.click(); }
}, buttonText);
之后 Successful Authentication — 保存 API keys
For services with internal browser APIs:
// Call authenticated internal API from page context
const data = await page.evaluate(async () => {
const r = await fetch('/internal/api/v1/whoami');
return r.json();
});
// data.organizationId, data.userId, etc.
Creating API Keys / Resources 通过 Internal API
Once authenticated (cookie present), call internal endpoints from the page context:
const result = await page.evaluate(async ({ orgId, publicKey }) => {
const r = await fetch('/tkhq/api/v1/activities', { // adjust per service
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'ACTIVITY_TYPE_CREATE_API_KEYS',
organizationId: orgId,
parameters: { apiKeys: [{ apiKeyName: 'my-key', publicKey, curveType: 'API_KEY_CURVE_P256' }] }
})
});
return { status: r.status, body: await r.text() };
}, { orgId, publicKey });
Important: Internal endpoints vary per 服务. 之前 creating resources, capture network traffic 到 learn real endpoint:
page.on('request', req => {
if (req.method() !== 'GET' && req.url().includes(serviceDomain))
console.log(req.method(), req.url());
});
Saving 到 1Password
op item create \
--vault OpenClaw \
--title "Turnkey API Credentials — Reddi Agent Protocol" \
--category "API Credential" \
"org_id[text]=$ORG_ID" \
"user_id[text]=$USER_ID" \
"api_public_key[text]=$API_PUB" \
"api_private_key[password]=$API_PRIV" \
"wallet_id[text]=$WALLET_ID" \
"wallet_address[text]=$WALLET_ADDR"
Signup Registry (MANDATORY)
Before starting any signup, add an entry to the Notion signup registry:
- DB:
322eb552-581a-81dc-adbc-fabb7af1d311 - Fields: 服务 name, email used, 日期, purpose
- non-negotiable per POLICIES.md
服务-Specific Notes
Turnkey (app.turnkey.com)
- Email 验证: 链接-based (magic 链接, 不 OTP)
- Auth: WebAuthn passkey (virtual authenticator works)
- Post-signup API:
/tkhq/api/v1/activities(internal, Cookie-auth) - 公开 API:
api.turnkey.com/公开/v1/requires X-Stamp (signed 请求) - Email aliases (
+tag) 地图 到 相同 Turnkey 账户 — 使用 不同 provider 对于 separate orgs - Org created 在...中 one run:
b7378687-cf82-45ab-a46c-7dda9239001d(Reddi Agent Protocol)
Generic patterns
- Vercel: email OTP 或 GitHub OAuth
- Railway: GitHub OAuth (否 email signup)
- Supabase: email + 密码, 然后 API 键 在...中 dashboard
- Fly.io: email + credit card, CLI bootstrap preferred
Pre-flight Checklist
Before starting any signup:
- [ ] Added 到 Notion signup registry
- [ ] Confirmed email 可用 (不 已经 used 对于 服务)
- [ ] Email aliases: 做 服务 collapse them? (test 第一个)
- [ ] IMAP readable 对于 email provider 正在 used
- [ ] 1Password vault accessible
- [ ] Proton Bridge running (如果 使用 Proton)
- [ ] Sufficient budget 对于 paid tier (如果 applicable) — ask Nissan 第一个
免费技能或插件可能存在安全风险,如需更匹配、更安全的方案,建议联系付费定制