@layers/amba 4.1.7 → 4.1.9

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/README.md CHANGED
@@ -42,6 +42,21 @@ fetches the canonical prompt from
42
42
  falls back to an inlined snapshot when offline. Use `--no-skills` to
43
43
  skip the skill install.
44
44
 
45
+ ### Invite-only signups
46
+
47
+ While new accounts are invite-only, pass your invite code:
48
+
49
+ ```bash
50
+ npx @layers/amba init --invite <code-from-your-invite-email>
51
+ ```
52
+
53
+ `AMBA_INVITE_CODE` works too; the flag wins when both are set. Without
54
+ a valid code, init stops with `INVITE_REQUIRED`, `INVITE_INVALID` or
55
+ `SIGNUPS_CLOSED` and prints where to request access
56
+ (<https://app.amba.dev/request-access>). The invite code arrives by
57
+ email. With `--json`, the failure is a JSON document on stdout carrying
58
+ `error.code`.
59
+
45
60
  ## Configure + first call
46
61
 
47
62
  ```bash
@@ -23,6 +23,37 @@
23
23
  * Project is resolved via `loadProjectConfig` (AMBA_PROJECT_ID env, then
24
24
  * .env / .env.local in the cwd) — same pattern as `amba secrets *`.
25
25
  */
26
+ /** Database limits block (free-tier compute + storage caps). */
27
+ interface DatabaseLimits {
28
+ limits_enforced: boolean;
29
+ source?: 'tier' | 'override' | 'unmanaged';
30
+ compute: {
31
+ max_cu: number;
32
+ used_cu_hours: number | null;
33
+ limit_cu_hours: number | null;
34
+ pct: number | null;
35
+ };
36
+ storage: {
37
+ used_mb: number | null;
38
+ limit_mb: number | null;
39
+ pct: number | null;
40
+ };
41
+ period: {
42
+ start: string;
43
+ resets_at: string;
44
+ } | null;
45
+ limit_reached: ('database_compute' | 'database_storage')[];
46
+ observed_at: string | null;
47
+ upgrade?: {
48
+ cli?: string;
49
+ mcp_tool?: string;
50
+ } | null;
51
+ }
52
+ /**
53
+ * Print the database limits block. Exported for `amba status`'s one-line
54
+ * summary + tests.
55
+ */
56
+ export declare function formatDatabaseLimits(db: DatabaseLimits): string[];
26
57
  export declare function billingStatusCommand(): Promise<void>;
27
58
  export declare function billingUpgradeCommand(input: {
28
59
  tier: 'pro' | 'scale';
@@ -32,3 +63,4 @@ export declare function billingPortalCommand(): Promise<void>;
32
63
  export declare function billingSetCeilingCommand(input: {
33
64
  ceiling: number | null;
34
65
  }): Promise<void>;
66
+ export {};
@@ -21,6 +21,12 @@ export interface InitOptions {
21
21
  sandbox?: boolean;
22
22
  /** Override the auto-generated sandbox email. Only meaningful with `--sandbox`. */
23
23
  sandboxEmail?: string;
24
+ /**
25
+ * Platform invite code (`--invite`) sent with the sandbox signup when
26
+ * new accounts are invite-only. Falls back to `AMBA_INVITE_CODE`.
27
+ * Passing the flag selects the headless signup path.
28
+ */
29
+ inviteCode?: string;
24
30
  /** Skip MCP-client-config writes. Mostly for testing. */
25
31
  noMcpConfig?: boolean;
26
32
  /**
@@ -38,6 +44,8 @@ export interface InitOptions {
38
44
  export declare function initCommand(options?: InitOptions): Promise<void>;
39
45
  export declare function runSandboxInit(cwd: string, options: {
40
46
  sandboxEmail?: string;
47
+ /** Platform invite code for a fresh signup; falls back to `AMBA_INVITE_CODE`. */
48
+ inviteCode?: string;
41
49
  noMcpConfig?: boolean;
42
50
  /**
43
51
  * Skip the project-local skill install. Default is to install. Set
@@ -52,6 +60,11 @@ export declare function runSandboxInit(cwd: string, options: {
52
60
  */
53
61
  homeDir?: string;
54
62
  }): Promise<SandboxResult>;
63
+ /**
64
+ * `--json` failure document. A gated signup carries its 403 code plus
65
+ * where to request access; every other failure reports `INIT_FAILED`.
66
+ */
67
+ export declare function sandboxErrorToJson(err: unknown): Record<string, unknown>;
55
68
  /**
56
69
  * Build the plaintext (no ANSI) success-output block printed at the
57
70
  * end of `amba init --sandbox`. Pure function — exported for the
@@ -148,6 +148,11 @@ export interface EnsureDeveloperOptions {
148
148
  * identity is reused.
149
149
  */
150
150
  sandboxEmail?: string;
151
+ /**
152
+ * Platform invite code for the sandbox signup (`--invite`). Falls back
153
+ * to `AMBA_INVITE_CODE`. Ignored when an existing identity is reused.
154
+ */
155
+ inviteCode?: string;
151
156
  }
152
157
  export interface EnsureDeveloperResult {
153
158
  credentials: DeveloperCredentials;
package/dist/index.js CHANGED
@@ -597,12 +597,36 @@ const RESERVED_BINDING_PREFIXES = ["AMBA_", "EDGE_"];
597
597
  const RESERVED_BINDING_EXACT_NAMES = ["STORAGE", "EDGE_DB_PROXY"];
598
598
  const VALID_BINDING_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
599
599
  const MAX_BINDING_NAME_LENGTH = 64;
600
+ const INJECTED_BINDINGS_BY_PREFIX = {
601
+ AMBA_: [
602
+ "AMBA_API_URL",
603
+ "AMBA_PROJECT_ID",
604
+ "AMBA_INTERNAL_TOKEN",
605
+ "AMBA_AI_GATEWAY_URL"
606
+ ],
607
+ EDGE_: ["EDGE_HEADER_SIGNING_SECRET"]
608
+ };
609
+ const RESERVED_EXACT_PURPOSE = {
610
+ STORAGE: "the platform's built-in object-storage binding (env.STORAGE)",
611
+ EDGE_DB_PROXY: "a platform database binding"
612
+ };
613
+ const SUGGESTED_BINDING_PREFIX = "APP_";
614
+ function suggestedBindingName(name) {
615
+ const reservedPrefix = RESERVED_BINDING_PREFIXES.find((prefix) => name.startsWith(prefix)) ?? "";
616
+ const rest = name.slice(reservedPrefix.length);
617
+ const candidate = `${SUGGESTED_BINDING_PREFIX}${rest}`;
618
+ return rest.length > 0 && VALID_BINDING_NAME_RE.test(candidate) ? candidate : `${SUGGESTED_BINDING_PREFIX}SECRET`;
619
+ }
600
620
  /** Return why a binding name is reserved/invalid, or `null` if acceptable. */
601
621
  function getBindingReservationReason(name) {
602
622
  if (typeof name !== "string" || name.length === 0) return "Binding name must be a non-empty string";
603
623
  if (name.length > MAX_BINDING_NAME_LENGTH) return `Binding name must be at most ${MAX_BINDING_NAME_LENGTH} characters`;
604
- for (const prefix of RESERVED_BINDING_PREFIXES) if (name.startsWith(prefix)) return `Binding name starts with reserved prefix "${prefix}" (platform namespace)`;
605
- for (const exact of RESERVED_BINDING_EXACT_NAMES) if (name === exact) return `Binding name "${exact}" is reserved by a platform binding`;
624
+ for (const exact of RESERVED_BINDING_EXACT_NAMES) if (name === exact) return `"${exact}" is reserved for ${RESERVED_EXACT_PURPOSE[exact]}. Use your own name instead, for example "${suggestedBindingName(name)}"`;
625
+ for (const prefix of RESERVED_BINDING_PREFIXES) if (name.startsWith(prefix)) {
626
+ const injected = INJECTED_BINDINGS_BY_PREFIX[prefix];
627
+ if (injected.includes(name)) return `"${name}" is injected into every function by the platform; read env.${name} instead of setting it`;
628
+ return `names starting with "${prefix}" are reserved for values the platform injects into every function (${injected.join(", ")}). Use your own prefix instead, for example "${suggestedBindingName(name)}"`;
629
+ }
606
630
  if (!VALID_BINDING_NAME_RE.test(name)) return "Binding name must match /^[A-Z][A-Z0-9_]*$/ (uppercase ASCII, digits, underscore; must start with a letter)";
607
631
  return null;
608
632
  }
@@ -1300,6 +1324,66 @@ function generateSandboxEmail() {
1300
1324
  function generateSandboxPassword() {
1301
1325
  return randomBytes(24).toString("base64url");
1302
1326
  }
1327
+ /** Request header carrying AMBA_SIGNUP_TOKEN on the signup call. */
1328
+ const SIGNUP_TOKEN_HEADER = "X-Amba-Signup-Token";
1329
+ /** Environment variable read when `--invite` is absent. */
1330
+ const INVITE_CODE_ENV = "AMBA_INVITE_CODE";
1331
+ /** The 403 codes signup answers with when the platform is gated. */
1332
+ const SIGNUP_ACCESS_ERROR_CODES = [
1333
+ "SIGNUPS_CLOSED",
1334
+ "INVITE_REQUIRED",
1335
+ "INVITE_INVALID"
1336
+ ];
1337
+ const DEFAULT_REQUEST_ACCESS_URL = `${CONSOLE_URL}/request-access`;
1338
+ const REQUEST_ACCESS_ENDPOINT = `POST /v1/auth/developer/waitlist`;
1339
+ const RERUN_WITH_INVITE = "npx @layers/amba init --invite <code>";
1340
+ function isSignupAccessErrorCode(code) {
1341
+ return SIGNUP_ACCESS_ERROR_CODES.includes(code);
1342
+ }
1343
+ /**
1344
+ * The flag value when set, else `AMBA_INVITE_CODE`, else undefined. Blank
1345
+ * values count as absent so an empty env var never sends `invite_code: ""`.
1346
+ */
1347
+ function resolveInviteCode(flag) {
1348
+ const fromFlag = flag?.trim();
1349
+ if (fromFlag) return fromFlag;
1350
+ return process.env["AMBA_INVITE_CODE"]?.trim() || void 0;
1351
+ }
1352
+ /**
1353
+ * Ordered next steps for a gated signup, phrased for whoever runs the CLI
1354
+ * (a developer or their coding agent).
1355
+ */
1356
+ function signupAccessNextSteps(code, requestAccessUrl) {
1357
+ const requestAccess = `Request access at ${requestAccessUrl}; the invite code arrives by email.`;
1358
+ switch (code) {
1359
+ case "INVITE_REQUIRED": return [`Have an invite code? Rerun \`${RERUN_WITH_INVITE}\` (or set ${INVITE_CODE_ENV}).`, `No code yet? ${requestAccess}`];
1360
+ case "INVITE_INVALID": return [`Check the code and rerun \`${RERUN_WITH_INVITE}\` (or correct ${INVITE_CODE_ENV}).`, `Need a fresh code? ${requestAccess}`];
1361
+ case "SIGNUPS_CLOSED": return [`${requestAccess} Rerun \`${RERUN_WITH_INVITE}\` once it lands.`];
1362
+ }
1363
+ }
1364
+ /**
1365
+ * Thrown when signup answers 403 SIGNUPS_CLOSED / INVITE_REQUIRED /
1366
+ * INVITE_INVALID. `message` carries the server's sentence plus the next
1367
+ * steps; the structured fields feed `amba init --json`.
1368
+ */
1369
+ var SignupAccessError = class extends Error {
1370
+ code;
1371
+ serverMessage;
1372
+ requestAccessUrl;
1373
+ requestAccessEndpoint;
1374
+ nextSteps;
1375
+ constructor(input) {
1376
+ const requestAccessUrl = input.requestAccessUrl ?? DEFAULT_REQUEST_ACCESS_URL;
1377
+ const nextSteps = signupAccessNextSteps(input.code, requestAccessUrl);
1378
+ super([`Sandbox signup failed (${input.code}): ${input.serverMessage}`, ...nextSteps].join("\n "));
1379
+ this.name = "SignupAccessError";
1380
+ this.code = input.code;
1381
+ this.serverMessage = input.serverMessage;
1382
+ this.requestAccessUrl = requestAccessUrl;
1383
+ this.requestAccessEndpoint = input.requestAccessEndpoint ?? REQUEST_ACCESS_ENDPOINT;
1384
+ this.nextSteps = nextSteps;
1385
+ }
1386
+ };
1303
1387
  /**
1304
1388
  * POST the synthesized credentials at the public signup endpoint.
1305
1389
  *
@@ -1311,28 +1395,45 @@ function generateSandboxPassword() {
1311
1395
  * Returns the unwrapped, flattened shape consumed by the rest of the
1312
1396
  * sandbox flow. Throws with a human-readable message on any non-2xx so
1313
1397
  * the CLI's `runAction` wrapper can surface it without crashing on a
1314
- * generic 'fetch failed'.
1398
+ * generic 'fetch failed'. A gated platform (403 SIGNUPS_CLOSED /
1399
+ * INVITE_REQUIRED / INVITE_INVALID) throws `SignupAccessError`.
1315
1400
  */
1316
1401
  async function performSandboxSignup(req, options = {}) {
1317
1402
  const apiUrl = options.apiUrl ?? process.env["AMBA_API_URL"] ?? "https://api.amba.dev";
1318
- const res = await (options.fetchImpl ?? fetch)(`${apiUrl}/v1/auth/developer/signup`, {
1403
+ const fetchImpl = options.fetchImpl ?? fetch;
1404
+ const url = `${apiUrl}/v1/auth/developer/signup`;
1405
+ const headers = {
1406
+ "Content-Type": "application/json",
1407
+ "User-Agent": "amba-cli/sandbox"
1408
+ };
1409
+ const signupToken = process.env["AMBA_SIGNUP_TOKEN"]?.trim();
1410
+ if (signupToken) headers[SIGNUP_TOKEN_HEADER] = signupToken;
1411
+ const body = {
1412
+ email: req.email,
1413
+ password: req.password,
1414
+ name: req.name ?? "amba-sandbox-cli"
1415
+ };
1416
+ const inviteCode = resolveInviteCode(req.inviteCode);
1417
+ if (inviteCode) body["invite_code"] = inviteCode;
1418
+ const res = await fetchImpl(url, {
1319
1419
  method: "POST",
1320
- headers: {
1321
- "Content-Type": "application/json",
1322
- "User-Agent": "amba-cli/sandbox"
1323
- },
1324
- body: JSON.stringify({
1325
- email: req.email,
1326
- password: req.password,
1327
- name: req.name ?? "amba-sandbox-cli"
1328
- })
1420
+ headers,
1421
+ body: JSON.stringify(body)
1329
1422
  });
1330
1423
  if (!res.ok) {
1331
1424
  let detail = `${res.status} ${res.statusText}`;
1425
+ let errorBody = {};
1332
1426
  try {
1333
- const body = await res.json();
1334
- if (body.error?.message) detail = body.error.message;
1427
+ errorBody = await res.json();
1428
+ if (errorBody.error?.message) detail = errorBody.error.message;
1335
1429
  } catch {}
1430
+ const code = errorBody.error?.code;
1431
+ if (res.status === 403 && isSignupAccessErrorCode(code)) throw new SignupAccessError({
1432
+ code,
1433
+ serverMessage: detail,
1434
+ requestAccessUrl: errorBody.error?.details?.request_access_url,
1435
+ requestAccessEndpoint: errorBody.error?.details?.request_access_endpoint
1436
+ });
1336
1437
  throw new Error(`Sandbox signup failed: ${detail}`);
1337
1438
  }
1338
1439
  let raw;
@@ -1932,7 +2033,8 @@ async function ensureDeveloperIdentity(options = {}) {
1932
2033
  if (options.signupOnMissing === false) throw new Error(`No verified Amba identity at ${developerCredentialsPath(options.homeDir)} and signup-on-missing is disabled. Run \`amba login\` to authenticate.`);
1933
2034
  const signup = await performSandboxSignup({
1934
2035
  email: options.sandboxEmail?.trim() || generateSandboxEmail(),
1935
- password: generateSandboxPassword()
2036
+ password: generateSandboxPassword(),
2037
+ inviteCode: options.inviteCode
1936
2038
  }, {
1937
2039
  apiUrl,
1938
2040
  fetchImpl
@@ -2685,6 +2787,8 @@ Tell the user where their credentials live:
2685
2787
  - \`project_id\`, \`client_key\` — the values they paste into their app's \`.env.local\` / \`.env\`.
2686
2788
  - \`server_key\` — never ship to user devices; only into a server \`.env\` or a secret manager. The \`amb_dev_sk_\` / \`amb_live_sk_\` prefix is the marker.
2687
2789
 
2790
+ **Signup refused with 403 \`INVITE_REQUIRED\` or \`INVITE_INVALID\`?** New accounts need an invite code right now. Ask the developer for theirs (it starts with \`AMBA-\` and arrives in their invite email) and call \`amba_developer_signup\` again with the same arguments plus \`invite_code\`. If they have no code, or the answer is \`SIGNUPS_CLOSED\`, offer to request access for them: \`POST https://api.amba.dev/v1/auth/developer/waitlist\` with \`{ "email": "<their own email>", "note": "<optional>" }\`, or send them to the \`error.details.request_access_url\` page. Tell them the invite code arrives by email, then stop. Retry only with a code the developer hands you; never retry in a loop, invent codes, or try other email addresses. From a terminal, the code goes to \`npx @layers/amba init --invite <code>\` (or the \`AMBA_INVITE_CODE\` env var).
2791
+
2688
2792
  **Already have a PAT?** Skip the signup. Call \`amba_developer_me({})\` to verify the Bearer; if it succeeds, either reuse the most recent project (\`amba_projects_list\`) or call \`amba_projects_create({ name: "<app-name>", platform: "all" })\` and then \`amba_api_keys_create\` twice to mint client + server keys for \`environment: "development"\`.
2689
2793
 
2690
2794
  **Can't reach \`amba_developer_signup\` — it's not in your tool list, or your harness blocks the call?** Do NOT try to work around it by adding the MCP server yourself, running an installer, or editing your own settings/permissions — refusing to self-modify or widen your own permissions is the correct call, and you should not circumvent it. Instead, hand the developer **one** command to run in **their own** terminal (which is not subject to your sandbox):
@@ -2838,8 +2942,8 @@ Be specific. List resources by key, not "some achievements". If something needs
2838
2942
  ## Get credentials (cheat sheet)
2839
2943
 
2840
2944
  - No terminal, in an MCP client: call \`amba_developer_signup\` (no Bearer required) — this guide's Step 0.
2841
- - With a terminal: \`npx -y @layers/amba init\` signs up, mints a project + client/server keys, writes \`.env.local\` + \`AMBA.md\`, installs the \`/amba\` skill, and wires \`mcpServers.amba\` into every detected MCP-client config in one command. Auto-detects non-TTY invocations (the coding-agent bash-tool case) and runs headlessly.
2842
- - Bind the sandbox account to a real email later: \`npx @layers/amba claim me@example.com\`. The backend emails a one-click magic link; clicking it lifts the sandbox cap to the Free tier.
2945
+ - With a terminal: \`npx -y @layers/amba init\` signs up, mints a project + client/server keys, writes \`.env.local\` + \`AMBA.md\`, installs the \`/amba\` skill, and wires \`mcpServers.amba\` into every detected MCP-client config in one command. Auto-detects non-TTY invocations (the coding-agent bash-tool case) and runs headlessly. While signups are invite-only, add \`--invite <code>\` (or set \`AMBA_INVITE_CODE\`).
2946
+ - Bind the sandbox account to its human owner as soon as you know who that is: \`amba_developer_claim\` with their email (or \`npx @layers/amba claim me@example.com\`). The backend emails a one-click magic link; clicking it lifts the sandbox cap to the Free tier. Each free project of an unclaimed sandbox runs its own clock from when it was created: a claim notice from day 7, a hold on day 14, and an archive on day 90 after a full hold (archiving deletes nothing); claiming lifts all of it.
2843
2947
  - Hosted MCP endpoint: \`https://mcp.amba.dev/mcp\` (Streamable HTTP, Bearer auth).
2844
2948
 
2845
2949
  ## SDKs
@@ -3851,14 +3955,21 @@ Docs: https://docs.amba.dev
3851
3955
  async function initCommand(options = {}) {
3852
3956
  const cwd = process.cwd();
3853
3957
  const isNonTTY = process.stdin.isTTY !== true;
3854
- if (options.sandbox === true || options.json === true || isNonTTY) {
3855
- const result = await runSandboxInit(cwd, {
3856
- sandboxEmail: options.sandboxEmail,
3857
- noMcpConfig: options.noMcpConfig,
3858
- noSkills: options.noSkills,
3859
- json: options.json,
3860
- homeDir: options.homeDir
3861
- });
3958
+ if (options.sandbox === true || options.json === true || Boolean(options.inviteCode?.trim()) || isNonTTY) {
3959
+ let result;
3960
+ try {
3961
+ result = await runSandboxInit(cwd, {
3962
+ sandboxEmail: options.sandboxEmail,
3963
+ inviteCode: options.inviteCode,
3964
+ noMcpConfig: options.noMcpConfig,
3965
+ noSkills: options.noSkills,
3966
+ json: options.json,
3967
+ homeDir: options.homeDir
3968
+ });
3969
+ } catch (err) {
3970
+ if (options.json) process.stdout.write(JSON.stringify(sandboxErrorToJson(err), null, 2) + "\n");
3971
+ throw err;
3972
+ }
3862
3973
  if (options.json) process.stdout.write(JSON.stringify(sandboxResultToJson(result), null, 2) + "\n");
3863
3974
  else printSandboxNextSteps(result);
3864
3975
  return;
@@ -4103,7 +4214,8 @@ async function runSandboxInit(cwd, options) {
4103
4214
  };
4104
4215
  } else identity = await ensureDeveloperIdentity({
4105
4216
  homeDir: options.homeDir,
4106
- sandboxEmail: options.sandboxEmail
4217
+ sandboxEmail: options.sandboxEmail,
4218
+ inviteCode: options.inviteCode
4107
4219
  });
4108
4220
  setBearerOverride(identity.credentials.pat);
4109
4221
  const framework = await detectFramework(cwd);
@@ -4172,6 +4284,31 @@ async function runSandboxInit(cwd, options) {
4172
4284
  setupTargets
4173
4285
  };
4174
4286
  }
4287
+ /**
4288
+ * `--json` failure document. A gated signup carries its 403 code plus
4289
+ * where to request access; every other failure reports `INIT_FAILED`.
4290
+ */
4291
+ function sandboxErrorToJson(err) {
4292
+ if (err instanceof SignupAccessError) return {
4293
+ ok: false,
4294
+ mode: "sandbox",
4295
+ error: {
4296
+ code: err.code,
4297
+ message: err.serverMessage,
4298
+ request_access_url: err.requestAccessUrl,
4299
+ request_access_endpoint: err.requestAccessEndpoint
4300
+ },
4301
+ next_steps: err.nextSteps
4302
+ };
4303
+ return {
4304
+ ok: false,
4305
+ mode: "sandbox",
4306
+ error: {
4307
+ code: "INIT_FAILED",
4308
+ message: err instanceof Error ? err.message : String(err)
4309
+ }
4310
+ };
4311
+ }
4175
4312
  function sandboxResultToJson(r) {
4176
4313
  return {
4177
4314
  ok: true,
@@ -6830,6 +6967,28 @@ async function adminWrite$1(method, path, body) {
6830
6967
  }
6831
6968
  return await res.json();
6832
6969
  }
6970
+ function pctLabel(pct) {
6971
+ return pct === null ? "" : pc.dim(` (${Math.round(pct * 100)}%)`);
6972
+ }
6973
+ /**
6974
+ * Print the database limits block. Exported for `amba status`'s one-line
6975
+ * summary + tests.
6976
+ */
6977
+ function formatDatabaseLimits(db) {
6978
+ const lines = [];
6979
+ const computeUsed = db.compute.used_cu_hours === null ? "—" : `${db.compute.used_cu_hours}`;
6980
+ const computeLimit = db.compute.limit_cu_hours === null ? "no cap" : `${db.compute.limit_cu_hours} CU-hours`;
6981
+ lines.push(` ${pc.dim("Database compute".padEnd(22))} ${computeUsed} / ${computeLimit}${pctLabel(db.compute.pct)}` + pc.dim(` max ${db.compute.max_cu} CU`));
6982
+ const storageUsed = db.storage.used_mb === null ? "—" : `${db.storage.used_mb} MB`;
6983
+ const storageLimit = db.storage.limit_mb === null ? "no cap" : `${db.storage.limit_mb} MB`;
6984
+ lines.push(` ${pc.dim("Database size".padEnd(22))} ${storageUsed} / ${storageLimit}${pctLabel(db.storage.pct)}`);
6985
+ if (db.period && db.compute.limit_cu_hours !== null) lines.push(` ${pc.dim("Compute resets".padEnd(22))} ${new Date(db.period.resets_at).toLocaleDateString(void 0, { timeZone: "UTC" })} UTC`);
6986
+ const upgrade = db.upgrade?.cli ?? "amba billing upgrade --tier pro";
6987
+ if (db.limit_reached.includes("database_compute")) lines.push(pc.red(" Database paused: ") + `compute limit reached; database calls return 402 FREE_TIER_LIMIT_REACHED until the period resets. Upgrade now: ${pc.bold(upgrade)}`);
6988
+ if (db.limit_reached.includes("database_storage")) lines.push(pc.red(" Database full: ") + `storage limit reached; writes that add data return 402. Delete data or upgrade: ${pc.bold(upgrade)}`);
6989
+ else if (!db.limit_reached.includes("database_compute") && db.compute.pct !== null && db.compute.pct >= .8) lines.push(pc.yellow(" Near the compute limit: ") + `upgrade before the database pauses: ${pc.bold(upgrade)}`);
6990
+ return lines;
6991
+ }
6833
6992
  /** Append "· $X.XX" to a usage line when the meter accrued cost this period. */
6834
6993
  function costSuffix(cost) {
6835
6994
  if (!cost || cost.cost_usd <= 0) return "";
@@ -6878,6 +7037,11 @@ async function billingStatusCommand() {
6878
7037
  console.log(formatAxis("Database storage", s.headroom.db_storage_mb, true) + costSuffix(cost.db_storage_mb));
6879
7038
  console.log(formatAxis("Media storage", s.headroom.media_storage_mb, true) + costSuffix(cost.media_storage_mb));
6880
7039
  if (s.headroom.app_mcp_tool_calls) console.log(formatAxis("Agent tool calls", s.headroom.app_mcp_tool_calls, false) + costSuffix(cost.app_mcp_tool_calls));
7040
+ if (s.database) {
7041
+ console.log();
7042
+ console.log(pc.bold(" Database limits") + (s.database.observed_at ? pc.dim(` — measured ${new Date(s.database.observed_at).toLocaleString()}`) : ""));
7043
+ for (const line of formatDatabaseLimits(s.database)) console.log(line);
7044
+ }
6881
7045
  console.log();
6882
7046
  if (s.human_action_required !== "none") {
6883
7047
  const action = s.human_action_required.replaceAll("_", " ");
@@ -6908,6 +7072,7 @@ async function billingUpgradeCommand(input) {
6908
7072
  console.log(` ${pc.cyan(res.data.url)}`);
6909
7073
  console.log();
6910
7074
  console.log(pc.dim(" Subscription status updates automatically once Stripe confirms (a few seconds)."));
7075
+ console.log(pc.dim(" Paid plans lift the free-tier database compute cap; a paused database resumes within seconds."));
6911
7076
  console.log();
6912
7077
  } catch (err) {
6913
7078
  handleError$1(err);
@@ -9780,7 +9945,7 @@ async function diffConfigCommand(file = DEFAULT_BUNDLE_FILE, options = {}) {
9780
9945
  //#endregion
9781
9946
  //#region src/index.ts
9782
9947
  const program = new Command();
9783
- program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.1.7");
9948
+ program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.1.9");
9784
9949
  program.option("--token <pat>", "Use a Personal Access Token for headless / CI / agent use (overrides ~/.amba/credentials.json + AMBA_PAT env)");
9785
9950
  program.hook("preAction", (thisCommand) => {
9786
9951
  setBearerOverride(resolveTokenSource({
@@ -9795,7 +9960,7 @@ function runAction(fn) {
9795
9960
  process.exit(1);
9796
9961
  });
9797
9962
  }
9798
- program.command("init").description("Initialize Amba in the current project (mints a personal dev project by default)").option("--with-example", "Scaffold a sample app.tsx + README snippet into the current directory").option("--env <env>", "'development' (default) or 'production'").option("--sandbox", "Headless agentic mode: auto-signup, write .env.local + AMBA.md, auto-wire MCP client configs. No prompts.").option("--email <email>", "Override the auto-generated sandbox email (requires --sandbox)").option("--no-mcp-config", "Skip writing MCP client config files (rare; mostly for testing)").option("--no-skills", "Skip installing the project-local /amba-build Claude Code skill (default: install during --sandbox)").option("--json", "Emit a machine-readable JSON summary on stdout instead of human-readable lines").action(async (opts) => {
9963
+ program.command("init").description("Initialize Amba in the current project (mints a personal dev project by default)").option("--with-example", "Scaffold a sample app.tsx + README snippet into the current directory").option("--env <env>", "'development' (default) or 'production'").option("--sandbox", "Headless agentic mode: auto-signup, write .env.local + AMBA.md, auto-wire MCP client configs. No prompts.").option("--email <email>", "Override the auto-generated sandbox email (requires --sandbox)").option("--invite <code>", "Invite code for creating a new account while signups are invite-only (implies --sandbox; also read from AMBA_INVITE_CODE)").option("--no-mcp-config", "Skip writing MCP client config files (rare; mostly for testing)").option("--no-skills", "Skip installing the project-local /amba-build Claude Code skill (default: install during --sandbox)").option("--json", "Emit a machine-readable JSON summary on stdout instead of human-readable lines").action(async (opts) => {
9799
9964
  let env;
9800
9965
  if (opts.env === "development" || opts.env === "dev") env = "development";
9801
9966
  else if (opts.env === "production" || opts.env === "prod") env = "production";
@@ -9803,27 +9968,29 @@ program.command("init").description("Initialize Amba in the current project (min
9803
9968
  console.error(`Error: --env must be 'development' or 'production' (got '${opts.env}').`);
9804
9969
  process.exit(1);
9805
9970
  }
9806
- if (opts.email && !opts.sandbox) {
9971
+ const sandbox = opts.sandbox === true || Boolean(opts.invite?.trim());
9972
+ if (opts.email && !sandbox) {
9807
9973
  console.error("Error: --email is only valid with --sandbox.");
9808
9974
  process.exit(1);
9809
9975
  }
9810
- if (opts.json && !opts.sandbox) {
9976
+ if (opts.json && !sandbox) {
9811
9977
  console.error("Error: --json is only valid with --sandbox.");
9812
9978
  process.exit(1);
9813
9979
  }
9814
- if (opts.mcpConfig === false && !opts.sandbox) {
9980
+ if (opts.mcpConfig === false && !sandbox) {
9815
9981
  console.error("Error: --no-mcp-config is only valid with --sandbox.");
9816
9982
  process.exit(1);
9817
9983
  }
9818
- if (opts.skills === false && !opts.sandbox) {
9984
+ if (opts.skills === false && !sandbox) {
9819
9985
  console.error("Error: --no-skills is only valid with --sandbox.");
9820
9986
  process.exit(1);
9821
9987
  }
9822
9988
  await runAction(() => initCommand({
9823
9989
  withExample: opts.withExample,
9824
9990
  env,
9825
- sandbox: opts.sandbox,
9991
+ sandbox,
9826
9992
  sandboxEmail: opts.email,
9993
+ inviteCode: opts.invite,
9827
9994
  noMcpConfig: opts.mcpConfig === false,
9828
9995
  noSkills: opts.skills === false,
9829
9996
  json: opts.json
@@ -9985,7 +10152,7 @@ functionDomains.command("remove <name> <hostname>").description("Detach an exact
9985
10152
  await runAction(() => functionDomainsRemoveCommand(name, hostname));
9986
10153
  });
9987
10154
  const secrets = program.command("secrets").description("Function secrets (GCP canonical, Workers-Secret synced)");
9988
- secrets.command("set <name> [value]").description("Store a secret and queue its sync to your deployed function(s). Omit --function for a project-wide secret. Use --from-stdin to keep the value out of shell history.").option("--function <name>", "Function the secret binds to. OMIT this flag to set a PROJECT-WIDE secret — one value visible to every function in the project (and to any function deployed later). With it, the secret is scoped to just that function and overrides a same-name project-wide value. Use the same name as the entry passed to 'amba functions deploy'. Secrets may be set before the function is deployed; the sync drains once you deploy.").option("--env <env>", "'dev' | 'prod' — INFORMATIONAL ONLY. Both share one secret namespace per project. Use distinct secret names (e.g. STRIPE_KEY_DEV / STRIPE_KEY_PROD) or two amba projects for real env isolation.").option("--from-stdin", "Read the secret value from stdin instead of the positional arg. Mutually exclusive with <value>. Pipe in: `echo $KEY | amba secrets set NAME --from-stdin`.").action(async (name, value, opts) => {
10155
+ secrets.command("set <name> [value]").description("Store a secret and queue its sync to your deployed function(s). Omit --function for a project-wide secret. Use --from-stdin to keep the value out of shell history. Names starting with AMBA_ or EDGE_ are reserved for values every function already receives (env.AMBA_PROJECT_ID, env.AMBA_API_URL, env.AMBA_INTERNAL_TOKEN); use your own prefix, e.g. APP_CLIENT_KEY.").option("--function <name>", "Function the secret binds to. OMIT this flag to set a PROJECT-WIDE secret — one value visible to every function in the project (and to any function deployed later). With it, the secret is scoped to just that function and overrides a same-name project-wide value. Use the same name as the entry passed to 'amba functions deploy'. Secrets may be set before the function is deployed; the sync drains once you deploy.").option("--env <env>", "'dev' | 'prod' — INFORMATIONAL ONLY. Both share one secret namespace per project. Use distinct secret names (e.g. STRIPE_KEY_DEV / STRIPE_KEY_PROD) or two amba projects for real env isolation.").option("--from-stdin", "Read the secret value from stdin instead of the positional arg. Mutually exclusive with <value>. Pipe in: `echo $KEY | amba secrets set NAME --from-stdin`.").action(async (name, value, opts) => {
9989
10156
  await runAction(() => secretsSetCommand(name, value, {
9990
10157
  function: opts.function,
9991
10158
  env: opts.env ?? "dev",
package/dist/sandbox.d.ts CHANGED
@@ -43,6 +43,12 @@ export interface SandboxSignupRequest {
43
43
  email: string;
44
44
  password: string;
45
45
  name?: string;
46
+ /**
47
+ * Platform invite code (`--invite`). Falls back to `AMBA_INVITE_CODE`
48
+ * when absent. Sent as `invite_code`; the server ignores case, spaces
49
+ * and dashes.
50
+ */
51
+ inviteCode?: string;
46
52
  }
47
53
  /**
48
54
  * Shape returned by `POST /v1/auth/developer/signup`. Only the fields the
@@ -146,6 +152,36 @@ export declare function generateSandboxEmail(): string;
146
152
  * accept "agent signup" with no password and we'd drop this entirely.
147
153
  */
148
154
  export declare function generateSandboxPassword(): string;
155
+ /** Request header carrying AMBA_SIGNUP_TOKEN on the signup call. */
156
+ export declare const SIGNUP_TOKEN_HEADER = "X-Amba-Signup-Token";
157
+ /** Environment variable read when `--invite` is absent. */
158
+ export declare const INVITE_CODE_ENV = "AMBA_INVITE_CODE";
159
+ /** The 403 codes signup answers with when the platform is gated. */
160
+ export declare const SIGNUP_ACCESS_ERROR_CODES: readonly ["SIGNUPS_CLOSED", "INVITE_REQUIRED", "INVITE_INVALID"];
161
+ export type SignupAccessErrorCode = (typeof SIGNUP_ACCESS_ERROR_CODES)[number];
162
+ /**
163
+ * The flag value when set, else `AMBA_INVITE_CODE`, else undefined. Blank
164
+ * values count as absent so an empty env var never sends `invite_code: ""`.
165
+ */
166
+ export declare function resolveInviteCode(flag?: string): string | undefined;
167
+ /**
168
+ * Thrown when signup answers 403 SIGNUPS_CLOSED / INVITE_REQUIRED /
169
+ * INVITE_INVALID. `message` carries the server's sentence plus the next
170
+ * steps; the structured fields feed `amba init --json`.
171
+ */
172
+ export declare class SignupAccessError extends Error {
173
+ readonly code: SignupAccessErrorCode;
174
+ readonly serverMessage: string;
175
+ readonly requestAccessUrl: string;
176
+ readonly requestAccessEndpoint: string;
177
+ readonly nextSteps: string[];
178
+ constructor(input: {
179
+ code: SignupAccessErrorCode;
180
+ serverMessage: string;
181
+ requestAccessUrl?: string;
182
+ requestAccessEndpoint?: string;
183
+ });
184
+ }
149
185
  /**
150
186
  * POST the synthesized credentials at the public signup endpoint.
151
187
  *
@@ -157,7 +193,8 @@ export declare function generateSandboxPassword(): string;
157
193
  * Returns the unwrapped, flattened shape consumed by the rest of the
158
194
  * sandbox flow. Throws with a human-readable message on any non-2xx so
159
195
  * the CLI's `runAction` wrapper can surface it without crashing on a
160
- * generic 'fetch failed'.
196
+ * generic 'fetch failed'. A gated platform (403 SIGNUPS_CLOSED /
197
+ * INVITE_REQUIRED / INVITE_INVALID) throws `SignupAccessError`.
161
198
  */
162
199
  export declare function performSandboxSignup(req: SandboxSignupRequest, options?: {
163
200
  apiUrl?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layers/amba",
3
- "version": "4.1.7",
3
+ "version": "4.1.9",
4
4
  "description": "amba — agent-native backend-as-a-service. Functions, collections, storage, AI, email, queues, sites, and `amba ship` to take an Expo app live on the App Store + Google Play. `npx @layers/amba init` to start.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,8 +49,8 @@
49
49
  "tsdown": "^0.12.5",
50
50
  "typescript": "^5.8.3",
51
51
  "vitest": "^3.2.4",
52
- "@layers/amba-mcp": "4.0.10",
53
- "@layers/amba-shared": "4.0.5"
52
+ "@layers/amba-mcp": "4.0.12",
53
+ "@layers/amba-shared": "4.0.6"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsdown && tsc --emitDeclarationOnly",
@@ -69,7 +69,9 @@ Check whether a project exists in this directory:
69
69
 
70
70
  The project status is `"provisioning"` immediately after signup; the per-tenant DB activates within ~5 seconds. Don't block — your next SDK or MCP call may briefly retry, that's fine. If you want to be polite, call `amba_projects_get_provisioning_status({ project_id })` once and proceed when it returns `"active"` (or after 15s, whichever first).
71
71
 
72
- > **If you don't see Amba MCP tools available** (no `mcp__amba__*` in your tool list), Amba's MCP server isn't wired into your agent's tool surface this session. Run `npx -y @layers/amba init` via your bash tool — it provisions the account/project, writes `.env.local`, installs this skill, and wires the MCP entry into the user's agent config so the typed tools light up on the next agent launch.
72
+ **Signup refused with 403 `INVITE_REQUIRED` or `INVITE_INVALID`?** New accounts need an invite code right now. Ask the developer for theirs (it starts with `AMBA-` and arrives in their invite email) and call `amba_developer_signup` again with the same arguments plus `invite_code` (or rerun `npx -y @layers/amba init --invite <code>`; `AMBA_INVITE_CODE` works too). If they have no code, or the answer is `SIGNUPS_CLOSED`, offer to request access for them: `POST https://api.amba.dev/v1/auth/developer/waitlist` with `{ "email": "<their own email>", "note": "<optional>" }`, or send them to the `error.details.request_access_url` page. Tell them the invite code arrives by email, then stop. Retry only with a code the developer hands you; never retry in a loop, invent codes, or try other email addresses.
73
+
74
+ > **If you don't see Amba MCP tools available** (no `mcp__amba__*` in your tool list), Amba's MCP server isn't wired into your agent's tool surface this session. Run `npx -y @layers/amba init` via your bash tool — it provisions the account/project, writes `.env.local`, installs this skill, and wires the MCP entry into the user's agent config so the typed tools light up on the next agent launch. While signups are invite-only, add `--invite <code>` with the developer's invite code; without one, init stops and prints where to request access.
73
75
  >
74
76
  > **If your harness ALSO blocks that bash command** (a locked-down agent: no installer, no network egress, no settings edits) — do NOT try to add the MCP server yourself, run the installer another way, or edit your own settings/permissions to authorize it. Refusing to self-modify or widen your own permissions is correct; don't circumvent it. Instead, ask the developer to run **one** command in **their own** terminal (not subject to your sandbox): `npx @layers/amba init`. It creates the account with no browser and writes `AMBA_PROJECT_ID` + `AMBA_CLIENT_KEY` to `.env.local`. Ask them to run it, say when it's done, then resume from Step 0 by reading `.env.local`. Pasting an `amb_ck_…` client key is the last-resort lite path — enough to wire `Amba.configure()` + verify events land, but a client key alone can't do admin provisioning (achievements, collections, segments), which needs the PAT/server key.
75
77
  >