@tokagent/tokagentos 2.0.17 → 2.0.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokagent/tokagentos",
3
- "version": "2.0.17",
3
+ "version": "2.0.19",
4
4
  "description": "tokagentOS CLI - Create and upgrade tokagentOS project templates",
5
5
  "type": "module",
6
6
  "bin": {
@@ -77,6 +77,50 @@ function configureLitellmEnvMirror(): void {
77
77
  }
78
78
  configureLitellmEnvMirror();
79
79
 
80
+ /**
81
+ * Tokagent billing gateway → OpenAI-compatible chat provider.
82
+ *
83
+ * When the user has set `BILLING_CHAT_KEY=sk-ai-...` in their .env (the
84
+ * minted billing API key) AND `TOKAGENT_GATEWAY_URL` is set (always true on
85
+ * v2.0.7+ scaffolds — defaults to the Tokamak-hosted Railway gateway), wire
86
+ * them onto OPENAI_* so plugin-openai treats the gateway as its provider.
87
+ *
88
+ * Effect: chat composer activates without the user needing to set
89
+ * OPENAI_API_KEY/OPENAI_BASE_URL manually. The minted sk-ai-* key is sent
90
+ * as `Authorization: Bearer sk-ai-...`, which the billing-server's
91
+ * resolveBillingIdentity recognizes (v2.0.18+) and routes through the
92
+ * billing rail (auth → reserve → upstream LiteLLM → commit).
93
+ *
94
+ * Coupled like the LiteLLM mirror — if only one of the two is set, this is
95
+ * a no-op (we don't want to point OpenAI plugin at the gateway with no key,
96
+ * or send the billing key to api.openai.com).
97
+ *
98
+ * Explicit OPENAI_* in .env still wins over BILLING_* — same precedence
99
+ * model as the LiteLLM block above.
100
+ */
101
+ function configureBillingChatMirror(): void {
102
+ const hasKey = !!process.env.BILLING_CHAT_KEY?.trim();
103
+ const hasUrl = !!process.env.TOKAGENT_GATEWAY_URL?.trim();
104
+ if (!hasKey || !hasUrl) return;
105
+ if (process.env.OPENAI_API_KEY?.trim() || process.env.OPENAI_BASE_URL?.trim()) {
106
+ // User configured OpenAI explicitly — respect their choice.
107
+ return;
108
+ }
109
+ // The billing gateway exposes /v1/chat/completions + /v1/messages at the
110
+ // root of TOKAGENT_GATEWAY_URL. plugin-openai appends `/v1/...` itself, so
111
+ // strip any trailing /v1 the user may have appended and a trailing slash.
112
+ const base = process.env
113
+ .TOKAGENT_GATEWAY_URL!.trim()
114
+ .replace(/\/$/, "")
115
+ .replace(/\/v1$/, "");
116
+ process.env.OPENAI_API_KEY = process.env.BILLING_CHAT_KEY!.trim();
117
+ process.env.OPENAI_BASE_URL = `${base}/v1`;
118
+ console.info(
119
+ "[tokagent] BILLING_CHAT_KEY + TOKAGENT_GATEWAY_URL detected — wired as OpenAI-compatible provider for chat.",
120
+ );
121
+ }
122
+ configureBillingChatMirror();
123
+
80
124
  /**
81
125
  * Plugins that depend on PTY/native workspace tooling.
82
126
  * Keep them out of cloud images where those binaries are intentionally absent.
@@ -163,6 +163,40 @@ export async function revokeApiKey(
163
163
  .where(eq(apiKeys.id, id));
164
164
  }
165
165
 
166
+ /**
167
+ * Hard-delete an API key row from the database (vs. {@link revokeApiKey},
168
+ * which sets `revokedAt` and keeps the row for audit trail).
169
+ *
170
+ * Use this when the operator wants to reclaim disk space or when revoked
171
+ * keys would otherwise accumulate unbounded over the lifetime of a wallet.
172
+ * The historical `billing_call_log` rows still reference `apiKeyId` as a
173
+ * plain text column (no FK constraint), so call-log history survives.
174
+ *
175
+ * Authorization: the caller MUST already have proven ownership — same
176
+ * checks as {@link revokeApiKey}.
177
+ *
178
+ * Idempotent: if the key doesn't exist, returns silently (no throw).
179
+ */
180
+ export async function deleteApiKey(
181
+ db: BillingDatabase,
182
+ id: string,
183
+ wallet: Address,
184
+ ): Promise<void> {
185
+ const rows = await db
186
+ .select({ id: apiKeys.id, wallet: apiKeys.wallet })
187
+ .from(apiKeys)
188
+ .where(eq(apiKeys.id, id));
189
+
190
+ if (rows.length === 0) return; // already gone — idempotent
191
+
192
+ const row = rows[0]!;
193
+ if (row.wallet.toLowerCase() !== wallet.toLowerCase()) {
194
+ throw new Error(`deleteApiKey: key ${id} does not belong to wallet ${wallet}`);
195
+ }
196
+
197
+ await db.delete(apiKeys).where(eq(apiKeys.id, id));
198
+ }
199
+
166
200
  /**
167
201
  * Batch-update `last_used_at` for a set of key IDs.
168
202
  * Called by the Phase 5 cron worker, not on the hot request path.
@@ -202,6 +202,14 @@ BILLING_MODE=client
202
202
  TOKAGENT_GATEWAY_URL=https://billing-service-production-a8e7.up.railway.app
203
203
  # TOKAGENT_GATEWAY_TIMEOUT_MS=30000 # default 30s; bump for slow connections
204
204
 
205
+ # Paste your minted API key here (from the billing dashboard "Keys" tab) to
206
+ # enable the agent's chat composer. With BILLING_CHAT_KEY + the gateway URL
207
+ # above, the runtime auto-wires OPENAI_API_KEY / OPENAI_BASE_URL so
208
+ # plugin-openai routes inference through the billing rail — every billed
209
+ # call debits PTON from your on-chain balance.
210
+ # Mint a key at: <TOKAGENT_GATEWAY_URL>/v1/billing/dashboard/ (Keys tab)
211
+ # BILLING_CHAT_KEY=sk-ai-...
212
+
205
213
  # ─────────────────────────────────────────────────────────────────────
206
214
  # ADVANCED — self-host the billing rail
207
215
  # ─────────────────────────────────────────────────────────────────────
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokagent/plugin-tokagent-billing",
3
- "version": "2.0.10",
3
+ "version": "2.0.11",
4
4
  "description": "elizaOS plugin: Web3 credit-billing routes and middleware for the tokagentos LLM gateway.",
5
5
  "type": "module",
6
6
  "publishConfig": { "access": "public" },
@@ -857,8 +857,9 @@ async function renderKeysTable() {
857
857
  const tr = document.createElement("tr");
858
858
  const status = k.revokedAt ? `<span class="outcome-pill outcome-failed">revoked</span>` : `<span class="outcome-pill outcome-success">active</span>`;
859
859
  const action = k.revokedAt
860
- ? ""
861
- : `<button class="btn btn-danger" type="button" data-revoke="${escape(k.id)}">Revoke</button>`;
860
+ ? `<button class="btn btn-danger" type="button" data-delete="${escape(k.id)}" title="Permanently remove this revoked key from the database">Delete</button>`
861
+ : `<button class="btn btn-danger" type="button" data-revoke="${escape(k.id)}">Revoke</button>
862
+ <button class="btn btn-ghost" type="button" data-delete="${escape(k.id)}" title="Skip the revoke step and hard-delete immediately">Delete</button>`;
862
863
  tr.innerHTML = `
863
864
  <td><code>${escape(k.id)}</code></td>
864
865
  <td>${escape(k.name ?? "—")}</td>
@@ -883,6 +884,25 @@ async function renderKeysTable() {
883
884
  }
884
885
  });
885
886
  }
887
+ // Hard-delete (?hard=true) removes the row from the DB instead of soft-
888
+ // revoking. Soft revoke keeps the row for audit trail but accumulates
889
+ // unbounded over time; hard delete reclaims the row. Call-log history
890
+ // survives because billing_call_log.api_key_id has no FK constraint.
891
+ for (const btn of tbody.querySelectorAll("[data-delete]")) {
892
+ btn.addEventListener("click", async () => {
893
+ const id = btn.getAttribute("data-delete");
894
+ if (!window.confirm(`Permanently delete API key ${id}?\nThis cannot be undone.`)) return;
895
+ btn.disabled = true;
896
+ try {
897
+ await apiJson(`/v1/keys/${encodeURIComponent(id)}?hard=true`, { method: "DELETE" });
898
+ await renderKeysTable();
899
+ } catch (e) {
900
+ setStatus($("#key-create-status"), `Delete failed: ${e.message}`, "err");
901
+ } finally {
902
+ btn.disabled = false;
903
+ }
904
+ });
905
+ }
886
906
  }
887
907
 
888
908
  // ----------------------------- Charts (zero-dep canvas) -----------------------------
@@ -81,9 +81,22 @@ export async function resolveBillingIdentity(
81
81
  }
82
82
  }
83
83
 
84
- // ---- 2. Authorization: Bearer <jwt> ----
84
+ // ---- 2. Authorization: Bearer (API key OR SIWE JWT) ----
85
+ // Anthropic and OpenAI SDKs (plus elizaOS's plugin-openai) send the API key
86
+ // as `Authorization: Bearer <key>`. Try as sk-ai-* API key first so a user
87
+ // can plug the billing gateway into any OpenAI-compatible client by setting
88
+ // OPENAI_API_KEY=sk-ai-... + OPENAI_BASE_URL=<gateway>. Falls back to SIWE
89
+ // JWT for the dashboard's wallet-signed session path.
85
90
  const bearer = bearerToken(req);
86
91
  if (bearer) {
92
+ if (bearer.startsWith("sk-ai-")) {
93
+ const result = await resolveApiKey(db, bearer, authSecret);
94
+ if (result) {
95
+ return { wallet: result.wallet, apiKeyId: result.id };
96
+ }
97
+ // Fall through to JWT path — defensive, in case some future API key
98
+ // format collides with the prefix check.
99
+ }
87
100
  const result = await verifySession(bearer, authSecret);
88
101
  if (result) {
89
102
  return { wallet: result.wallet };
@@ -17,6 +17,7 @@ import {
17
17
  mintApiKey,
18
18
  listApiKeys,
19
19
  revokeApiKey,
20
+ deleteApiKey,
20
21
  } from "@tokagentos/billing";
21
22
  import {
22
23
  getBillingState,
@@ -153,9 +154,24 @@ async function handleRevokeKey(
153
154
  return;
154
155
  }
155
156
 
157
+ // `?hard=true` hard-deletes the row instead of soft-revoking. Soft revoke
158
+ // sets `revoked_at` and leaves the row in `billing_api_keys` for audit
159
+ // trail — useful but accumulates rows over time. Hard delete reclaims the
160
+ // row entirely; the historical `billing_call_log` rows still reference
161
+ // `api_key_id` as a plain text column (no FK), so call-log history is not
162
+ // affected. The default remains soft-revoke for backward compatibility.
163
+ const hardFlag = req.query?.["hard"];
164
+ const hardDelete =
165
+ hardFlag === "true" || hardFlag === "1" || hardFlag === "";
166
+
156
167
  try {
157
- await revokeApiKey(db, keyId, identity.wallet);
158
- res.status(200).json({ revoked: true, id: keyId });
168
+ if (hardDelete) {
169
+ await deleteApiKey(db, keyId, identity.wallet);
170
+ res.status(200).json({ deleted: true, id: keyId });
171
+ } else {
172
+ await revokeApiKey(db, keyId, identity.wallet);
173
+ res.status(200).json({ revoked: true, id: keyId });
174
+ }
159
175
  } catch (err: unknown) {
160
176
  const message = err instanceof Error ? err.message : "revoke failed";
161
177
  if (message.includes("not found")) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "1.0.0",
3
- "generatedAt": "2026-05-19T18:38:05.516Z",
3
+ "generatedAt": "2026-05-19T20:00:04.880Z",
4
4
  "repoUrl": "https://github.com/elizaos/eliza",
5
5
  "templates": [
6
6
  {