tenjin-cli 0.1.0-alpha.0 → 0.1.0-alpha.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.
Files changed (138) hide show
  1. package/NOTICE.md +33 -0
  2. package/README.md +198 -18
  3. package/dist/{_esm-ZH2J4QEH.js → _esm-C5CRWCYU.js} +2 -2
  4. package/dist/_esm-FEASBWRP.js +881 -0
  5. package/dist/buy-U35DQ6XM.js +1598 -0
  6. package/dist/buy-U35DQ6XM.js.map +1 -0
  7. package/dist/candidate-RLYCQMF5.js +127 -0
  8. package/dist/candidate-RLYCQMF5.js.map +1 -0
  9. package/dist/{ccip-FXWZDDF5.js → ccip-NQXCEK2U.js} +6 -5
  10. package/dist/ccip-XR5NE5QK.js +19 -0
  11. package/dist/chunk-27ONARHV.js +171 -0
  12. package/dist/chunk-27ONARHV.js.map +1 -0
  13. package/dist/chunk-2I4QTOLB.js +11 -0
  14. package/dist/chunk-2I4QTOLB.js.map +1 -0
  15. package/dist/chunk-3OV7E5G5.js +18 -0
  16. package/dist/chunk-3OV7E5G5.js.map +1 -0
  17. package/dist/chunk-5CLSD5YN.js +209 -0
  18. package/dist/chunk-5CLSD5YN.js.map +1 -0
  19. package/dist/chunk-7KUW4MJD.js +59 -0
  20. package/dist/chunk-7KUW4MJD.js.map +1 -0
  21. package/dist/{doctor-MVLZRVPU.js → chunk-7PHSZKQS.js} +88 -93
  22. package/dist/chunk-7PHSZKQS.js.map +1 -0
  23. package/dist/chunk-CYOF7ROH.js +70 -0
  24. package/dist/chunk-CYOF7ROH.js.map +1 -0
  25. package/dist/chunk-HIVZG55D.js +204 -0
  26. package/dist/chunk-HIVZG55D.js.map +1 -0
  27. package/dist/chunk-HSDCI7OV.js +7806 -0
  28. package/dist/chunk-HSDCI7OV.js.map +1 -0
  29. package/dist/{chunk-LTTRND4A.js → chunk-IYLHQMT5.js} +85 -9
  30. package/dist/chunk-IYLHQMT5.js.map +1 -0
  31. package/dist/chunk-JPHG2JW3.js +96 -0
  32. package/dist/chunk-JPHG2JW3.js.map +1 -0
  33. package/dist/{chunk-ZPZFPVDN.js → chunk-MUBUQVC3.js} +1 -1
  34. package/dist/chunk-MUBUQVC3.js.map +1 -0
  35. package/dist/chunk-N62ROTMC.js +79 -0
  36. package/dist/chunk-N62ROTMC.js.map +1 -0
  37. package/dist/{chunk-SXQQ2ZSR.js → chunk-NCE5NSN6.js} +186 -18
  38. package/dist/chunk-NCE5NSN6.js.map +1 -0
  39. package/dist/{chunk-FIYZLITH.js → chunk-OYMYCW7J.js} +23 -4
  40. package/dist/{chunk-FIYZLITH.js.map → chunk-OYMYCW7J.js.map} +1 -1
  41. package/dist/chunk-QNP7H47L.js +152 -0
  42. package/dist/chunk-QNP7H47L.js.map +1 -0
  43. package/dist/{chunk-TX5T3LKJ.js → chunk-QVX5MWJZ.js} +21 -17
  44. package/dist/{chunk-TX5T3LKJ.js.map → chunk-QVX5MWJZ.js.map} +1 -1
  45. package/dist/{chunk-CPUZRZ2A.js → chunk-RBJPAZFI.js} +25 -170
  46. package/dist/chunk-RBJPAZFI.js.map +1 -0
  47. package/dist/chunk-RRN3YTK7.js +6199 -0
  48. package/dist/chunk-RRN3YTK7.js.map +1 -0
  49. package/dist/chunk-RY4XKNV3.js +140 -0
  50. package/dist/chunk-RY4XKNV3.js.map +1 -0
  51. package/dist/chunk-S3IGX57D.js +14682 -0
  52. package/dist/chunk-S3IGX57D.js.map +1 -0
  53. package/dist/{config-XVXZ3V2Q.js → chunk-TFGSOPWR.js} +117 -62
  54. package/dist/chunk-TFGSOPWR.js.map +1 -0
  55. package/dist/chunk-TLCMMYLY.js +126 -0
  56. package/dist/chunk-TLCMMYLY.js.map +1 -0
  57. package/dist/{chunk-PAAL45FJ.js → chunk-UGX7YPX2.js} +3446 -448
  58. package/dist/chunk-UGX7YPX2.js.map +1 -0
  59. package/dist/chunk-UO22YNY4.js +69 -0
  60. package/dist/chunk-UO22YNY4.js.map +1 -0
  61. package/dist/chunk-UPIEAMGH.js +160 -0
  62. package/dist/chunk-UPIEAMGH.js.map +1 -0
  63. package/dist/chunk-XJIW7USF.js +217 -0
  64. package/dist/chunk-XJIW7USF.js.map +1 -0
  65. package/dist/{chunk-F5DZJLU3.js → chunk-YALGASEU.js} +2065 -1258
  66. package/dist/chunk-YALGASEU.js.map +1 -0
  67. package/dist/chunk-YBMMV4Y4.js +51 -0
  68. package/dist/chunk-YBMMV4Y4.js.map +1 -0
  69. package/dist/chunk-YIYQ2ZNT.js +286 -0
  70. package/dist/chunk-YIYQ2ZNT.js.map +1 -0
  71. package/dist/{chunk-376LHMK2.js → chunk-YN5SHIXQ.js} +36 -10
  72. package/dist/chunk-YN5SHIXQ.js.map +1 -0
  73. package/dist/chunk-Z52RA3NC.js +137 -0
  74. package/dist/chunk-Z52RA3NC.js.map +1 -0
  75. package/dist/chunk-Z6MFJV5Z.js +1330 -0
  76. package/dist/chunk-Z6MFJV5Z.js.map +1 -0
  77. package/dist/chunk-ZABPQJ3M.js +83 -0
  78. package/dist/chunk-ZABPQJ3M.js.map +1 -0
  79. package/dist/{chunk-WOHCAMDQ.js → chunk-ZMHYXMO6.js} +1887 -1029
  80. package/dist/chunk-ZMHYXMO6.js.map +1 -0
  81. package/dist/{chunk-3IMLRUZE.js → chunk-ZXTAQYNA.js} +6 -2
  82. package/dist/{cli-LMHGCZ5S.js → cli-4NNBEQYH.js} +175 -179
  83. package/dist/cli-4NNBEQYH.js.map +1 -0
  84. package/dist/config-2VSXWWHK.js +21 -0
  85. package/dist/doctor-JJPNIBYH.js +20 -0
  86. package/dist/doctor-JJPNIBYH.js.map +1 -0
  87. package/dist/ethersCompat-Df4a6QIZ-3YZIAXDK.js +18 -0
  88. package/dist/ethersCompat-Df4a6QIZ-3YZIAXDK.js.map +1 -0
  89. package/dist/index.js +1 -1
  90. package/dist/inspect-JGYJWFDP.js +96 -0
  91. package/dist/inspect-JGYJWFDP.js.map +1 -0
  92. package/dist/install-ZQRMSRGW.js +378 -0
  93. package/dist/install-ZQRMSRGW.js.map +1 -0
  94. package/dist/lookup-WYVTOMVY.js +113 -0
  95. package/dist/lookup-WYVTOMVY.js.map +1 -0
  96. package/dist/{mine.wasm-GQECMSGN.js → mine.wasm-6PIT3DK6.js} +2 -2
  97. package/dist/outcome-HKKT6QIA.js +66 -0
  98. package/dist/outcome-HKKT6QIA.js.map +1 -0
  99. package/dist/publish-D76B3MBK.js +1480 -0
  100. package/dist/publish-D76B3MBK.js.map +1 -0
  101. package/dist/{secp256k1-35YNNB6F.js → secp256k1-N3ZHU7LB.js} +5 -4
  102. package/dist/secp256k1-N3ZHU7LB.js.map +1 -0
  103. package/dist/usdc-JIAJW4SV.js +22 -0
  104. package/dist/usdc-JIAJW4SV.js.map +1 -0
  105. package/dist/viemAdapter-By13KInL-ZZE6UV4Y.js +14 -0
  106. package/dist/viemAdapter-By13KInL-ZZE6UV4Y.js.map +1 -0
  107. package/dist/wallet-6QKS6UHW.js +179 -0
  108. package/dist/wallet-6QKS6UHW.js.map +1 -0
  109. package/dist/wallet-PA54CRBL.js +30 -0
  110. package/dist/wallet-PA54CRBL.js.map +1 -0
  111. package/package.json +20 -15
  112. package/skills/tenjin/SKILL.md +199 -0
  113. package/skills/tenjin-publish/SKILL.md +96 -0
  114. package/skills/tenjin-search/SKILL.md +122 -0
  115. package/dist/chunk-376LHMK2.js.map +0 -1
  116. package/dist/chunk-CPUZRZ2A.js.map +0 -1
  117. package/dist/chunk-F5DZJLU3.js.map +0 -1
  118. package/dist/chunk-LTTRND4A.js.map +0 -1
  119. package/dist/chunk-PAAL45FJ.js.map +0 -1
  120. package/dist/chunk-QHIEYBLV.js +0 -320
  121. package/dist/chunk-QHIEYBLV.js.map +0 -1
  122. package/dist/chunk-SXQQ2ZSR.js.map +0 -1
  123. package/dist/chunk-WOHCAMDQ.js.map +0 -1
  124. package/dist/chunk-ZPZFPVDN.js.map +0 -1
  125. package/dist/cli-LMHGCZ5S.js.map +0 -1
  126. package/dist/config-XVXZ3V2Q.js.map +0 -1
  127. package/dist/doctor-MVLZRVPU.js.map +0 -1
  128. package/dist/usdc-OQUOFF6R.js +0 -18
  129. package/dist/wallet-NRKWLKHX.js +0 -153
  130. package/dist/wallet-NRKWLKHX.js.map +0 -1
  131. package/dist/wallet-QUWK2PA4.js +0 -22
  132. /package/dist/{_esm-ZH2J4QEH.js.map → _esm-C5CRWCYU.js.map} +0 -0
  133. /package/dist/{ccip-FXWZDDF5.js.map → _esm-FEASBWRP.js.map} +0 -0
  134. /package/dist/{chunk-3IMLRUZE.js.map → ccip-NQXCEK2U.js.map} +0 -0
  135. /package/dist/{secp256k1-35YNNB6F.js.map → ccip-XR5NE5QK.js.map} +0 -0
  136. /package/dist/{usdc-OQUOFF6R.js.map → chunk-ZXTAQYNA.js.map} +0 -0
  137. /package/dist/{wallet-QUWK2PA4.js.map → config-2VSXWWHK.js.map} +0 -0
  138. /package/dist/{mine.wasm-GQECMSGN.js.map → mine.wasm-6PIT3DK6.js.map} +0 -0
@@ -0,0 +1,1480 @@
1
+ import { createRequire as __tenjinCreateRequire } from 'node:module'; const require = __tenjinCreateRequire(import.meta.url);
2
+ import {
3
+ buildSiwxHeader,
4
+ headingOutline
5
+ } from "./chunk-S3IGX57D.js";
6
+ import {
7
+ dropCandidate,
8
+ readCandidate
9
+ } from "./chunk-TLCMMYLY.js";
10
+ import {
11
+ rateLimitError
12
+ } from "./chunk-YIYQ2ZNT.js";
13
+ import {
14
+ ATOMIC_RE,
15
+ UUID_RE
16
+ } from "./chunk-3OV7E5G5.js";
17
+ import {
18
+ sanitizeForTerminal
19
+ } from "./chunk-Z52RA3NC.js";
20
+ import {
21
+ CLIENT_HEADER,
22
+ httpRequest,
23
+ trimSlash
24
+ } from "./chunk-QNP7H47L.js";
25
+ import "./chunk-N62ROTMC.js";
26
+ import "./chunk-RRN3YTK7.js";
27
+ import "./chunk-CYOF7ROH.js";
28
+ import "./chunk-ZABPQJ3M.js";
29
+ import "./chunk-HSDCI7OV.js";
30
+ import {
31
+ resolveContextSettings,
32
+ resolvePublishSettings
33
+ } from "./chunk-UPIEAMGH.js";
34
+ import {
35
+ PublishModeSchema,
36
+ formatUsdDisplay,
37
+ parsePublishModeFlag,
38
+ parseUsdToAtomic,
39
+ toMoney
40
+ } from "./chunk-IYLHQMT5.js";
41
+ import {
42
+ describeWallet,
43
+ resolveWalletProvider
44
+ } from "./chunk-Z6MFJV5Z.js";
45
+ import "./chunk-YN5SHIXQ.js";
46
+ import {
47
+ hasCode
48
+ } from "./chunk-2I4QTOLB.js";
49
+ import "./chunk-YBMMV4Y4.js";
50
+ import "./chunk-UO22YNY4.js";
51
+ import {
52
+ writeFileAtomic
53
+ } from "./chunk-MUBUQVC3.js";
54
+ import {
55
+ CliError,
56
+ external_exports,
57
+ sessionPath
58
+ } from "./chunk-OYMYCW7J.js";
59
+ import "./chunk-NCE5NSN6.js";
60
+ import "./chunk-RY4XKNV3.js";
61
+ import "./chunk-QVX5MWJZ.js";
62
+ import "./chunk-YALGASEU.js";
63
+ import "./chunk-HIVZG55D.js";
64
+ import "./chunk-RBJPAZFI.js";
65
+ import "./chunk-5CLSD5YN.js";
66
+ import "./chunk-ZXTAQYNA.js";
67
+
68
+ // src/commands/publish.ts
69
+ import { readFile as readFile2 } from "fs/promises";
70
+
71
+ // src/lib/scan.ts
72
+ var LONG_QUOTE_WORDS = 80;
73
+ function scan(text) {
74
+ const lines = text.split("\n");
75
+ const findings = [
76
+ ...scanHex64(lines),
77
+ ...scanLineDetectors(lines),
78
+ ...scanPemBlocks(lines),
79
+ ...scanLongVerbatim(lines)
80
+ ];
81
+ return dedupeAndSort(suppressEmailsInDbUris(findings));
82
+ }
83
+ function suppressEmailsInDbUris(findings) {
84
+ const uris = findings.filter((f) => f.check === "db-connection-uri");
85
+ if (uris.length === 0) return findings;
86
+ return findings.filter(
87
+ (f) => f.check !== "email" || !uris.some((u) => u.line === f.line && f.span[0] >= u.span[0] && f.span[1] <= u.span[1])
88
+ );
89
+ }
90
+ function maskKeeping(match, prefixLen) {
91
+ const prefix = match.slice(0, Math.min(prefixLen, match.length));
92
+ return `${prefix}\u2026[redacted ${match.length - prefix.length} chars]`;
93
+ }
94
+ var LINE_DETECTORS = [
95
+ // Secrets/keys — severity block, excerpt always masked. Provider patterns
96
+ // adapted from gitleaks (MIT). The 0x-64-hex EVM key is handled separately
97
+ // (scanHex64) so a tx/block hash is not hard-blocked. See header + NOTICE.md.
98
+ {
99
+ check: "aws-access-key",
100
+ severity: "block",
101
+ // gitleaks aws-access-token.
102
+ re: /\b(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\b/g,
103
+ excerpt: (m) => maskKeeping(m[0], 4)
104
+ },
105
+ {
106
+ check: "jwt",
107
+ severity: "block",
108
+ // gitleaks jwt (three base64url segments, header starts eyJ).
109
+ re: /\beyJ[A-Za-z0-9_-]{5,}\.eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\b/g,
110
+ excerpt: (m) => maskKeeping(m[0], 3)
111
+ },
112
+ {
113
+ check: "github-token",
114
+ severity: "block",
115
+ // gitleaks github-pat / -oauth / -app / -refresh (36 chars) + fine-grained pat.
116
+ re: /\b(?:gh[posru]_[0-9A-Za-z]{36}|github_pat_[0-9A-Za-z_]{82})\b/g,
117
+ excerpt: (m) => maskKeeping(m[0], 4)
118
+ },
119
+ {
120
+ check: "slack-token",
121
+ severity: "block",
122
+ // gitleaks slack tokens (bot/user/app/refresh/legacy prefixes).
123
+ re: /\bxox[baprs]-[0-9A-Za-z-]{10,}\b/g,
124
+ excerpt: (m) => maskKeeping(m[0], 5)
125
+ },
126
+ {
127
+ check: "stripe-token",
128
+ severity: "block",
129
+ // gitleaks stripe-access-token.
130
+ re: /\b[sr]k_(?:live|test)_[0-9A-Za-z]{10,99}\b/g,
131
+ excerpt: (m) => maskKeeping(m[0], 8)
132
+ },
133
+ {
134
+ check: "anthropic-key",
135
+ severity: "block",
136
+ // gitleaks anthropic-api-key. Ordered before openai so sk-ant- isn't
137
+ // mis-typed as a bare OpenAI sk- key.
138
+ re: /\bsk-ant-[0-9A-Za-z_-]{20,}\b/g,
139
+ excerpt: (m) => maskKeeping(m[0], 7)
140
+ },
141
+ {
142
+ check: "openai-key",
143
+ severity: "block",
144
+ // gitleaks openai-api-key: classic sk- plus the modern sk-proj-/sk-svcacct-/
145
+ // sk-admin- keys. The body is base62 and MUST contain a digit (real keys are
146
+ // high-entropy), which keeps sk- kebab identifiers (sk-user-profile-updated…)
147
+ // from hard-blocking. `(?!ant-)` keeps an Anthropic key out. Only the base62
148
+ // leading run of a modern key is matched — enough to flag it. The run is
149
+ // terminated by a non-base62 lookahead (not \b, which a trailing `_` — a word
150
+ // char — would not fire), so an underscore-adjacent key still matches.
151
+ // Assumption + chosen tradeoff: a real key opens with a >=20-char base62 run,
152
+ // so the {20,} floor is what separates a key from a kebab identifier. A key
153
+ // whose FIRST segment is shorter than 20 (e.g. `sk-proj-abc123_` then more,
154
+ // broken up by underscores) slips past this BARE-PASTE detector; that is
155
+ // accepted, because such a value in an assignment still warns via
156
+ // secret-assignment — the bare-paste path is the only gap.
157
+ re: /\bsk-(?!ant-)(?:proj-|svcacct-|admin-)?(?=[0-9A-Za-z]*[0-9])[0-9A-Za-z]{20,}(?![0-9A-Za-z])/g,
158
+ excerpt: (m) => maskKeeping(m[0], 3)
159
+ },
160
+ {
161
+ check: "google-key",
162
+ severity: "block",
163
+ // gitleaks gcp-api-key (AIza) and gcp-oauth-client-secret (GOCSPX-).
164
+ re: /\b(?:AIza[0-9A-Za-z_-]{35}|GOCSPX-[0-9A-Za-z_-]{20,})\b/g,
165
+ excerpt: (m) => maskKeeping(m[0], m[0].startsWith("AIza") ? 4 : 7)
166
+ },
167
+ {
168
+ check: "npm-token",
169
+ severity: "block",
170
+ // gitleaks npm-access-token.
171
+ re: /\bnpm_[0-9A-Za-z]{36}\b/g,
172
+ excerpt: (m) => maskKeeping(m[0], 4)
173
+ },
174
+ {
175
+ check: "db-connection-uri",
176
+ severity: "block",
177
+ // A connection string with an embedded password (scheme://user:pass@host).
178
+ // Only the password (group 3) is secret; the excerpt masks it and keeps the
179
+ // rest so the finding is legible. Placeholder-gated like the other high-
180
+ // confidence shapes so a docs example (postgres://postgres:postgres@…,
181
+ // ://user:<password>@…) doesn't hard-block; a real password still blocks.
182
+ re: /\b([a-z][a-z0-9+.-]*):\/\/([^\s:@/]+):([^\s@/]+)@([^\s/:]+)/gi,
183
+ excerpt: (m) => `${m[1]}://${m[2]}:[redacted]@${m[4]}`,
184
+ skip: (m) => isPlaceholder(m[3] ?? "") || isExamplePassword(m[3] ?? "")
185
+ },
186
+ {
187
+ check: "bearer-token",
188
+ severity: "block",
189
+ // An Authorization: Bearer header carrying a live token; placeholder-gated
190
+ // like secret-assignment so `Bearer <token>` examples don't hard-block.
191
+ re: /\bAuthorization:\s*Bearer\s+(\S{8,})/gi,
192
+ excerpt: (m) => `Authorization: Bearer [redacted ${m[1]?.length ?? 0} chars]`,
193
+ skip: (m) => isPlaceholder(m[1] ?? "")
194
+ },
195
+ // Generic secret-named assignment — WARN, not block (review): a keyword match is
196
+ // lower-confidence than a structured shape, and warn still forces confirmation
197
+ // in default auto mode, so nothing publishes unseen while benign config
198
+ // (SECRET_NAME=…, MYSQL_ROOT_PASSWORD=…) is not permanently non-bypassable. The
199
+ // excerpt stays masked. Placeholder and structural (path/URL/regex) values are
200
+ // skipped entirely.
201
+ {
202
+ check: "secret-assignment",
203
+ severity: "warn",
204
+ // Key: api[_-]?key, secret, access/private key, passw(or)?d, token,
205
+ // credential(s), auth token — camelCase-insensitive. Value: a quoted string
206
+ // (interior spaces allowed) or an unquoted run, ≥6 chars. Value is masked.
207
+ re: /\b([A-Za-z0-9_]*(?:API[_-]?KEY|SECRET|ACCESS[_-]?KEY|PRIVATE[_-]?KEY|PASSW(?:OR)?D|TOKEN|CREDENTIALS?|AUTH[_-]?TOKEN)[A-Za-z0-9_]*)\s*[:=]\s*("[^"]{6,}"|'[^']{6,}'|[^\s"']{6,})/gi,
208
+ excerpt: (m) => `${m[1]}=[redacted ${dequote(m[2] ?? "").length} chars]`,
209
+ skip: (m) => {
210
+ const raw = m[2] ?? "";
211
+ const value = dequote(raw);
212
+ if (isPlaceholder(value) || isStructural(value)) return true;
213
+ return raw === value && isCodeExpression(value);
214
+ }
215
+ },
216
+ // Wallet addresses — warn (a contract address may be intentional). Not a secret,
217
+ // shown abbreviated. viem's checksum validation is deliberately NOT used to gate
218
+ // this: EIP-55 strict validation rejects the valid all-lowercase/all-uppercase
219
+ // forms, which a scanner must still surface, so shape matching is the right test.
220
+ {
221
+ check: "wallet-address",
222
+ severity: "warn",
223
+ re: /(?<![0-9a-fx])0x[0-9a-fA-F]{40}(?![0-9a-fA-F])/gi,
224
+ excerpt: (m) => `${m[0].slice(0, 6)}\u2026${m[0].slice(-4)}`
225
+ },
226
+ // Employer-internal markers — warn.
227
+ {
228
+ check: "confidential-marker",
229
+ severity: "warn",
230
+ re: /\b(?:CONFIDENTIAL|INTERNAL ONLY)\b/gi,
231
+ excerpt: (m) => m[0]
232
+ },
233
+ {
234
+ check: "internal-hostname",
235
+ severity: "warn",
236
+ re: /\b(?:[a-z0-9-]+\.)+(?:internal|corp|local|lan|intranet)\b/gi,
237
+ excerpt: (m) => m[0]
238
+ },
239
+ // Personal data — warn. `email` subsumes corp-domain emails (an internal marker).
240
+ {
241
+ check: "email",
242
+ severity: "warn",
243
+ re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
244
+ excerpt: (m) => m[0]
245
+ },
246
+ {
247
+ check: "phone",
248
+ severity: "warn",
249
+ re: /(?<!\d)(?:\+?\d{1,2}[\s.-]?)?(?:\(\d{3}\)|\d{3})[\s.-]\d{3}[\s.-]\d{4}(?!\d)/g,
250
+ excerpt: (m) => m[0]
251
+ }
252
+ ];
253
+ function scanLineDetectors(lines) {
254
+ const out = [];
255
+ for (let i = 0; i < lines.length; i++) {
256
+ const line = lines[i] ?? "";
257
+ for (const detector of LINE_DETECTORS) {
258
+ detector.re.lastIndex = 0;
259
+ let m;
260
+ while ((m = detector.re.exec(line)) !== null) {
261
+ if (!detector.skip?.(m)) {
262
+ out.push({
263
+ check: detector.check,
264
+ severity: detector.severity,
265
+ line: i + 1,
266
+ span: [m.index, m.index + m[0].length],
267
+ excerpt: detector.excerpt(m)
268
+ });
269
+ }
270
+ }
271
+ }
272
+ }
273
+ return out;
274
+ }
275
+ function dequote(value) {
276
+ const m = /^"([^"]*)"$|^'([^']*)'$/.exec(value);
277
+ return m !== null ? m[1] ?? m[2] ?? "" : value;
278
+ }
279
+ var PLACEHOLDER_WORD = /^(?:xxx+|example|example-value|placeholder|redacted|changeme|dummy|sample|todo|tbd|none|null|secret|test|\*+|\.{3,})$/i;
280
+ var PLACEHOLDER_PREFIX = /^(?:your|my)[-_][a-z]+(?:[-_][a-z]+)*$/i;
281
+ function isPlaceholder(value) {
282
+ if (/^[<${%]/.test(value)) return true;
283
+ return PLACEHOLDER_WORD.test(value) || PLACEHOLDER_PREFIX.test(value);
284
+ }
285
+ function isCodeExpression(value) {
286
+ const v = value.replace(/[,;]+$/, "");
287
+ if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(v)) return true;
288
+ if (/^[A-Za-z_$][\w$.]*\([^)]*\)$/.test(v)) return true;
289
+ if (/\($/.test(v)) return true;
290
+ return /^[a-z_$][a-z_$]*$/.test(v);
291
+ }
292
+ function isStructural(value) {
293
+ return /^(?:\.?\.?\/|~\/|https?:\/\/|\^)/.test(value);
294
+ }
295
+ var EXAMPLE_PASSWORDS = /* @__PURE__ */ new Set([
296
+ "password",
297
+ "postgres",
298
+ "mysql",
299
+ "root",
300
+ "admin",
301
+ "changeme",
302
+ "example",
303
+ "secret",
304
+ "test",
305
+ "guest",
306
+ "toor"
307
+ ]);
308
+ function isExamplePassword(password) {
309
+ return EXAMPLE_PASSWORDS.has(password.toLowerCase());
310
+ }
311
+ var HEX64_RE = /(?<![0-9a-fx])0x[0-9a-fA-F]{64}(?![0-9a-fA-F])/gi;
312
+ var HASH_LABEL_RE = /(?:^|[^a-z0-9])(?:blockhash|txhash|txn|tx|hash)[\s/:=]*$/i;
313
+ function scanHex64(lines) {
314
+ const out = [];
315
+ for (let i = 0; i < lines.length; i++) {
316
+ const line = lines[i] ?? "";
317
+ HEX64_RE.lastIndex = 0;
318
+ let m;
319
+ while ((m = HEX64_RE.exec(line)) !== null) {
320
+ const hash = isHashContext(line, m.index);
321
+ out.push({
322
+ check: hash ? "hex32-value" : "raw-private-key",
323
+ severity: hash ? "warn" : "block",
324
+ line: i + 1,
325
+ span: [m.index, m.index + m[0].length],
326
+ excerpt: hash ? `${m[0].slice(0, 6)}\u2026${m[0].slice(-4)}` : maskKeeping(m[0], 2)
327
+ });
328
+ }
329
+ }
330
+ return out;
331
+ }
332
+ function isHashContext(line, matchIndex) {
333
+ const before = line.slice(0, matchIndex);
334
+ if (HASH_LABEL_RE.test(before)) return true;
335
+ const prefix = /(\S*)$/.exec(before)?.[1] ?? "";
336
+ return /^https?:\/\//.test(line.slice(matchIndex - prefix.length));
337
+ }
338
+ var PEM_BEGIN = /-----BEGIN[ A-Z0-9]*PRIVATE KEY(?: BLOCK)?-----/;
339
+ function scanPemBlocks(lines) {
340
+ const out = [];
341
+ for (let i = 0; i < lines.length; i++) {
342
+ const line = lines[i] ?? "";
343
+ const m = PEM_BEGIN.exec(line);
344
+ if (m !== null) {
345
+ out.push({
346
+ check: "pem-private-key",
347
+ severity: "block",
348
+ line: i + 1,
349
+ span: [m.index, m.index + m[0].length],
350
+ excerpt: m[0]
351
+ // the header marker only — no key material follows on it
352
+ });
353
+ }
354
+ }
355
+ return out;
356
+ }
357
+ var FENCE = /^(\s*)(`{3,}|~{3,})/;
358
+ function scanLongVerbatim(lines) {
359
+ const out = [];
360
+ let i = 0;
361
+ while (i < lines.length) {
362
+ const line = lines[i] ?? "";
363
+ const fence = FENCE.exec(line);
364
+ if (fence !== null) {
365
+ const marker = fence[2] ?? "```";
366
+ const start = i;
367
+ const body = [];
368
+ i++;
369
+ while (i < lines.length && !closesFence(lines[i] ?? "", marker)) {
370
+ body.push(lines[i] ?? "");
371
+ i++;
372
+ }
373
+ i++;
374
+ pushIfLong(out, body, start, line);
375
+ continue;
376
+ }
377
+ if (/^\s*>/.test(line)) {
378
+ const start = i;
379
+ const body = [];
380
+ while (i < lines.length && /^\s*>/.test(lines[i] ?? "")) {
381
+ body.push((lines[i] ?? "").replace(/^\s*>\s?/, ""));
382
+ i++;
383
+ }
384
+ pushIfLong(out, body, start, lines[start] ?? "");
385
+ continue;
386
+ }
387
+ i++;
388
+ }
389
+ return out;
390
+ }
391
+ function closesFence(line, marker) {
392
+ const m = FENCE.exec(line);
393
+ return m !== null && (m[2] ?? "").startsWith(marker);
394
+ }
395
+ function pushIfLong(out, body, startLine, firstLine) {
396
+ const text = body.join(" ").trim();
397
+ const words = text.length === 0 ? 0 : text.split(/\s+/).length;
398
+ if (words < LONG_QUOTE_WORDS) return;
399
+ const excerpt = text.length > 60 ? `${text.slice(0, 57)}\u2026` : text;
400
+ out.push({
401
+ check: "long-verbatim-quote",
402
+ severity: "warn",
403
+ line: startLine + 1,
404
+ span: [0, firstLine.length],
405
+ excerpt
406
+ });
407
+ }
408
+ function dedupeAndSort(findings) {
409
+ const seen = /* @__PURE__ */ new Set();
410
+ const unique = findings.filter((f) => {
411
+ const key = `${f.check}:${f.line}:${f.span[0]}:${f.span[1]}`;
412
+ if (seen.has(key)) return false;
413
+ seen.add(key);
414
+ return true;
415
+ });
416
+ return unique.sort(
417
+ (a, b) => a.line - b.line || a.span[0] - b.span[0] || a.check.localeCompare(b.check)
418
+ );
419
+ }
420
+
421
+ // src/lib/card.ts
422
+ var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
423
+ function parseFrontmatter(text) {
424
+ const match = FRONTMATTER_RE.exec(text);
425
+ if (match === null) return { frontmatter: {}, body: text };
426
+ const block = match[1] ?? "";
427
+ const body = text.slice(match[0].length);
428
+ return { frontmatter: parseBlock(block), body };
429
+ }
430
+ function indentOf(line) {
431
+ return line.length - line.trimStart().length;
432
+ }
433
+ function parseBlock(block) {
434
+ const lines = block.split(/\r?\n/).filter((l) => l.trim().length > 0 && !/^\s*#/.test(l));
435
+ const out = {};
436
+ let i = 0;
437
+ while (i < lines.length) {
438
+ const line = lines[i] ?? "";
439
+ if (indentOf(line) !== 0) {
440
+ throw usage(`Unexpected indentation in frontmatter: ${JSON.stringify(line)}`);
441
+ }
442
+ const kv = splitKey(line);
443
+ if (kv === null) throw usage(`Malformed frontmatter line: ${JSON.stringify(line)}`);
444
+ const { key, value } = kv;
445
+ if (value.length > 0) {
446
+ out[key] = parseInlineValue(value);
447
+ i++;
448
+ continue;
449
+ }
450
+ const child = [];
451
+ i++;
452
+ while (i < lines.length && indentOf(lines[i] ?? "") > 0) {
453
+ child.push(lines[i] ?? "");
454
+ i++;
455
+ }
456
+ if (child.length === 0) {
457
+ out[key] = "";
458
+ } else if (child.every((c) => /^\s*-\s/.test(c))) {
459
+ out[key] = child.map((c) => unquote(c.replace(/^\s*-\s+/, "").trim()));
460
+ } else {
461
+ out[key] = parseNestedMap(child);
462
+ }
463
+ }
464
+ return out;
465
+ }
466
+ function parseNestedMap(lines) {
467
+ const out = {};
468
+ const baseIndent = indentOf(lines[0] ?? "");
469
+ let i = 0;
470
+ while (i < lines.length) {
471
+ const line = lines[i] ?? "";
472
+ if (indentOf(line) !== baseIndent)
473
+ throw usage(`Malformed nested frontmatter: ${JSON.stringify(line)}`);
474
+ const kv = splitKey(line);
475
+ if (kv === null) throw usage(`Malformed nested frontmatter line: ${JSON.stringify(line)}`);
476
+ const { key, value } = kv;
477
+ if (value.length > 0) {
478
+ const parsed = parseInlineValue(value);
479
+ out[key] = Array.isArray(parsed) ? parsed : [parsed];
480
+ i++;
481
+ continue;
482
+ }
483
+ const items = [];
484
+ i++;
485
+ while (i < lines.length && indentOf(lines[i] ?? "") > baseIndent) {
486
+ const c = lines[i] ?? "";
487
+ if (!/^\s*-\s/.test(c))
488
+ throw usage(`Expected a list item in frontmatter: ${JSON.stringify(c)}`);
489
+ items.push(unquote(c.replace(/^\s*-\s+/, "").trim()));
490
+ i++;
491
+ }
492
+ out[key] = items;
493
+ }
494
+ return out;
495
+ }
496
+ function splitKey(line) {
497
+ const m = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*:\s?(.*)$/.exec(line);
498
+ if (m === null || m[1] === void 0) return null;
499
+ guardKey(m[1]);
500
+ return { key: m[1], value: (m[2] ?? "").trim() };
501
+ }
502
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
503
+ function guardKey(key) {
504
+ if (UNSAFE_KEYS.has(key)) {
505
+ throw new CliError("USAGE", `Unsupported key: ${JSON.stringify(key)}`, {
506
+ fix: "Rename it; __proto__, constructor, and prototype are not allowed as keys."
507
+ });
508
+ }
509
+ }
510
+ function parseInlineValue(value) {
511
+ if (value.startsWith("[") && value.endsWith("]")) {
512
+ const inner = value.slice(1, -1).trim();
513
+ if (inner.length === 0) return [];
514
+ return inner.split(",").map((v) => unquote(v.trim()));
515
+ }
516
+ return unquote(value);
517
+ }
518
+ function unquote(value) {
519
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2 || value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
520
+ return value.slice(1, -1);
521
+ }
522
+ return value;
523
+ }
524
+ var ARTIFACT_TYPES = ["document", "skill", "dataset"];
525
+ var TEMPORAL_MODES = ["snapshot", "maintained", "evergreen"];
526
+ var MEDIA_TYPE_RE = /^[a-z0-9]+\/[a-z0-9][a-z0-9.+-]*$/i;
527
+ var CANONICAL_KEY_RE = /^[a-z][a-z0-9_]{0,31}$/;
528
+ var ISO_OFFSET_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
529
+ var CardErrors = class {
530
+ fieldErrors = {};
531
+ add(field, message) {
532
+ const key = `resource.${field}`;
533
+ (this.fieldErrors[key] ??= []).push(message);
534
+ }
535
+ get empty() {
536
+ return Object.keys(this.fieldErrors).length === 0;
537
+ }
538
+ throwIfAny() {
539
+ if (this.empty) return;
540
+ const first = Object.entries(this.fieldErrors)[0];
541
+ const summary = first !== void 0 ? `${first[0]}: ${first[1][0]}` : "invalid card";
542
+ throw new CliError("USAGE", `Invalid answer card (${summary})`, {
543
+ fix: "Correct the reported resource.<field> values (frontmatter or flags), then re-run.",
544
+ details: { fieldErrors: this.fieldErrors }
545
+ });
546
+ }
547
+ };
548
+ function deriveCard(frontmatter, flags) {
549
+ const errs = new CardErrors();
550
+ const card = {};
551
+ const scalar = (fmKey, flagVal) => {
552
+ if (flagVal !== void 0) return flagVal;
553
+ const fm = frontmatter[fmKey];
554
+ if (fm === void 0) return void 0;
555
+ if (typeof fm !== "string") {
556
+ errs.add(fmKey, "expected a single string value");
557
+ return void 0;
558
+ }
559
+ return fm;
560
+ };
561
+ const list = (fmKey, flagVal) => {
562
+ if (flagVal !== void 0 && flagVal.length > 0) return flagVal;
563
+ const fm = frontmatter[fmKey];
564
+ if (fm === void 0) return void 0;
565
+ if (!Array.isArray(fm)) {
566
+ errs.add(fmKey, "expected a list");
567
+ return void 0;
568
+ }
569
+ return fm;
570
+ };
571
+ const artifactType = scalar("artifactType", flags.artifactType);
572
+ if (artifactType !== void 0) {
573
+ if (ARTIFACT_TYPES.includes(artifactType)) {
574
+ card.artifactType = artifactType;
575
+ } else {
576
+ errs.add("artifactType", `must be one of ${ARTIFACT_TYPES.join(", ")}`);
577
+ }
578
+ }
579
+ const mediaType = scalar("mediaType", void 0);
580
+ if (mediaType !== void 0) {
581
+ if (mediaType.length > 100) errs.add("mediaType", "must be at most 100 characters");
582
+ else if (!MEDIA_TYPE_RE.test(mediaType))
583
+ errs.add("mediaType", "must be a MIME type, e.g. text/markdown");
584
+ else card.mediaType = mediaType;
585
+ }
586
+ const temporalMode = scalar("temporalMode", flags.temporalMode);
587
+ if (temporalMode !== void 0) {
588
+ if (TEMPORAL_MODES.includes(temporalMode)) {
589
+ card.temporalMode = temporalMode;
590
+ } else {
591
+ errs.add("temporalMode", `must be one of ${TEMPORAL_MODES.join(", ")}`);
592
+ }
593
+ }
594
+ const asOf = validateIso(scalar("asOf", flags.asOf), "asOf", errs);
595
+ if (asOf !== void 0) card.asOf = asOf;
596
+ const validUntil = validateIso(scalar("validUntil", flags.validUntil), "validUntil", errs);
597
+ if (validUntil !== void 0) card.validUntil = validUntil;
598
+ if (asOf !== void 0 && validUntil !== void 0 && Date.parse(validUntil) < Date.parse(asOf)) {
599
+ errs.add("validUntil", "must be at or after asOf");
600
+ }
601
+ const supersedes = scalar("supersedesPostId", void 0);
602
+ if (supersedes !== void 0) {
603
+ if (UUID_RE.test(supersedes)) card.supersedesPostId = supersedes;
604
+ else errs.add("supersedesPostId", "must be a post uuid");
605
+ }
606
+ const questions = list("questionsAnswered", flags.question);
607
+ if (questions !== void 0) {
608
+ validateStringList(questions, "questionsAnswered", errs);
609
+ card.questionsAnswered = questions;
610
+ }
611
+ const tasks = list("tasksSupported", flags.task);
612
+ if (tasks !== void 0) {
613
+ validateStringList(tasks, "tasksSupported", errs);
614
+ card.tasksSupported = tasks;
615
+ }
616
+ const scope = scalar("scope", flags.scope);
617
+ if (scope !== void 0) {
618
+ if (scope.length > 500) errs.add("scope", "must be at most 500 characters");
619
+ else card.scope = scope;
620
+ }
621
+ const exclusions = scalar("exclusions", flags.exclusions);
622
+ if (exclusions !== void 0) {
623
+ if (exclusions.length > 500) errs.add("exclusions", "must be at most 500 characters");
624
+ else card.exclusions = exclusions;
625
+ }
626
+ const provenance = scalar("provenanceSummary", flags.provenance);
627
+ if (provenance !== void 0) {
628
+ if (provenance.length > 500) errs.add("provenanceSummary", "must be at most 500 characters");
629
+ else card.provenanceSummary = provenance;
630
+ }
631
+ const methodology = scalar("methodologySummary", flags.methodology);
632
+ if (methodology !== void 0) {
633
+ if (methodology.length > 500) errs.add("methodologySummary", "must be at most 500 characters");
634
+ else card.methodologySummary = methodology;
635
+ }
636
+ const appliesTo = flags.appliesTo ?? readAppliesTo(frontmatter, errs);
637
+ if (appliesTo !== void 0) {
638
+ const validated = validateAppliesTo(appliesTo, errs);
639
+ if (validated !== void 0) card.appliesTo = validated;
640
+ }
641
+ const cadence = scalar("maintenanceCadence", void 0);
642
+ if (cadence !== void 0) {
643
+ if (cadence.length > 120) errs.add("maintenanceCadence", "must be at most 120 characters");
644
+ else card.maintenanceCadence = cadence;
645
+ }
646
+ const repro = scalar("reproductionMinutes", void 0);
647
+ if (repro !== void 0) {
648
+ const n = Number(repro);
649
+ if (!Number.isInteger(n) || n < 0 || n > 1e6) {
650
+ errs.add("reproductionMinutes", "must be an integer from 0 to 1000000");
651
+ } else {
652
+ card.reproductionMinutes = n;
653
+ }
654
+ }
655
+ const cost = scalar("estimatedPaidInputCost", void 0);
656
+ if (cost !== void 0) {
657
+ if (ATOMIC_RE.test(cost)) card.estimatedPaidInputCost = cost;
658
+ else errs.add("estimatedPaidInputCost", "must be an atomic USDC digit string");
659
+ }
660
+ errs.throwIfAny();
661
+ return Object.keys(card).length > 0 ? card : void 0;
662
+ }
663
+ function validateIso(value, field, errs) {
664
+ if (value === void 0) return void 0;
665
+ if (!ISO_OFFSET_RE.test(value) || !Number.isFinite(Date.parse(value))) {
666
+ errs.add(field, "must be an ISO-8601 timestamp with an offset, e.g. 2026-07-01T00:00:00Z");
667
+ return void 0;
668
+ }
669
+ return value;
670
+ }
671
+ function validateStringList(list, field, errs) {
672
+ if (list.length > 10) errs.add(field, "at most 10 items");
673
+ for (const item of list) {
674
+ if (item.length === 0) errs.add(field, "items must be non-empty");
675
+ else if (item.length > 200) errs.add(field, "each item is at most 200 characters");
676
+ }
677
+ }
678
+ function readAppliesTo(frontmatter, errs) {
679
+ const fm = frontmatter.appliesTo;
680
+ if (fm === void 0) return void 0;
681
+ if (typeof fm === "string" || Array.isArray(fm)) {
682
+ errs.add("appliesTo", "must be a map of key to a list of values");
683
+ return void 0;
684
+ }
685
+ return fm;
686
+ }
687
+ function validateAppliesTo(appliesTo, errs) {
688
+ const keys = Object.keys(appliesTo);
689
+ if (keys.length > 8) errs.add("appliesTo", "at most 8 keys");
690
+ const out = {};
691
+ for (const [key, rawValues] of Object.entries(appliesTo)) {
692
+ if (!CANONICAL_KEY_RE.test(key)) {
693
+ errs.add("appliesTo", `key ${JSON.stringify(key)} must match ^[a-z][a-z0-9_]{0,31}$`);
694
+ continue;
695
+ }
696
+ const values = rawValues.map((v) => v.trim());
697
+ if (values.length > 20) errs.add("appliesTo", `key ${key} has more than 20 values`);
698
+ for (const v of values) {
699
+ if (v.length < 1 || v.length > 120) {
700
+ errs.add("appliesTo", `key ${key} values must be 1 to 120 characters`);
701
+ }
702
+ }
703
+ out[key] = values;
704
+ }
705
+ return Object.keys(out).length > 0 ? out : void 0;
706
+ }
707
+ function parseAppliesToFlags(pairs) {
708
+ const out = {};
709
+ for (const pair of pairs) {
710
+ const eq = pair.indexOf("=");
711
+ if (eq <= 0) {
712
+ throw new CliError("USAGE", `Invalid --applies-to ${JSON.stringify(pair)}`, {
713
+ fix: "Use key=value, e.g. --applies-to products=Vercel."
714
+ });
715
+ }
716
+ const key = pair.slice(0, eq).trim();
717
+ const value = pair.slice(eq + 1).trim();
718
+ guardKey(key);
719
+ (out[key] ??= []).push(value);
720
+ }
721
+ return out;
722
+ }
723
+ var MISSING_SENTENCES = {
724
+ questionsOrTasks: "Add at least one question answered or task supported.",
725
+ scope: "Describe the scope (what this piece covers).",
726
+ exclusions: "State the exclusions (what this piece does not cover).",
727
+ asOf: "Set an as-of date (a snapshot card needs one).",
728
+ provenanceOrMethodology: "Add a provenance or methodology summary."
729
+ };
730
+ function missingSentences(tokens) {
731
+ return tokens.map((t) => MISSING_SENTENCES[t] ?? t);
732
+ }
733
+ function localCardEligibility(card) {
734
+ const tokens = [];
735
+ const hasQuestionsOrTasks = (card?.questionsAnswered?.some(hasText) ?? false) || (card?.tasksSupported?.some(hasText) ?? false);
736
+ if (!hasQuestionsOrTasks) tokens.push("questionsOrTasks");
737
+ if (!hasText(card?.scope)) tokens.push("scope");
738
+ if (!hasText(card?.exclusions)) tokens.push("exclusions");
739
+ if (card?.temporalMode === "snapshot" && !hasText(card.asOf)) tokens.push("asOf");
740
+ if (!hasText(card?.provenanceSummary) && !hasText(card?.methodologySummary)) {
741
+ tokens.push("provenanceOrMethodology");
742
+ }
743
+ return { cacheEligible: tokens.length === 0, missing: missingSentences(tokens) };
744
+ }
745
+ function hasText(value) {
746
+ return value !== void 0 && value.trim().length > 0;
747
+ }
748
+ function usage(message) {
749
+ return new CliError("USAGE", message, {
750
+ fix: "Frontmatter supports key: value, key: [a, b], block lists, and a one-level appliesTo map."
751
+ });
752
+ }
753
+
754
+ // src/lib/posts-api.ts
755
+ var PUBLISH_STATUSES = ["draft", "published", "unlisted"];
756
+ var PRICE_RE = /^(0|[1-9]\d{0,12})$/;
757
+ var HANDLE_RE = /^[a-z0-9-]{2,32}$/;
758
+ var RESERVED_HANDLES = /* @__PURE__ */ new Set(["latest"]);
759
+ function buildPostCreateBody(input) {
760
+ const isDraft = input.status === "draft";
761
+ const title = input.title?.trim();
762
+ const bodyMd = input.bodyMd;
763
+ const hasTitle = title !== void 0 && title.length > 0;
764
+ const hasBody = bodyMd !== void 0 && bodyMd.trim().length > 0;
765
+ if (isDraft && !hasTitle && !hasBody) {
766
+ throw new CliError("USAGE", "A draft needs a title or a body.", {
767
+ fix: "Add a `title:` or some Markdown; a completely empty draft is rejected."
768
+ });
769
+ }
770
+ if (!isDraft && !hasTitle) {
771
+ throw new CliError("USAGE", "A published post needs a title.", {
772
+ fix: "Add a `title:` to the frontmatter or a leading `# Heading`, or pass --draft."
773
+ });
774
+ }
775
+ if (!isDraft && !hasBody) {
776
+ throw new CliError("USAGE", "A published post needs a body.", {
777
+ fix: "Add Markdown below the frontmatter, or pass --draft."
778
+ });
779
+ }
780
+ if (title !== void 0 && title.length > 200) {
781
+ throw new CliError("USAGE", "title must be at most 200 characters.");
782
+ }
783
+ if (bodyMd !== void 0 && bodyMd.length > 2e5) {
784
+ throw new CliError("USAGE", "bodyMd must be at most 200000 characters.");
785
+ }
786
+ if (input.excerpt !== void 0 && input.excerpt.length > 500) {
787
+ throw new CliError("USAGE", "excerpt must be at most 500 characters.");
788
+ }
789
+ if (input.tags !== void 0) {
790
+ if (input.tags.length > 5) throw new CliError("USAGE", "at most 5 tags.");
791
+ for (const tag of input.tags) {
792
+ if (tag.length === 0 || tag.length > 50) {
793
+ throw new CliError("USAGE", "each tag is 1 to 50 characters.");
794
+ }
795
+ }
796
+ }
797
+ if (input.priceAtomic !== void 0 && !PRICE_RE.test(input.priceAtomic)) {
798
+ throw new CliError("USAGE", `Invalid price: ${JSON.stringify(input.priceAtomic)}`, {
799
+ fix: "A price is an atomic USDC integer up to 13 digits, e.g. 100000 for $0.10."
800
+ });
801
+ }
802
+ if (input.handle !== void 0) {
803
+ if (!HANDLE_RE.test(input.handle)) {
804
+ throw new CliError("USAGE", `Invalid handle: ${JSON.stringify(input.handle)}`, {
805
+ fix: "A handle is 2 to 32 chars of a-z, 0-9, or hyphen."
806
+ });
807
+ }
808
+ if (input.handle.startsWith("0x") || RESERVED_HANDLES.has(input.handle)) {
809
+ throw new CliError("USAGE", `Reserved handle: ${JSON.stringify(input.handle)}`, {
810
+ fix: "Pick a handle that is not 0x-prefixed and not a reserved word (e.g. latest)."
811
+ });
812
+ }
813
+ }
814
+ return {
815
+ ...title !== void 0 && title.length > 0 ? { title } : {},
816
+ ...bodyMd !== void 0 ? { bodyMd } : {},
817
+ ...input.excerpt !== void 0 ? { excerpt: input.excerpt } : {},
818
+ ...input.tags !== void 0 && input.tags.length > 0 ? { tags: input.tags } : {},
819
+ ...input.priceAtomic !== void 0 ? { price: input.priceAtomic } : {},
820
+ ...input.handle !== void 0 ? { handle: input.handle } : {},
821
+ status: input.status,
822
+ ...input.resource !== void 0 ? { resource: input.resource } : {}
823
+ };
824
+ }
825
+ var resourceEchoSchema = external_exports.object({
826
+ cacheEligible: external_exports.boolean(),
827
+ cacheEligibleMissing: external_exports.array(external_exports.string())
828
+ }).passthrough();
829
+ var ownPostSchema = external_exports.object({
830
+ id: external_exports.string(),
831
+ slug: external_exports.string(),
832
+ title: external_exports.string(),
833
+ status: external_exports.string(),
834
+ price: external_exports.string(),
835
+ url: external_exports.string(),
836
+ warnings: external_exports.array(external_exports.string()).optional(),
837
+ resource: resourceEchoSchema.optional()
838
+ }).passthrough();
839
+ var MAX_RECOVERIES = 3;
840
+ async function publishPost(input, auth, opts) {
841
+ const body = buildPostCreateBody(input);
842
+ const url = `${trimSlash(opts.baseUrl)}/api/posts`;
843
+ const bodyStr = JSON.stringify(body);
844
+ let recoveries = 0;
845
+ for (; ; ) {
846
+ const authHeaders = await auth.headersFor({ method: "POST", url, body: bodyStr });
847
+ const res = await httpRequest(url, {
848
+ method: "POST",
849
+ timeoutMs: opts.timeoutMs,
850
+ headers: { "x-tenjin-client": CLIENT_HEADER, ...authHeaders },
851
+ jsonBody: body,
852
+ ...opts.fetchImpl !== void 0 ? { fetchImpl: opts.fetchImpl } : {}
853
+ });
854
+ if (!res.ok) throw writeTransportError(url, res);
855
+ if (res.status === 401 && recoveries < MAX_RECOVERIES) {
856
+ const code = auth401Code(res);
857
+ if (await auth.recover(code)) {
858
+ recoveries++;
859
+ continue;
860
+ }
861
+ throw authError(code, res);
862
+ }
863
+ if (res.status === 401) throw authError(auth401Code(res), res);
864
+ if (res.status === 429) throw rateLimitError(url, (n) => res.header(n));
865
+ if (res.status !== 201 && res.status !== 200) throw publishFailed(res);
866
+ const parsed = ownPostSchema.safeParse(res.json);
867
+ if (!parsed.success) {
868
+ throw new CliError("CONTRACT_MISMATCH", "The publish response did not match the contract.", {
869
+ fix: "Update tenjin-cli; the server contract may have changed.",
870
+ details: parsed.error.issues
871
+ });
872
+ }
873
+ const post = parsed.data;
874
+ return {
875
+ resourceId: post.id,
876
+ slug: post.slug,
877
+ title: post.title,
878
+ status: post.status,
879
+ priceAtomic: post.price,
880
+ url: post.url,
881
+ ...post.resource !== void 0 ? { cacheEligible: post.resource.cacheEligible } : {},
882
+ cacheEligibleMissing: post.resource?.cacheEligibleMissing ?? [],
883
+ warnings: post.warnings ?? []
884
+ };
885
+ }
886
+ }
887
+ function auth401Code(res) {
888
+ const header = res.header("www-authenticate");
889
+ const m = header !== void 0 ? /error="([^"]+)"/.exec(header) : null;
890
+ if (m?.[1] !== void 0) return m[1];
891
+ return bodyErrorCode(res.json);
892
+ }
893
+ function bodyErrorCode(json) {
894
+ if (typeof json === "object" && json !== null) {
895
+ const err = json.error;
896
+ if (typeof err === "object" && err !== null) {
897
+ const code = err.code;
898
+ if (typeof code === "string") return code;
899
+ }
900
+ }
901
+ return void 0;
902
+ }
903
+ function serverMessage(json) {
904
+ if (typeof json === "object" && json !== null) {
905
+ const err = json.error;
906
+ if (typeof err === "object" && err !== null) {
907
+ const m = err.message;
908
+ if (typeof m === "string") return m;
909
+ }
910
+ }
911
+ return void 0;
912
+ }
913
+ function authError(code, res) {
914
+ return new CliError(
915
+ "PUBLISH_FAILED",
916
+ serverMessage(res.json) ?? `Publish was not authorized (${code ?? "401"}).`,
917
+ {
918
+ fix: code === "session_key_unbound" ? "Delete ~/.tenjin/session.json to re-establish the session key." : "Check the wallet and retry.",
919
+ details: {
920
+ code: code ?? "unauthorized",
921
+ ...res.json !== void 0 ? { server: res.json } : {}
922
+ }
923
+ }
924
+ );
925
+ }
926
+ function publishFailed(res) {
927
+ return new CliError(
928
+ "PUBLISH_FAILED",
929
+ serverMessage(res.json) ?? `Publish failed (${res.status}).`,
930
+ {
931
+ fix: "Review the server error, then re-run `tenjin publish`.",
932
+ details: { status: res.status, ...res.json !== void 0 ? { server: res.json } : {} }
933
+ }
934
+ );
935
+ }
936
+ function writeTransportError(url, result) {
937
+ const code = result.kind === "network" || result.kind === "timeout" ? "NETWORK_ERROR" : "API_UNREACHABLE";
938
+ return new CliError(code, `${url}: ${result.message}`, {
939
+ fix: "Check --base-url and your network, then retry."
940
+ });
941
+ }
942
+
943
+ // src/lib/session-key.ts
944
+ import { createHash, randomBytes, webcrypto } from "crypto";
945
+ import { readFile, stat } from "fs/promises";
946
+ var SESSION_SCOPE = "read+write";
947
+ var SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
948
+ var EXP_SKEW_MS = 6e4;
949
+ var SessionFileSchema = external_exports.object({
950
+ /** Lowercased wallet address this delegation is bound to. */
951
+ address: external_exports.string(),
952
+ /** The constant base64 SIWX `Tenjin-Session-Delegation` header value. */
953
+ delegation: external_exports.string(),
954
+ /** Delegation expiry (ISO 8601); a request is never signed at/after this. */
955
+ exp: external_exports.string(),
956
+ scope: external_exports.string(),
957
+ /** base64url raw 65-byte uncompressed P-256 point (0x04||X||Y). */
958
+ publicKeyRaw: external_exports.string(),
959
+ /** The P-256 private key as a JWK, re-imported to sign each request. */
960
+ privateKeyJwk: external_exports.record(external_exports.string(), external_exports.unknown())
961
+ });
962
+ var subtle = webcrypto.subtle;
963
+ function toBase64(bytes) {
964
+ return Buffer.from(bytes).toString("base64");
965
+ }
966
+ function toBase64Url(bytes) {
967
+ return Buffer.from(bytes).toString("base64url");
968
+ }
969
+ function contentDigest(body) {
970
+ const hash = createHash("sha256").update(body, "utf8").digest();
971
+ return `sha-256=:${hash.toString("base64")}:`;
972
+ }
973
+ function targetUri(url) {
974
+ const u = new URL(url);
975
+ return `${u.protocol}//${u.host}${u.pathname}${u.search}`;
976
+ }
977
+ function signatureParams(input) {
978
+ const covered = input.contentDigest !== void 0 ? '"@method" "@target-uri" "content-digest"' : '"@method" "@target-uri"';
979
+ return `(${covered});created=${input.created};nonce="${input.nonce}";keyid="${input.keyid}";alg="ecdsa-p256-sha256"`;
980
+ }
981
+ function signatureBase(input) {
982
+ const params = signatureParams(input);
983
+ const lines = [
984
+ `"@method": ${input.method.toUpperCase()}`,
985
+ `"@target-uri": ${targetUri(input.url)}`,
986
+ ...input.contentDigest !== void 0 ? [`"content-digest": ${input.contentDigest}`] : [],
987
+ `"@signature-params": ${params}`
988
+ ];
989
+ return lines.join("\n");
990
+ }
991
+ function generateP256KeyPair() {
992
+ return subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [
993
+ "sign",
994
+ "verify"
995
+ ]);
996
+ }
997
+ async function importSigningKey(jwk) {
998
+ return subtle.importKey(
999
+ "jwk",
1000
+ jwk,
1001
+ { name: "ECDSA", namedCurve: "P-256" },
1002
+ false,
1003
+ ["sign"]
1004
+ );
1005
+ }
1006
+ async function signBase(jwk, base) {
1007
+ const key = await importSigningKey(jwk);
1008
+ const sig = await subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, Buffer.from(base, "utf8"));
1009
+ return toBase64(new Uint8Array(sig));
1010
+ }
1011
+ function keyidFor(publicKeyRaw) {
1012
+ return `p256:${publicKeyRaw}`;
1013
+ }
1014
+ function delegationResources(publicKeyRaw, expIso) {
1015
+ return [
1016
+ `urn:tenjin:session:pubkey:p256:${publicKeyRaw}`,
1017
+ `urn:tenjin:session:exp:${expIso}`,
1018
+ `urn:tenjin:session:scope:${SESSION_SCOPE}`
1019
+ ];
1020
+ }
1021
+ async function establishSession(config, deps = {}) {
1022
+ const now = deps.now ?? Date.now;
1023
+ const pair = await generateP256KeyPair();
1024
+ const rawPub = new Uint8Array(await subtle.exportKey("raw", pair.publicKey));
1025
+ const publicKeyRaw = toBase64Url(rawPub);
1026
+ const jwk = await subtle.exportKey("jwk", pair.privateKey);
1027
+ const expIso = new Date(now() + SESSION_TTL_MS).toISOString();
1028
+ const delegation = await buildSiwxHeader(config.signer, {
1029
+ baseUrl: config.baseUrl,
1030
+ chainId: config.chainId,
1031
+ ttlMs: SESSION_TTL_MS,
1032
+ statement: "Delegate a Tenjin session key.",
1033
+ resources: delegationResources(publicKeyRaw, expIso)
1034
+ });
1035
+ const file = {
1036
+ address: config.signer.address.toLowerCase(),
1037
+ delegation,
1038
+ exp: expIso,
1039
+ scope: SESSION_SCOPE,
1040
+ publicKeyRaw,
1041
+ privateKeyJwk: jwk
1042
+ };
1043
+ await saveSessionFile(config.dataDir, file);
1044
+ return file;
1045
+ }
1046
+ async function signWithSession(file, req, deps = {}) {
1047
+ const now = deps.now ?? Date.now;
1048
+ const nonce = deps.nonce ?? (() => randomBytes(16).toString("hex"));
1049
+ const digest = contentDigest(req.body);
1050
+ const created = Math.floor(now() / 1e3);
1051
+ const params = {
1052
+ method: req.method,
1053
+ url: req.url,
1054
+ contentDigest: digest,
1055
+ created,
1056
+ nonce: nonce(),
1057
+ keyid: keyidFor(file.publicKeyRaw)
1058
+ };
1059
+ const base = signatureBase(params);
1060
+ const signature = await signBase(file.privateKeyJwk, base);
1061
+ return {
1062
+ "Tenjin-Session-Delegation": file.delegation,
1063
+ "Signature-Input": `tenjin=${signatureParams(params)}`,
1064
+ Signature: `tenjin=:${signature}:`,
1065
+ "Content-Digest": digest
1066
+ };
1067
+ }
1068
+ async function loadSessionFile(dir) {
1069
+ const path = sessionPath(dir);
1070
+ if (process.platform !== "win32") {
1071
+ try {
1072
+ const mode = (await stat(path)).mode & 63;
1073
+ if (mode !== 0) return null;
1074
+ } catch (err) {
1075
+ if (hasCode(err, "ENOENT")) return null;
1076
+ throw sessionReadError(path, err);
1077
+ }
1078
+ }
1079
+ let raw;
1080
+ try {
1081
+ raw = await readFile(path, "utf8");
1082
+ } catch (err) {
1083
+ if (hasCode(err, "ENOENT")) return null;
1084
+ throw sessionReadError(path, err);
1085
+ }
1086
+ let json;
1087
+ try {
1088
+ json = JSON.parse(raw);
1089
+ } catch {
1090
+ return null;
1091
+ }
1092
+ const parsed = SessionFileSchema.safeParse(json);
1093
+ return parsed.success ? parsed.data : null;
1094
+ }
1095
+ function sessionReadError(path, cause) {
1096
+ return new CliError("INTERNAL", `Could not read the session cache at ${path}`, {
1097
+ fix: `Check file permissions on ${path}, or delete it to re-establish.`,
1098
+ cause
1099
+ });
1100
+ }
1101
+ async function saveSessionFile(dir, file) {
1102
+ await writeFileAtomic(sessionPath(dir), `${JSON.stringify(file, null, 2)}
1103
+ `, {
1104
+ mode: 384,
1105
+ dirMode: 448
1106
+ });
1107
+ }
1108
+ function isSessionUsable(file, address, now) {
1109
+ if (file.address !== address.toLowerCase()) return false;
1110
+ if (file.scope !== SESSION_SCOPE) return false;
1111
+ const expMs = Date.parse(file.exp);
1112
+ if (!Number.isFinite(expMs)) return false;
1113
+ return now < expMs - EXP_SKEW_MS;
1114
+ }
1115
+ function recoveryFor(code) {
1116
+ switch (code) {
1117
+ case "proof_expired":
1118
+ return "resign";
1119
+ case "session_expired":
1120
+ case "proof_revoked":
1121
+ case "insufficient_scope":
1122
+ return "reestablish";
1123
+ case "session_key_unbound":
1124
+ return "fatal";
1125
+ default:
1126
+ return "fatal";
1127
+ }
1128
+ }
1129
+ function createSessionKeyAuth(config, deps = {}) {
1130
+ const now = deps.now ?? Date.now;
1131
+ let cached = null;
1132
+ let forceReestablish = false;
1133
+ const ensure = async () => {
1134
+ if (!forceReestablish && cached !== null && isSessionUsable(cached, config.signer.address, now())) {
1135
+ return cached;
1136
+ }
1137
+ if (!forceReestablish) {
1138
+ const onDisk = await loadSessionFile(config.dataDir);
1139
+ if (onDisk !== null && isSessionUsable(onDisk, config.signer.address, now())) {
1140
+ cached = onDisk;
1141
+ return cached;
1142
+ }
1143
+ }
1144
+ cached = await establishSession(config, deps);
1145
+ forceReestablish = false;
1146
+ return cached;
1147
+ };
1148
+ return {
1149
+ async headersFor(req) {
1150
+ const file = await ensure();
1151
+ return signWithSession(file, req, deps);
1152
+ },
1153
+ async recover(code) {
1154
+ const recovery = recoveryFor(code);
1155
+ if (recovery === "fatal") return false;
1156
+ if (recovery === "reestablish") {
1157
+ cached = null;
1158
+ forceReestablish = true;
1159
+ }
1160
+ return true;
1161
+ }
1162
+ };
1163
+ }
1164
+ function createSiwxAuth(config) {
1165
+ return {
1166
+ async headersFor() {
1167
+ const header = await buildSiwxHeader(config.signer, {
1168
+ baseUrl: config.baseUrl,
1169
+ chainId: config.chainId
1170
+ });
1171
+ return { "SIGN-IN-WITH-X": header };
1172
+ },
1173
+ async recover(code) {
1174
+ return code === "nonce_already_used" || code === "invalid_proof" || code === "proof_expired";
1175
+ }
1176
+ };
1177
+ }
1178
+
1179
+ // src/commands/publish.ts
1180
+ var WRITE_CHAIN_ID = "eip155:8453";
1181
+ async function runPublish(args, ctx, deps = {}) {
1182
+ const env = deps.env ?? process.env;
1183
+ const cwd = deps.cwd ?? process.cwd();
1184
+ if (args.mode !== void 0) parsePublishModeFlag(args.mode, "--mode");
1185
+ const { raw, candidate } = await resolveSource(args, ctx.dataDir);
1186
+ const { frontmatter, body } = parseFrontmatter(raw);
1187
+ const status = resolveStatus(args, frontmatter);
1188
+ const title = resolveTitle(frontmatter, body);
1189
+ const tags = resolveTags(frontmatter);
1190
+ const excerpt = expectString(frontmatter, "excerpt");
1191
+ const handle = expectString(frontmatter, "handle");
1192
+ const cardFlags = cardFlagsFrom(args);
1193
+ if (candidate?.meta.question !== void 0 && cardFlags.question === void 0 && frontmatter.questionsAnswered === void 0) {
1194
+ cardFlags.question = [candidate.meta.question];
1195
+ }
1196
+ const card = deriveCard(frontmatter, cardFlags);
1197
+ const settings = await resolvePublishSettings({
1198
+ dataDir: ctx.dataDir,
1199
+ cwd,
1200
+ ...args.mode !== void 0 ? { flag: args.mode } : {},
1201
+ env
1202
+ });
1203
+ for (const warning of settings.warnings) {
1204
+ ctx.io.stderr.write(`${warning}
1205
+ `);
1206
+ }
1207
+ const envMode = env.TENJIN_PUBLISH_MODE;
1208
+ if (envMode !== void 0 && envMode.length > 0 && !PublishModeSchema.safeParse(envMode).success) {
1209
+ ctx.io.stderr.write(
1210
+ `Ignoring invalid TENJIN_PUBLISH_MODE=${JSON.stringify(envMode)}; using ${settings.mode} (${settings.modeSource}).
1211
+ `
1212
+ );
1213
+ }
1214
+ if (settings.modeSource === "default") {
1215
+ const usd = formatUsdDisplay(settings.defaultPriceAtomic);
1216
+ ctx.io.stderr.write(
1217
+ `publish.mode: ${settings.mode} (default) - a clean scan publishes at $${usd} without asking. Change: tenjin config set publish.mode review.
1218
+ `
1219
+ );
1220
+ }
1221
+ const priceAtomic = resolvePrice(args, frontmatter, settings.defaultPriceAtomic);
1222
+ const findings = dedupeFindings([...scan(raw), ...scan(cardScanText(card))]);
1223
+ const blocking = findings.filter((f) => f.severity === "block");
1224
+ const warns = findings.filter((f) => f.severity === "warn");
1225
+ const eligibility = localCardEligibility(card);
1226
+ const price = toMoney(priceAtomic);
1227
+ if (blocking.length > 0) {
1228
+ throw new CliError("PUBLISH_BLOCKED", blockMessage(blocking), {
1229
+ fix: "Remove the secret from the file (it is never masked away by --yes), then re-run.",
1230
+ details: {
1231
+ mode: settings.mode,
1232
+ findings: blocking.map(publicFinding),
1233
+ price: { atomic: price.atomic, usd: price.usd }
1234
+ }
1235
+ });
1236
+ }
1237
+ const needsConfirm = settings.mode === "review" || settings.mode === "auto" && warns.length > 0;
1238
+ if (needsConfirm && args.yes !== true) {
1239
+ throw new CliError("NEEDS_CONFIRMATION", confirmMessage(warns.length, price.usd), {
1240
+ fix: "Review the findings, then re-run with --yes (or resolve the source and re-run).",
1241
+ details: {
1242
+ mode: settings.mode,
1243
+ price: { atomic: price.atomic, usd: price.usd },
1244
+ findings: warns.map(publicFinding),
1245
+ card: eligibility,
1246
+ target: { status, titlePreview: sanitizeForTerminal(title ?? "(untitled draft)") }
1247
+ }
1248
+ });
1249
+ }
1250
+ const runtime = await resolveContextSettings(ctx);
1251
+ const provider = resolveWalletProvider(
1252
+ ctx,
1253
+ deps.provider !== void 0 ? { provider: deps.provider } : {}
1254
+ );
1255
+ await describeWallet(provider);
1256
+ const signer = await provider.getSigner();
1257
+ const auth = resolveWriteAuth(signer, runtime.baseUrl, ctx.dataDir, deps, env);
1258
+ const input = {
1259
+ ...title !== void 0 ? { title } : {},
1260
+ bodyMd: body,
1261
+ ...excerpt !== void 0 ? { excerpt } : {},
1262
+ ...tags !== void 0 ? { tags } : {},
1263
+ priceAtomic,
1264
+ ...handle !== void 0 ? { handle } : {},
1265
+ status,
1266
+ ...card !== void 0 ? { resource: card } : {}
1267
+ };
1268
+ const result = await publishPost(input, auth, {
1269
+ baseUrl: runtime.baseUrl,
1270
+ timeoutMs: ctx.flags.timeout,
1271
+ ...deps.fetchImpl !== void 0 ? { fetchImpl: deps.fetchImpl } : {}
1272
+ });
1273
+ const candidateInfo = candidate !== void 0 ? await clearPublishedCandidate(ctx, candidate.id) : void 0;
1274
+ return receipt(result, runtime.baseUrl, candidateInfo);
1275
+ }
1276
+ async function clearPublishedCandidate(ctx, id) {
1277
+ try {
1278
+ if (await dropCandidate(ctx.dataDir, id)) return { id, cleared: true };
1279
+ const warning = `Published, but candidate ${id} was already gone; nothing to clear.`;
1280
+ ctx.io.stderr.write(`${warning}
1281
+ `);
1282
+ return { id, cleared: false, warning };
1283
+ } catch (err) {
1284
+ const warning = `Published successfully, but could not clear candidate ${id}: ${errorMessage(err)}. Remove it with \`tenjin candidate drop ${id}\`.`;
1285
+ ctx.io.stderr.write(`${warning}
1286
+ `);
1287
+ return { id, cleared: false, warning };
1288
+ }
1289
+ }
1290
+ function errorMessage(err) {
1291
+ return err instanceof Error ? err.message : String(err);
1292
+ }
1293
+ async function resolveSource(args, dataDir) {
1294
+ if (args.candidate !== void 0) {
1295
+ if (args.file !== void 0) {
1296
+ throw new CliError("USAGE", "Pass EITHER a file or --candidate, not both.", {
1297
+ fix: "Publish a file with `tenjin publish post.md`, or a candidate with `--candidate <id>`."
1298
+ });
1299
+ }
1300
+ if (!UUID_RE.test(args.candidate)) {
1301
+ throw new CliError("USAGE", `Invalid candidate id: ${JSON.stringify(args.candidate)}`, {
1302
+ fix: "Pass a candidate id from `tenjin candidate list`."
1303
+ });
1304
+ }
1305
+ const record = await readCandidate(dataDir, args.candidate);
1306
+ if (record === null) {
1307
+ throw new CliError("USAGE", `Unknown candidate: ${JSON.stringify(args.candidate)}`, {
1308
+ fix: "List parked candidates with `tenjin candidate list`."
1309
+ });
1310
+ }
1311
+ return { raw: await readDraft(record), candidate: record };
1312
+ }
1313
+ if (args.file === void 0) {
1314
+ throw new CliError("USAGE", "Nothing to publish.", {
1315
+ fix: "Pass a Markdown file (`tenjin publish post.md`) or `--candidate <id>`."
1316
+ });
1317
+ }
1318
+ return { raw: await readMarkdown(args.file) };
1319
+ }
1320
+ async function readDraft(record) {
1321
+ try {
1322
+ return await readFile2(record.draftPath, "utf8");
1323
+ } catch (err) {
1324
+ throw new CliError("INTERNAL", `Candidate ${record.id} is missing its draft.`, {
1325
+ fix: `Drop it with \`tenjin candidate drop ${record.id}\` and re-add the draft.`,
1326
+ cause: err
1327
+ });
1328
+ }
1329
+ }
1330
+ function receipt(result, baseUrl, candidateInfo) {
1331
+ const price = toMoney(result.priceAtomic);
1332
+ const missing = missingSentences(result.cacheEligibleMissing);
1333
+ const cacheEligible = result.cacheEligible ?? false;
1334
+ const deskUrl = `${trimSlash(baseUrl)}/desk`;
1335
+ const title = sanitizeForTerminal(result.title);
1336
+ const human = [
1337
+ `Published ${title} (${result.status}) for ${price.usd} USD \u2192 ${result.url}`,
1338
+ cacheEligible ? "Answer card is lookup-eligible." : missing.length > 0 ? `Answer card not lookup-eligible yet: ${missing.join(" ")}` : "Published as a browse-only document (no answer card).",
1339
+ ...candidateInfo?.cleared === true ? [`Cleared candidate ${candidateInfo.id}.`] : [],
1340
+ ...result.warnings.map((w) => `warning: ${sanitizeForTerminal(w)}`)
1341
+ ];
1342
+ return {
1343
+ data: {
1344
+ resourceId: result.resourceId,
1345
+ url: result.url,
1346
+ status: result.status,
1347
+ price,
1348
+ cacheEligible,
1349
+ missing,
1350
+ deskUrl,
1351
+ ...candidateInfo !== void 0 ? { candidate: candidateInfo } : {},
1352
+ ...result.warnings.length > 0 ? { warnings: result.warnings } : {}
1353
+ },
1354
+ humanLines: human
1355
+ };
1356
+ }
1357
+ async function readMarkdown(file) {
1358
+ try {
1359
+ return await readFile2(file, "utf8");
1360
+ } catch (err) {
1361
+ throw new CliError("USAGE", `Could not read ${JSON.stringify(file)}`, {
1362
+ fix: "Pass a path to a readable Markdown file, e.g. `tenjin publish post.md`.",
1363
+ cause: err
1364
+ });
1365
+ }
1366
+ }
1367
+ function resolveStatus(args, frontmatter) {
1368
+ if (args.draft === true) return "draft";
1369
+ const fm = frontmatter.status;
1370
+ if (fm === void 0) return "published";
1371
+ if (typeof fm !== "string" || !PUBLISH_STATUSES.includes(fm)) {
1372
+ throw new CliError("USAGE", `Invalid status ${JSON.stringify(fm)} in frontmatter.`, {
1373
+ fix: "Use status: draft | published | unlisted, or pass --draft."
1374
+ });
1375
+ }
1376
+ return fm;
1377
+ }
1378
+ function resolveTitle(frontmatter, body) {
1379
+ const fm = frontmatter.title;
1380
+ if (fm !== void 0) {
1381
+ if (typeof fm !== "string") {
1382
+ throw new CliError("USAGE", "frontmatter title must be a single string.");
1383
+ }
1384
+ return fm.trim();
1385
+ }
1386
+ const headings = headingOutline(body);
1387
+ const h1 = headings.find((h) => h.level === 1) ?? headings[0];
1388
+ return h1?.text;
1389
+ }
1390
+ function resolveTags(frontmatter) {
1391
+ const fm = frontmatter.tags;
1392
+ if (fm === void 0) return void 0;
1393
+ if (typeof fm === "string") return [fm];
1394
+ if (Array.isArray(fm)) return fm;
1395
+ throw new CliError("USAGE", "frontmatter tags must be a list of strings.");
1396
+ }
1397
+ function expectString(frontmatter, key) {
1398
+ const fm = frontmatter[key];
1399
+ if (fm === void 0) return void 0;
1400
+ if (typeof fm !== "string") {
1401
+ throw new CliError("USAGE", `frontmatter ${key} must be a single string.`);
1402
+ }
1403
+ return fm;
1404
+ }
1405
+ function resolvePrice(args, frontmatter, defaultAtomic) {
1406
+ if (args.price !== void 0) return parseUsdToAtomic(args.price);
1407
+ const fm = frontmatter.price;
1408
+ if (fm !== void 0) {
1409
+ if (typeof fm !== "string") {
1410
+ throw new CliError("USAGE", 'frontmatter price must be a decimal-USD string, e.g. "0.10".');
1411
+ }
1412
+ return parseUsdToAtomic(fm);
1413
+ }
1414
+ return defaultAtomic;
1415
+ }
1416
+ function cardFlagsFrom(args) {
1417
+ return {
1418
+ ...args.question !== void 0 && args.question.length > 0 ? { question: args.question } : {},
1419
+ ...args.task !== void 0 && args.task.length > 0 ? { task: args.task } : {},
1420
+ ...args.scope !== void 0 ? { scope: args.scope } : {},
1421
+ ...args.exclusions !== void 0 ? { exclusions: args.exclusions } : {},
1422
+ ...args.asOf !== void 0 ? { asOf: args.asOf } : {},
1423
+ ...args.validUntil !== void 0 ? { validUntil: args.validUntil } : {},
1424
+ ...args.artifactType !== void 0 ? { artifactType: args.artifactType } : {},
1425
+ ...args.temporalMode !== void 0 ? { temporalMode: args.temporalMode } : {},
1426
+ ...args.provenance !== void 0 ? { provenance: args.provenance } : {},
1427
+ ...args.methodology !== void 0 ? { methodology: args.methodology } : {},
1428
+ ...args.appliesTo !== void 0 && args.appliesTo.length > 0 ? { appliesTo: parseAppliesToFlags(args.appliesTo) } : {}
1429
+ };
1430
+ }
1431
+ function resolveWriteAuth(signer, baseUrl, dataDir, deps, env) {
1432
+ const config = { signer, baseUrl, chainId: WRITE_CHAIN_ID, dataDir };
1433
+ const useSession = deps.useSession ?? env.TENJIN_NO_SESSION !== "1";
1434
+ return useSession ? createSessionKeyAuth(config) : createSiwxAuth(config);
1435
+ }
1436
+ function cardScanText(card) {
1437
+ if (card === void 0) return "";
1438
+ const parts = [];
1439
+ const add = (v) => {
1440
+ if (v !== void 0) parts.push(v);
1441
+ };
1442
+ add(card.scope);
1443
+ add(card.exclusions);
1444
+ add(card.provenanceSummary);
1445
+ add(card.methodologySummary);
1446
+ add(card.mediaType);
1447
+ add(card.maintenanceCadence);
1448
+ add(card.asOf);
1449
+ add(card.validUntil);
1450
+ add(card.estimatedPaidInputCost);
1451
+ if (card.questionsAnswered !== void 0) parts.push(...card.questionsAnswered);
1452
+ if (card.tasksSupported !== void 0) parts.push(...card.tasksSupported);
1453
+ if (card.appliesTo !== void 0) {
1454
+ for (const values of Object.values(card.appliesTo)) parts.push(...values);
1455
+ }
1456
+ return parts.join("\n");
1457
+ }
1458
+ function dedupeFindings(findings) {
1459
+ const seen = /* @__PURE__ */ new Set();
1460
+ return findings.filter((f) => {
1461
+ const key = `${f.check}:${f.excerpt}`;
1462
+ if (seen.has(key)) return false;
1463
+ seen.add(key);
1464
+ return true;
1465
+ });
1466
+ }
1467
+ function publicFinding(f) {
1468
+ return { check: f.check, severity: f.severity, line: f.line, excerpt: f.excerpt };
1469
+ }
1470
+ function blockMessage(blocking) {
1471
+ const checks = [...new Set(blocking.map((f) => f.check))].join(", ");
1472
+ return `Publish blocked: the file contains ${blocking.length} secret finding(s) (${checks}).`;
1473
+ }
1474
+ function confirmMessage(warnCount, priceUsd) {
1475
+ return warnCount > 0 ? `Publish needs confirmation: ${warnCount} finding(s), price $${priceUsd}.` : `Publish needs confirmation: price $${priceUsd}.`;
1476
+ }
1477
+ export {
1478
+ runPublish
1479
+ };
1480
+ //# sourceMappingURL=publish-D76B3MBK.js.map