@bnbagent/deploy-provider-bnb 0.3.0 → 0.4.0

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": "@bnbagent/deploy-provider-bnb",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "BNB Chain trial-platform provider for @bnbagent/deploy — deploys agents to the managed free-trial runtime (bnbagent-api) over plain HTTPS: GitHub device-flow login, zip artifact upload, deploy + poll. No cloud credentials of your own.",
6
6
  "license": "Apache-2.0",
@@ -17,10 +17,10 @@
17
17
  "typecheck": "tsc --noEmit"
18
18
  },
19
19
  "peerDependencies": {
20
- "@bnbagent/deploy-core": "*"
20
+ "@bnbagent/deploy-core": "^0.4.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@bnbagent/deploy-core": "^0.3.0",
23
+ "@bnbagent/deploy-core": "^0.4.0",
24
24
  "@types/bun": "^1.3.0",
25
25
  "typescript": "^5.7.0"
26
26
  }
package/src/client.ts CHANGED
@@ -16,7 +16,21 @@ import { loadSession, saveSession, withSessionLock, type Session } from "./sessi
16
16
  */
17
17
  export function apiUrl(ctx: Context): string {
18
18
  const fromSpec = ctx.overrides?.bnb?.apiUrl;
19
- const url = (typeof fromSpec === "string" && fromSpec) || process.env.BNBAGENT_API_URL;
19
+ const fromEnv = process.env.BNBAGENT_API_URL?.trim();
20
+ const hasApiToken = Boolean(process.env.BNBAGENT_API_TOKEN?.trim());
21
+ // An API token is normally injected by CI while the TOML comes from the
22
+ // checked-out repository. Bind that credential to the trusted environment
23
+ // endpoint so repository config cannot redirect it to another HTTPS origin.
24
+ if (hasApiToken && !fromEnv) {
25
+ throw new BnbApiError(
26
+ 0,
27
+ "config.token_api_url_missing",
28
+ "BNBAGENT_API_TOKEN requires BNBAGENT_API_URL so the CI credential is bound to a trusted origin.",
29
+ );
30
+ }
31
+ const url = hasApiToken
32
+ ? fromEnv
33
+ : (typeof fromSpec === "string" && fromSpec.trim()) || fromEnv;
20
34
  if (!url) {
21
35
  throw new BnbApiError(
22
36
  0,
@@ -40,7 +54,7 @@ export function apiUrl(ctx: Context): string {
40
54
 
41
55
  /**
42
56
  * Automation/CI bearer: a platform API token (`bnbk_…`, minted with
43
- * `bnbagent-deploy token --new`) via BNBAGENT_API_TOKEN. When set it REPLACES
57
+ * `bnbagent-deploy token --provider bnb --new`) via BNBAGENT_API_TOKEN. When set it REPLACES
44
58
  * the session entirely — no device flow, no refresh, no session file.
45
59
  */
46
60
  export function envApiToken(): string | undefined {
@@ -116,7 +130,7 @@ export async function authedRequest(
116
130
  throw new BnbApiError(
117
131
  401,
118
132
  "auth.token_invalid",
119
- "BNBAGENT_API_TOKEN was rejected (revoked/expired?) — mint a new one: `bnbagent-deploy token --new` (while signed in).",
133
+ "BNBAGENT_API_TOKEN was rejected (revoked/expired?) — mint a new one: `bnbagent-deploy token --provider bnb --new` (while signed in).",
120
134
  );
121
135
  }
122
136
  throw e;
@@ -127,7 +141,7 @@ export async function authedRequest(
127
141
  throw new BnbApiError(
128
142
  401,
129
143
  "auth.signed_out",
130
- "Not signed in — run `bnbagent-deploy login` first (CI: set BNBAGENT_API_TOKEN).",
144
+ "Not signed in — run `bnbagent-deploy login --provider bnb` first (CI: set BNBAGENT_API_TOKEN).",
131
145
  );
132
146
  }
133
147
  // Host-bind the token: never send a session minted against platform A to a
@@ -138,7 +152,7 @@ export async function authedRequest(
138
152
  401,
139
153
  "auth.host_mismatch",
140
154
  `Your session was minted against ${session.apiUrl}, but this command targets ${base}. ` +
141
- `Refusing to send credentials cross-host — run \`bnbagent-deploy login\` against ${base}.`,
155
+ `Refusing to send credentials cross-host — run \`bnbagent-deploy login --provider bnb\` against ${base}.`,
142
156
  );
143
157
  }
144
158
  try {
@@ -155,7 +169,7 @@ async function refreshSession(base: string, session: Session): Promise<Session>
155
169
  return withSessionLock(async () => {
156
170
  const fresh = await loadSession();
157
171
  if (!fresh || !sameOrigin(fresh.apiUrl, base)) {
158
- throw new BnbApiError(401, "auth.expired", "Session expired — run `bnbagent-deploy login` again.");
172
+ throw new BnbApiError(401, "auth.expired", "Session expired — run `bnbagent-deploy login --provider bnb` again.");
159
173
  }
160
174
  if (fresh.refresh_token !== session.refresh_token) {
161
175
  return fresh;
@@ -170,7 +184,7 @@ async function refreshSession(base: string, session: Session): Promise<Session>
170
184
  throw new BnbApiError(
171
185
  401,
172
186
  "auth.expired",
173
- `Session expired${detail} — run \`bnbagent-deploy login\` again.`,
187
+ `Session expired${detail} — run \`bnbagent-deploy login --provider bnb\` again.`,
174
188
  );
175
189
  }
176
190
  const next: Session = { apiUrl: base, access_token: res.access_token, refresh_token: res.refresh_token };
package/src/extras.ts CHANGED
@@ -27,7 +27,7 @@ const emit = (data: unknown): void => {
27
27
  };
28
28
 
29
29
  /**
30
- * `bnbagent-deploy trial [--json]` — the structured trial-clock read
30
+ * `bnbagent-deploy trial --provider bnb [--json]` — the structured trial-clock read
31
31
  * (GET /v1/account/trial: case/started_at/expires_at/remaining_seconds).
32
32
  * Account-level: works in any directory. Orchestrators gate deploy offers on it.
33
33
  */
@@ -104,7 +104,7 @@ export async function clientsCmd(spec: DeploymentSpec | undefined, ctx: Context)
104
104
  }
105
105
 
106
106
  /**
107
- * `bnbagent-deploy token [--new | --revoke <id>] [--json]` (default: list).
107
+ * `bnbagent-deploy token --provider bnb [--new | --revoke <id>] [--json]` (default: list).
108
108
  * Account-level — usable in ANY directory (`--provider bnb` + BNBAGENT_API_URL
109
109
  * outside a project); the spec, when present, only supplies [bnb] apiUrl.
110
110
  */
@@ -138,7 +138,7 @@ export async function tokenCmd(_spec: DeploymentSpec | undefined, ctx: Context):
138
138
  return;
139
139
  }
140
140
  if (!list.tokens.length) {
141
- ctx.logger.info("No API tokens yet — mint one: bnbagent-deploy token --new");
141
+ ctx.logger.info("No API tokens yet — mint one: bnbagent-deploy token --provider bnb --new");
142
142
  return;
143
143
  }
144
144
  for (const t of list.tokens) {
@@ -146,5 +146,5 @@ export async function tokenCmd(_spec: DeploymentSpec | undefined, ctx: Context):
146
146
  `${t.prefix ?? "bnbk_…"} ${t.scope ?? "tenant"} expires ${t.expires_at ?? "never"} (id ${t.id})`,
147
147
  );
148
148
  }
149
- ctx.logger.info("Revoke one: bnbagent-deploy token --revoke <id>");
149
+ ctx.logger.info("Revoke one: bnbagent-deploy token --provider bnb --revoke <id>");
150
150
  }
package/src/index.ts CHANGED
@@ -93,6 +93,10 @@ function slugPath(id: string): string {
93
93
  return encodeURIComponent(id);
94
94
  }
95
95
 
96
+ export function isFreshSlugError(error: unknown): boolean {
97
+ return error instanceof BnbApiError && error.status === 404;
98
+ }
99
+
96
100
  /**
97
101
  * The HARD spec contract for bnb/trial. validate() reports these early;
98
102
  * deploy() re-asserts UNCONDITIONALLY — --force / skipValidation only skip the
@@ -153,7 +157,8 @@ async function slugRetiredIssue(name: string, ctx: Context): Promise<string | un
153
157
  if (agent.state === "deleted" || agent.state === "reclaiming") return retired;
154
158
  } catch (e) {
155
159
  if (e instanceof BnbApiError && (e.status === 410 || e.code === "agent.deleted")) return retired;
156
- // 404 = fresh slug; anything else (auth/network) is another gate's job.
160
+ if (isFreshSlugError(e)) return undefined;
161
+ throw e;
157
162
  }
158
163
  return undefined;
159
164
  }
@@ -221,7 +226,13 @@ const bnbProvider: CloudProvider = {
221
226
  if (envApiToken()) return [{ level: "info", code: "auth.ok", message: "BNBAGENT_API_TOKEN is set (CI bearer)." }];
222
227
  const session = await loadSession();
223
228
  if (!session) {
224
- return [{ level: "error", code: "auth.signed_out", message: "No session and no BNBAGENT_API_TOKEN — run `bnbagent-deploy login`." }];
229
+ return [
230
+ {
231
+ level: "error",
232
+ code: "auth.signed_out",
233
+ message: "No session and no BNBAGENT_API_TOKEN — run `bnbagent-deploy login --provider bnb`.",
234
+ },
235
+ ];
225
236
  }
226
237
  return [{ level: "info", code: "auth.ok", message: `Session present (minted against ${session.apiUrl}).` }];
227
238
  },
@@ -254,7 +265,7 @@ const bnbProvider: CloudProvider = {
254
265
  if ("status" in res && res.status === "slow_down") interval += 5;
255
266
  // authorization_pending → keep polling
256
267
  }
257
- throw new Error("Device-flow sign-in timed out — run `bnbagent-deploy login` again.");
268
+ throw new Error("Device-flow sign-in timed out — run `bnbagent-deploy login --provider bnb` again.");
258
269
  },
259
270
 
260
271
  /** Show the signed-in identity (GitHub login) + trial clock. */
@@ -262,7 +273,7 @@ const bnbProvider: CloudProvider = {
262
273
  const session = await loadSession();
263
274
  const viaToken = envApiToken();
264
275
  if (!session && !viaToken) {
265
- ctx.logger.info("Not signed in to the trial platform (run `bnbagent-deploy login`).");
276
+ ctx.logger.info("Not signed in to the trial platform (run `bnbagent-deploy login --provider bnb`).");
266
277
  return;
267
278
  }
268
279
  try {
@@ -280,7 +291,7 @@ const bnbProvider: CloudProvider = {
280
291
  }
281
292
  } catch (e) {
282
293
  if (e instanceof BnbApiError && e.status === 401) {
283
- ctx.logger.info("Session expired — run `bnbagent-deploy login` again.");
294
+ ctx.logger.info("Session expired — run `bnbagent-deploy login --provider bnb` again.");
284
295
  return;
285
296
  }
286
297
  throw e;
@@ -326,7 +337,7 @@ const bnbProvider: CloudProvider = {
326
337
  level: "error",
327
338
  code: "auth.signed_out",
328
339
  message: "Not signed in to the trial platform.",
329
- hint: "Run: bnbagent-deploy login (CI: set BNBAGENT_API_TOKEN)",
340
+ hint: "Run: bnbagent-deploy login --provider bnb (CI: set BNBAGENT_API_TOKEN)",
330
341
  });
331
342
  } else if (!apiToken && session && base && session.apiUrl && !sameOrigin(session.apiUrl, base)) {
332
343
  // Parity with authedRequest's host binding: a token minted against
@@ -336,9 +347,27 @@ const bnbProvider: CloudProvider = {
336
347
  level: "error",
337
348
  code: "auth.host_mismatch",
338
349
  message: `Signed in against ${session.apiUrl}, but this spec targets ${base} — authenticated calls would be refused.`,
339
- hint: "Run: bnbagent-deploy login (against the current apiUrl), or fix [bnb] apiUrl / BNBAGENT_API_URL.",
350
+ hint:
351
+ "Run: bnbagent-deploy login --provider bnb (against the current apiUrl), or fix [bnb] apiUrl / BNBAGENT_API_URL.",
340
352
  });
341
353
  }
354
+ let authReady = false;
355
+ if ((apiToken || session) && base && !issues.some((i) => i.code === "auth.host_mismatch")) {
356
+ try {
357
+ await authedRequest(ctx, "GET", "/v1/account");
358
+ authReady = true;
359
+ } catch (e) {
360
+ const err = e instanceof BnbApiError ? e : undefined;
361
+ issues.push({
362
+ level: "error",
363
+ code: err?.code ?? "auth.unavailable",
364
+ message: `Trial-platform authentication failed: ${(e as Error).message}`,
365
+ ...(err?.status === 401
366
+ ? { hint: "Re-run login, or replace the revoked/expired BNBAGENT_API_TOKEN." }
367
+ : {}),
368
+ });
369
+ }
370
+ }
342
371
  try {
343
372
  if (!base) throw new Error("no endpoint");
344
373
  const campaign: CampaignStatus = await publicRequest(base, "GET", "/v1/campaign");
@@ -358,14 +387,22 @@ const bnbProvider: CloudProvider = {
358
387
  }
359
388
  // Doomed-deploy precheck (needs working auth): a deleted slug is
360
389
  // retired forever — deploy would fail AFTER uploading the artifact.
361
- if ((apiToken || session) && base && !issues.some((i) => i.code === "auth.host_mismatch")) {
362
- const retired = await slugRetiredIssue(spec.name, ctx);
363
- if (retired) {
390
+ if (authReady && base) {
391
+ try {
392
+ const retired = await slugRetiredIssue(spec.name, ctx);
393
+ if (retired) {
394
+ issues.push({
395
+ level: "error",
396
+ code: "agent.retired",
397
+ message: retired,
398
+ hint: "Pick a new agent name (slug) in agent-deploy.toml.",
399
+ });
400
+ }
401
+ } catch (e) {
364
402
  issues.push({
365
403
  level: "error",
366
- code: "agent.retired",
367
- message: retired,
368
- hint: "Pick a new agent name (slug) in agent-deploy.toml.",
404
+ code: e instanceof BnbApiError ? e.code : "agent.precheck_failed",
405
+ message: `Could not verify the agent slug: ${(e as Error).message}`,
369
406
  });
370
407
  }
371
408
  }
@@ -412,7 +449,7 @@ const bnbProvider: CloudProvider = {
412
449
  );
413
450
  }
414
451
  if (!(e instanceof BnbApiError)) throw e; // the retired-slug Error above
415
- // 404 = fresh slug; other API/network errors are the next call's problem.
452
+ if (!isFreshSlugError(e)) throw e;
416
453
  }
417
454
 
418
455
  const shipped = spec.type === "zip" ? await shipZipArtifact(spec, ctx) : await shipImageArtifact(spec, ctx);
package/src/init.ts CHANGED
@@ -26,6 +26,9 @@ export async function initCmd(ctx: Context): Promise<void> {
26
26
  const defaultName = slugify(basename(ctx.baseDir));
27
27
  const name = process.stdin.isTTY ? slugify(ask(`Agent name (slug) [${defaultName}]: `) || defaultName) : defaultName;
28
28
  const apiUrl = process.stdin.isTTY ? ask("Trial platform URL (https://…): ") : "";
29
+ const apiUrlLine = apiUrl
30
+ ? `apiUrl = ${JSON.stringify(apiUrl)}`
31
+ : '# apiUrl = "https://<the-trial-platform-host>" # Or set BNBAGENT_API_URL';
29
32
 
30
33
  const toml = `name = "${name}"
31
34
 
@@ -37,7 +40,7 @@ entrypoint = "main.py" # .py → Python · .js/.mjs/.cjs → Node (zip
37
40
  # protocol = "A2A" # or "MCP" — buyers then authenticate via the platform token plane
38
41
 
39
42
  [bnb]
40
- apiUrl = "${apiUrl || "https://<the-trial-platform-host>"}"
43
+ ${apiUrlLine}
41
44
 
42
45
  # [env] # non-sensitive runtime config (plaintext)
43
46
  # MY_FLAG = "1"
@@ -49,7 +52,7 @@ apiUrl = "${apiUrl || "https://<the-trial-platform-host>"}"
49
52
  ctx.logger.success(`Wrote ${file}`);
50
53
  if (!apiUrl) ctx.logger.warn("Set [bnb] apiUrl (or BNBAGENT_API_URL) before deploying — there is no default.");
51
54
  ctx.logger.info("Next steps:");
52
- ctx.logger.info(" 1. bnbagent-deploy login (GitHub device flow)");
55
+ ctx.logger.info(" 1. bnbagent-deploy login --provider bnb (GitHub device flow)");
53
56
  ctx.logger.info(" 2. bnbagent-deploy pack (Python zip projects — vendors arm64 deps)");
54
57
  ctx.logger.info(" 3. bnbagent-deploy deploy");
55
58
  }