@layers/amba 4.1.1 → 4.1.3

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.
@@ -23,6 +23,26 @@ export interface ProjectSummary {
23
23
  google_oauth_client_id?: string | null;
24
24
  status?: string;
25
25
  created_at?: string;
26
+ /** Persisted subscription/accounting tier. */
27
+ tier?: 'free' | 'pro' | 'scale' | 'enterprise';
28
+ /** Runtime entitlement tier; differs from `tier` for comped projects. */
29
+ effective_tier?: 'free' | 'pro' | 'scale' | 'enterprise';
30
+ comped?: boolean;
31
+ }
32
+ export interface CreatedProject extends ProjectSummary {
33
+ api_keys?: {
34
+ client: {
35
+ key: string;
36
+ prefix?: string;
37
+ type?: 'client';
38
+ };
39
+ server: {
40
+ key: string;
41
+ prefix?: string;
42
+ type?: 'server';
43
+ };
44
+ };
45
+ provisioning_status?: string;
26
46
  }
27
47
  export declare function listProjects(): Promise<ApiListResponse<ProjectSummary>>;
28
48
  export declare function createProject(input: {
@@ -41,12 +61,7 @@ export declare function createProject(input: {
41
61
  * bootstrap. Defaults to `'production'` when omitted.
42
62
  */
43
63
  environment?: 'development' | 'production';
44
- }): Promise<ApiResponse<{
45
- id: string;
46
- name: string;
47
- platform: string;
48
- environment?: string;
49
- }>>;
64
+ }): Promise<ApiResponse<CreatedProject>>;
50
65
  /**
51
66
  * PATCH /admin/projects/:projectId — update mutable fields. Server
52
67
  * silently ignores unknown keys; we filter to the documented allow-list
@@ -281,6 +296,48 @@ export interface FunctionDeploymentRow {
281
296
  * function so external webhook senders can reach it. Default false. */
282
297
  is_public?: boolean;
283
298
  }
299
+ /** Provider-neutral lifecycle for an exact hostname bound to one function. */
300
+ export interface FunctionDomain {
301
+ id: string;
302
+ project_id: string;
303
+ function_name: string;
304
+ hostname: string;
305
+ cert_status: 'pending_validation' | 'pending_issuance' | 'pending_deployment' | 'active' | 'error';
306
+ ownership_status: 'pending' | 'active' | 'error';
307
+ /** True only when certificate/ownership are active and routing reconciliation succeeded. */
308
+ live: boolean;
309
+ created_at: string;
310
+ updated_at: string;
311
+ }
312
+ export interface FunctionDomainValidationRecord {
313
+ txt_name?: string;
314
+ txt_value?: string;
315
+ http_url?: string;
316
+ http_body?: string;
317
+ }
318
+ export interface FunctionDomainOwnershipVerification {
319
+ type?: string;
320
+ name?: string;
321
+ value?: string;
322
+ }
323
+ /** Attach always returns DNS instructions; refresh may add them when available. */
324
+ export interface AttachedFunctionDomain extends FunctionDomain {
325
+ dns_target: string;
326
+ /** Additive status when Amba controls the purchased domain's DNS zone. */
327
+ dns_autoconfigured?: boolean;
328
+ dns_records_published?: number;
329
+ /** True when automatic DNS publication failed and refresh should be retried. */
330
+ dns_error?: boolean;
331
+ ssl_validation: FunctionDomainValidationRecord[];
332
+ ownership_verification: FunctionDomainOwnershipVerification | null;
333
+ ownership_verification_http: {
334
+ http_url?: string;
335
+ http_body?: string;
336
+ } | null;
337
+ }
338
+ export type RefreshedFunctionDomain = FunctionDomain & Partial<Pick<AttachedFunctionDomain, 'dns_target' | 'dns_autoconfigured' | 'dns_records_published' | 'dns_error' | 'ssl_validation' | 'ownership_verification' | 'ownership_verification_http'>> & {
339
+ cleanup_pending?: boolean;
340
+ };
284
341
  export declare function recordFunctionDeployment(projectId: string, input: {
285
342
  name: string;
286
343
  runtime_script_name: string;
@@ -324,6 +381,17 @@ export declare function deployFunctionViaApi(projectId: string, input: {
324
381
  export declare function listFunctionDeployments(projectId: string, options?: {
325
382
  activeOnly?: boolean;
326
383
  }): Promise<ApiListResponse<FunctionDeploymentRow>>;
384
+ /** Attach one exact hostname to one deployed function. */
385
+ export declare function attachFunctionDomainViaApi(projectId: string, functionName: string, hostname: string): Promise<ApiResponse<AttachedFunctionDomain>>;
386
+ /** List provider-neutral ownership/certificate status for a function's hostnames. */
387
+ export declare function listFunctionDomainsViaApi(projectId: string, functionName: string): Promise<ApiListResponse<FunctionDomain>>;
388
+ /** Re-poll DNS ownership and certificate state for one exact hostname. */
389
+ export declare function refreshFunctionDomainViaApi(projectId: string, functionName: string, hostname: string): Promise<ApiResponse<RefreshedFunctionDomain>>;
390
+ /** Detach one exact hostname from a function. */
391
+ export declare function removeFunctionDomainViaApi(projectId: string, functionName: string, hostname: string): Promise<ApiResponse<{
392
+ hostname: string;
393
+ deleted: true;
394
+ }>>;
327
395
  export interface FunctionDeployContext {
328
396
  /** Dispatch namespace for the script PUT (e.g. `amba_tenant_functions`). */
329
397
  dispatch_namespace: string;
@@ -335,6 +403,8 @@ export interface FunctionDeployContext {
335
403
  * step skips the storage binding in that case.
336
404
  */
337
405
  r2_bucket_name: string | null;
406
+ /** Per-project key prefix for shared storage; empty for a dedicated bucket. */
407
+ r2_path_prefix: string | null;
338
408
  }
339
409
  export declare function getFunctionDispatchNamespace(projectId: string): Promise<ApiResponse<FunctionDeployContext>>;
340
410
  export interface InternalCredentialsResponse {
@@ -382,6 +452,11 @@ export interface FunctionLogEvent {
382
452
  export interface FunctionLogsResponse {
383
453
  events: FunctionLogEvent[];
384
454
  truncated: boolean;
455
+ /** Provider-neutral freshness for the underlying log-ingestion stream. */
456
+ ingestion?: {
457
+ status: 'current' | 'delayed' | 'no_batches' | 'unknown' | 'historical';
458
+ latest_batch_at: string | null;
459
+ };
385
460
  since: string;
386
461
  until: string;
387
462
  /** Present when `since_request_id` anchored the range — the anchor event's timestamp. */
@@ -419,7 +494,7 @@ export interface SecretSyncPendingRow {
419
494
  function_name: string | null;
420
495
  secret_name: string;
421
496
  expected_version: number;
422
- status: 'pending' | 'syncing' | 'sync_failed_retrying' | 'synced';
497
+ status: 'pending' | 'syncing' | 'sync_failed_retrying' | 'synced' | 'revocation_pending' | 'revoking' | 'revoke_failed_retrying';
423
498
  last_error: string | null;
424
499
  attempts: number;
425
500
  created_at: string;
@@ -441,7 +516,7 @@ export interface SetSecretResultRow {
441
516
  /** Backend version resource path — exposed for ops debugging. */
442
517
  version_path: string;
443
518
  /** `'pending'` until the secret sync completes. */
444
- sync_status: 'pending' | 'syncing' | 'sync_failed_retrying' | 'synced';
519
+ sync_status: 'pending';
445
520
  created_at: string;
446
521
  }
447
522
  export declare function setSecretViaApi(projectId: string, input: {
@@ -463,7 +538,7 @@ export interface ListSecretsRow {
463
538
  name: string;
464
539
  /** Latest secret-store version number. */
465
540
  version: number;
466
- sync_status: 'pending' | 'syncing' | 'sync_failed_retrying' | 'synced';
541
+ sync_status: 'pending' | 'syncing' | 'sync_failed_retrying' | 'synced' | 'revocation_pending' | 'revoking' | 'revoke_failed_retrying';
467
542
  attempts: number;
468
543
  last_error: string | null;
469
544
  updated_at: string;
@@ -473,11 +548,7 @@ export declare function listSecretsViaApi(projectId: string, options?: {
473
548
  }): Promise<ApiListResponse<ListSecretsRow>>;
474
549
  export declare function deleteSecretViaApi(projectId: string, name: string, options?: {
475
550
  function?: string;
476
- }): Promise<ApiResponse<{
477
- name: string;
478
- function: string | null;
479
- deleted: true;
480
- }>>;
551
+ }): Promise<void>;
481
552
  /** Closed set of column types the server accepts. */
482
553
  export type CollectionColumnType = 'uuid' | 'text' | 'integer' | 'bigint' | 'numeric' | 'boolean' | 'timestamptz' | 'date' | 'jsonb' | 'vector' | 'text[]' | 'integer[]' | 'bigint[]' | 'numeric[]' | 'boolean[]' | 'uuid[]';
483
554
  export interface CollectionColumn {
@@ -680,6 +751,16 @@ export declare function removeSiteDomainViaApi(projectId: string, siteName: stri
680
751
  hostname: string;
681
752
  deleted: true;
682
753
  }>>;
754
+ /** Force the provider lifecycle to be re-read and persist both site-domain states. */
755
+ export declare function refreshSiteDomainViaApi(projectId: string, siteName: string, hostname: string): Promise<ApiResponse<SiteDomainRow & {
756
+ ssl_validation: DomainValidationRecord[];
757
+ ownership_verification: DomainOwnershipVerification | null;
758
+ ownership_verification_http: {
759
+ http_url?: string;
760
+ http_body?: string;
761
+ } | null;
762
+ note?: string;
763
+ }>>;
683
764
  /**
684
765
  * Roll a live deployment back to a prior `deployment_id`. Rollback creates
685
766
  * a NEW deployment that serves the prior bundle (git-revert semantics,
package/dist/bundle.d.ts CHANGED
@@ -76,8 +76,8 @@ export declare class BundleSizeError extends Error {
76
76
  export declare const HANDLER_SHAPE_FIX_MESSAGE: string;
77
77
  /** Message used when the entry file has no `export default` at all. */
78
78
  export declare const HANDLER_SHAPE_MISSING_DEFAULT_MESSAGE: string;
79
- /** Message used when the default export is present but has no `fetch` method. */
80
- export declare const HANDLER_SHAPE_NO_FETCH_MESSAGE: string;
79
+ /** Back-compatible export name for the no-supported-handler message. */
80
+ export declare const HANDLER_SHAPE_NO_FETCH_MESSAGE = "Function entrypoint must have a callable `fetch` or `scheduled` method on its default export.";
81
81
  /**
82
82
  * Thrown when the bundled entry's `export default` is not the Module Worker
83
83
  * shape (`{ fetch(req, env, ctx) }`). Raised BEFORE any upload so a wrong
@@ -0,0 +1,5 @@
1
+ /** `amba functions domains ...` exact-hostname lifecycle commands. */
2
+ export declare function functionDomainsAttachCommand(functionName: string, hostnameInput: string): Promise<void>;
3
+ export declare function functionDomainsListCommand(functionName: string): Promise<void>;
4
+ export declare function functionDomainsRefreshCommand(functionName: string, hostnameInput: string): Promise<void>;
5
+ export declare function functionDomainsRemoveCommand(functionName: string, hostnameInput: string): Promise<void>;
@@ -41,6 +41,9 @@ export interface SitesDomainAddOptions {
41
41
  }
42
42
  export declare function sitesDomainAddCommand(hostname: string, options: SitesDomainAddOptions): Promise<void>;
43
43
  export declare function sitesDomainListCommand(siteName: string): Promise<void>;
44
+ export declare function sitesDomainRefreshCommand(hostname: string, options: {
45
+ site: string;
46
+ }): Promise<void>;
44
47
  export declare function sitesDomainRemoveCommand(hostname: string, options: {
45
48
  site: string;
46
49
  zoneId?: string;
package/dist/index.js CHANGED
@@ -864,6 +864,7 @@ async function request(method, path, body) {
864
864
  } catch {}
865
865
  throw new ApiClientError(errorMessage, res.status, errorCode);
866
866
  }
867
+ if (res.status === 204) return void 0;
867
868
  return await res.json();
868
869
  }
869
870
  /**
@@ -880,7 +881,13 @@ async function listProjects() {
880
881
  return request("GET", "/projects");
881
882
  }
882
883
  async function createProject(input) {
883
- return request("POST", "/projects", input);
884
+ const response = await request("POST", "/projects", input);
885
+ if ("project" in response.data) return { data: {
886
+ ...response.data.project,
887
+ api_keys: response.data.api_keys,
888
+ provisioning_status: response.data.provisioning_status
889
+ } };
890
+ return response;
884
891
  }
885
892
  /**
886
893
  * PATCH /admin/projects/:projectId — update mutable fields. Server
@@ -1019,6 +1026,25 @@ async function deployFunctionViaApi(projectId, input) {
1019
1026
  async function listFunctionDeployments(projectId, options = {}) {
1020
1027
  return request("GET", `/projects/${projectId}/functions/deployments${options.activeOnly ? "?active=1" : ""}`);
1021
1028
  }
1029
+ function functionDomainsPath(projectId, functionName) {
1030
+ return `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionName)}/domains`;
1031
+ }
1032
+ /** Attach one exact hostname to one deployed function. */
1033
+ async function attachFunctionDomainViaApi(projectId, functionName, hostname) {
1034
+ return request("POST", functionDomainsPath(projectId, functionName), { hostname });
1035
+ }
1036
+ /** List provider-neutral ownership/certificate status for a function's hostnames. */
1037
+ async function listFunctionDomainsViaApi(projectId, functionName) {
1038
+ return request("GET", functionDomainsPath(projectId, functionName));
1039
+ }
1040
+ /** Re-poll DNS ownership and certificate state for one exact hostname. */
1041
+ async function refreshFunctionDomainViaApi(projectId, functionName, hostname) {
1042
+ return request("POST", `${functionDomainsPath(projectId, functionName)}/${encodeURIComponent(hostname)}/refresh`);
1043
+ }
1044
+ /** Detach one exact hostname from a function. */
1045
+ async function removeFunctionDomainViaApi(projectId, functionName, hostname) {
1046
+ return request("DELETE", `${functionDomainsPath(projectId, functionName)}/${encodeURIComponent(hostname)}`);
1047
+ }
1022
1048
  async function scheduleFunction(projectId, input) {
1023
1049
  return request("POST", `/projects/${projectId}/functions/schedules`, input);
1024
1050
  }
@@ -1111,6 +1137,10 @@ async function addSiteDomainViaApi(projectId, siteName, hostname) {
1111
1137
  async function removeSiteDomainViaApi(projectId, siteName, hostname) {
1112
1138
  return request("DELETE", `/projects/${projectId}/sites/${encodeURIComponent(siteName)}/domains/${encodeURIComponent(hostname)}`);
1113
1139
  }
1140
+ /** Force the provider lifecycle to be re-read and persist both site-domain states. */
1141
+ async function refreshSiteDomainViaApi(projectId, siteName, hostname) {
1142
+ return request("POST", `/projects/${projectId}/sites/${encodeURIComponent(siteName)}/domains/${encodeURIComponent(hostname.toLowerCase())}/refresh-cert`);
1143
+ }
1114
1144
  /**
1115
1145
  * Roll a live deployment back to a prior `deployment_id`. Rollback creates
1116
1146
  * a NEW deployment that serves the prior bundle (git-revert semantics,
@@ -2002,7 +2032,11 @@ async function ensureProjectForCwd(cwd, options) {
2002
2032
  name: uniqueName,
2003
2033
  environment
2004
2034
  });
2005
- const { clientKey, serverKey } = await mintProjectKeyPair(project.data.id, environment);
2035
+ const initialKeys = project.data.api_keys;
2036
+ const { clientKey, serverKey } = initialKeys ? {
2037
+ clientKey: initialKeys.client.key,
2038
+ serverKey: initialKeys.server.key
2039
+ } : await mintProjectKeyPair(project.data.id, environment);
2006
2040
  const creds = {
2007
2041
  version: 1,
2008
2042
  project_id: project.data.id,
@@ -2924,7 +2958,7 @@ That's the entire setup. The CLI:
2924
2958
  detects on disk — Claude Code, Cursor, Windsurf.
2925
2959
  6. Verifies the PAT against the API and confirms it's good.
2926
2960
 
2927
- The Amba MCP toolset (\`amba_*\` tools — ~130 of them) is available to
2961
+ The Amba MCP toolset (\`amba_*\` tools — 500+ of them) is available to
2928
2962
  the agent immediately: pass the freshly-minted \`pat\` as an inline
2929
2963
  argument on every \`amba_*\` call in the current session. The next time
2930
2964
  your MCP client starts it picks the PAT up from the config as the
@@ -3874,6 +3908,7 @@ async function initCommand(options = {}) {
3874
3908
  const defaultProjectName = basename(cwd) || "amba-project";
3875
3909
  let projectId;
3876
3910
  let projectName;
3911
+ let createdKeys = null;
3877
3912
  if (linkedProject) {
3878
3913
  projectId = linkedProject.project_id;
3879
3914
  projectName = linkedProject.project_name;
@@ -3910,20 +3945,30 @@ async function initCommand(options = {}) {
3910
3945
  } else {
3911
3946
  const name = await prompt(` Project name (default: ${defaultProjectName}): `);
3912
3947
  const finalName = name.length > 0 ? name : defaultProjectName;
3913
- projectId = (await createProject({
3948
+ const res = await createProject({
3914
3949
  name: finalName,
3915
3950
  environment
3916
- })).data.id;
3951
+ });
3952
+ projectId = res.data.id;
3917
3953
  projectName = finalName;
3954
+ if (res.data.api_keys) createdKeys = {
3955
+ clientKey: res.data.api_keys.client.key,
3956
+ serverKey: res.data.api_keys.server.key
3957
+ };
3918
3958
  }
3919
3959
  } else {
3920
3960
  const name = await prompt(` Project name (default: ${defaultProjectName}): `);
3921
3961
  const finalName = name.length > 0 ? name : defaultProjectName;
3922
- projectId = (await createProject({
3962
+ const res = await createProject({
3923
3963
  name: finalName,
3924
3964
  environment
3925
- })).data.id;
3965
+ });
3966
+ projectId = res.data.id;
3926
3967
  projectName = finalName;
3968
+ if (res.data.api_keys) createdKeys = {
3969
+ clientKey: res.data.api_keys.client.key,
3970
+ serverKey: res.data.api_keys.server.key
3971
+ };
3927
3972
  }
3928
3973
  }
3929
3974
  if (spinner.stopped) spinner = createSpinner("minting keys");
@@ -3935,6 +3980,9 @@ async function initCommand(options = {}) {
3935
3980
  clientKey = linkedProject.client_key;
3936
3981
  if (linkedProject.server_key) serverKey = linkedProject.server_key;
3937
3982
  else serverKey = (await createApiKey(projectId, "server", environment)).data.key;
3983
+ } else if (createdKeys) {
3984
+ clientKey = createdKeys.clientKey;
3985
+ serverKey = createdKeys.serverKey;
3938
3986
  } else {
3939
3987
  const clientRes = await createApiKey(projectId, "client", environment);
3940
3988
  const serverRes = await createApiKey(projectId, "server", environment);
@@ -5435,7 +5483,7 @@ async function loadProjectConfig(cwd = process.cwd()) {
5435
5483
  if (!apiUrl) apiUrl = parsed["AMBA_API_URL"];
5436
5484
  if (projectId && apiUrl) break;
5437
5485
  }
5438
- if (!projectId) throw new Error(`AMBA_PROJECT_ID not found. Run ${pc.cyan("amba init")} or set it in .env.local.`);
5486
+ if (!projectId) throw new Error(`AMBA_PROJECT_ID not found. Export it, pass --project where supported, or run ${pc.cyan("amba init")} in this directory. The CLI reads .env.local/.env from the current working directory and does not search parent directories.`);
5439
5487
  return {
5440
5488
  projectId,
5441
5489
  apiUrl: apiUrl ?? "https://api.amba.dev"
@@ -5562,11 +5610,11 @@ var BundleSizeError = class extends Error {
5562
5610
  * service-worker shape (`export default async function (req) { … }`) is
5563
5611
  * rejected by the runtime at upload, so we reject it before the round-trip.
5564
5612
  */
5565
- const HANDLER_SHAPE_FIX_MESSAGE = "Function entrypoint must `export default { async fetch(req, env, ctx) { … } }` (the Module Worker shape). Found a default-exported function instead.";
5613
+ const HANDLER_SHAPE_FIX_MESSAGE = "Function entrypoint must `export default { async fetch(req, env, ctx) { … } }` or `export default { async scheduled(controller, env, ctx) { … } }` (the Module Worker shape). Found a default-exported function instead.";
5566
5614
  /** Message used when the entry file has no `export default` at all. */
5567
- const HANDLER_SHAPE_MISSING_DEFAULT_MESSAGE = "Function entrypoint must `export default { async fetch(req, env, ctx) { … } }` (the Module Worker shape). No default export was found.";
5568
- /** Message used when the default export is present but has no `fetch` method. */
5569
- const HANDLER_SHAPE_NO_FETCH_MESSAGE = "Function entrypoint must `export default { async fetch(req, env, ctx) { … } }` (the Module Worker shape). The default export has no `fetch` method.";
5615
+ const HANDLER_SHAPE_MISSING_DEFAULT_MESSAGE = "Function entrypoint must `export default { async fetch(req, env, ctx) { … } }` or `export default { async scheduled(controller, env, ctx) { … } }` (the Module Worker shape). No default export was found.";
5616
+ /** Back-compatible export name for the no-supported-handler message. */
5617
+ const HANDLER_SHAPE_NO_FETCH_MESSAGE = "Function entrypoint must have a callable `fetch` or `scheduled` method on its default export.";
5570
5618
  /**
5571
5619
  * Thrown when the bundled entry's `export default` is not the Module Worker
5572
5620
  * shape (`{ fetch(req, env, ctx) }`). Raised BEFORE any upload so a wrong
@@ -5600,11 +5648,14 @@ function assertHandlerShape(mod) {
5600
5648
  const def = mod.default;
5601
5649
  if (def === void 0 || def === null) throw new HandlerShapeError(HANDLER_SHAPE_MISSING_DEFAULT_MESSAGE, "missing-default");
5602
5650
  if (typeof def === "function") {
5603
- if (typeof def.fetch !== "function") throw new HandlerShapeError(HANDLER_SHAPE_FIX_MESSAGE, "plain-function");
5651
+ const fetchOnFn = def.fetch;
5652
+ const scheduledOnFn = def.scheduled;
5653
+ if (typeof fetchOnFn !== "function" && typeof scheduledOnFn !== "function") throw new HandlerShapeError(HANDLER_SHAPE_FIX_MESSAGE, "plain-function");
5604
5654
  return;
5605
5655
  }
5606
5656
  if (typeof def !== "object") throw new HandlerShapeError(HANDLER_SHAPE_NO_FETCH_MESSAGE, "no-fetch");
5607
- if (typeof def.fetch !== "function") throw new HandlerShapeError(HANDLER_SHAPE_NO_FETCH_MESSAGE, "no-fetch");
5657
+ const handler = def;
5658
+ if (typeof handler.fetch !== "function" && typeof handler.scheduled !== "function") throw new HandlerShapeError(HANDLER_SHAPE_NO_FETCH_MESSAGE, "no-fetch");
5608
5659
  }
5609
5660
  /**
5610
5661
  * Validate that the bundled customer code exports the Module Worker handler
@@ -5727,7 +5778,7 @@ async function functionsDeployCommand(target, options = {}) {
5727
5778
  const entryPoint = await resolveFunctionEntry(target, options.entry);
5728
5779
  const defaultName = targetIsDir ? basename(target.replace(/[/\\]+$/, "")) : basename(entryPoint).replace(/\.[^.]+$/, "");
5729
5780
  const functionName = options.name ?? defaultName;
5730
- validateFunctionName(functionName);
5781
+ validateFunctionName$1(functionName);
5731
5782
  const rateLimit = parseRateLimitFlags(options);
5732
5783
  console.log();
5733
5784
  console.log(pc.bold(` amba functions deploy ${pc.cyan(functionName)}`));
@@ -5775,7 +5826,7 @@ async function functionsListCommand() {
5775
5826
  console.log();
5776
5827
  }
5777
5828
  async function functionsDeleteCommand(name, options = {}) {
5778
- validateFunctionName(name);
5829
+ validateFunctionName$1(name);
5779
5830
  if (!options.confirm || options.confirm !== name) throw new Error(`Delete is destructive. Pass --confirm ${name} to proceed. Clients calling this function will start 404'ing immediately.`);
5780
5831
  const cascade = (await deleteFunctionViaApi((await loadProjectConfig()).projectId, name, { confirm: name })).data.cascade;
5781
5832
  console.log(pc.green(" ✓") + ` Deleted ${pc.bold(name)}.`);
@@ -5783,7 +5834,7 @@ async function functionsDeleteCommand(name, options = {}) {
5783
5834
  }
5784
5835
  async function functionsScheduleCommand(name, cron, options = {}) {
5785
5836
  const projectConfig = await loadProjectConfig();
5786
- validateFunctionName(name);
5837
+ validateFunctionName$1(name);
5787
5838
  const timezone = options.tz ?? "UTC";
5788
5839
  const res = await scheduleFunction(projectConfig.projectId, {
5789
5840
  name,
@@ -6115,6 +6166,14 @@ try {
6115
6166
  };
6116
6167
  return fetchFn(req, env, ctx);
6117
6168
  };
6169
+ } else if (mod.default && typeof mod.default.scheduled === 'function') {
6170
+ // Scheduled-only modules are valid in production. This command exposes
6171
+ // an HTTP server, so return a clear 405 instead of rejecting a deployable
6172
+ // handler shape during local startup.
6173
+ handler = () => new Response(
6174
+ 'This function only exports scheduled(); use amba functions schedule/run-now to invoke it.',
6175
+ { status: 405, headers: { 'content-type': 'text/plain; charset=utf-8' } },
6176
+ );
6118
6177
  } else if (typeof mod.default === 'function') {
6119
6178
  // Legacy service-worker shape (\`export default async function (req) { ... }\`).
6120
6179
  // REJECTED — production runs functions as Module Workers and the runtime
@@ -6124,7 +6183,7 @@ try {
6124
6183
  process.stderr.write(' Error: Function entrypoint must \`export default { async fetch(req, env, ctx) { … } }\` (the Module Worker shape). Found a default-exported function instead.\\n');
6125
6184
  process.exit(2);
6126
6185
  } else {
6127
- process.stderr.write(' Error: Function entrypoint must \`export default { async fetch(req, env, ctx) { … } }\` (the Module Worker shape). The default export has no \`fetch\` method.\\n');
6186
+ process.stderr.write(' Error: Function entrypoint default export must have a callable \`fetch\` or \`scheduled\` method.\\n');
6128
6187
  process.exit(2);
6129
6188
  }
6130
6189
  } catch (err) {
@@ -6194,8 +6253,8 @@ process.on('SIGINT', () => shutdown('SIGINT'));
6194
6253
  * function-name overwrites — same upsert semantics as `amba secrets set`.
6195
6254
  */
6196
6255
  async function functionsConsumeCommand(queueName, functionName, options = {}) {
6197
- validateFunctionName(functionName);
6198
- validateFunctionName(queueName);
6256
+ validateFunctionName$1(functionName);
6257
+ validateFunctionName$1(queueName);
6199
6258
  const res = await upsertQueueBinding((await loadProjectConfig()).projectId, {
6200
6259
  queue_name: queueName,
6201
6260
  function_name: functionName,
@@ -6225,11 +6284,11 @@ async function functionsConsumersListCommand() {
6225
6284
  console.log();
6226
6285
  }
6227
6286
  async function functionsConsumersUnbindCommand(queueName) {
6228
- validateFunctionName(queueName);
6287
+ validateFunctionName$1(queueName);
6229
6288
  await deleteQueueBinding((await loadProjectConfig()).projectId, queueName);
6230
6289
  console.log(pc.green(" ✓") + ` Unbound queue ${pc.cyan(queueName)}.`);
6231
6290
  }
6232
- function validateFunctionName(name) {
6291
+ function validateFunctionName$1(name) {
6233
6292
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Invalid function name '${name}'. Must match /^[a-z][a-z0-9_-]*$/ (lowercase ASCII, digits, underscore or hyphen; must start with a letter).`);
6234
6293
  if (name.length > 58) throw new Error("Function name must be 58 characters or fewer.");
6235
6294
  }
@@ -6302,6 +6361,10 @@ async function functionsLogsCommand(functionName, options = {}) {
6302
6361
  });
6303
6362
  if (res.data.anchor && !options.json) console.error(pc.dim(` Anchored at ${res.data.anchor} (--since-request-id).`));
6304
6363
  printEvents(res.data.events, options.json);
6364
+ if (res.data.events.length === 0 && !options.json) {
6365
+ if (res.data.ingestion?.status === "delayed") console.error(pc.yellow(` ! Log ingestion is delayed${res.data.ingestion.latest_batch_at ? `; latest batch arrived ${res.data.ingestion.latest_batch_at}` : ""}. Retry shortly.`));
6366
+ else if (res.data.ingestion?.status === "no_batches") console.error(pc.yellow(" ! No log batches reached this window yet. Retry shortly."));
6367
+ }
6305
6368
  if (res.data.truncated) if (options.json) process.stderr.write(`{"truncated":true}\n`);
6306
6369
  else console.error(pc.yellow(` ! Result truncated at limit; narrow --since / --until or raise --limit.`));
6307
6370
  }
@@ -6385,6 +6448,85 @@ function sleep$1(ms) {
6385
6448
  return new Promise((r) => setTimeout(r, ms));
6386
6449
  }
6387
6450
  //#endregion
6451
+ //#region src/commands/function-domains.ts
6452
+ /** `amba functions domains ...` exact-hostname lifecycle commands. */
6453
+ const FUNCTION_NAME_RE = /^[a-z][a-z0-9_-]{0,57}$/;
6454
+ function validateFunctionName(name) {
6455
+ if (!FUNCTION_NAME_RE.test(name)) throw new Error(`Invalid function name "${name}". Use lowercase letters, numbers, hyphens, or underscores (max 58 chars; start with a letter).`);
6456
+ }
6457
+ /**
6458
+ * Normalize case/trailing-dot spelling without trying to replace the API's
6459
+ * authoritative DNS validation. This command accepts one exact hostname only:
6460
+ * schemes, paths, ports, and wildcard labels are never meaningful here.
6461
+ */
6462
+ function normalizeExactHostname(input) {
6463
+ const hostname = input.trim().toLowerCase().replace(/\.$/, "");
6464
+ if (hostname.length === 0 || hostname.length > 253 || !hostname.includes(".") || hostname.includes("*") || hostname.includes("/") || hostname.includes(":") || /\s/.test(hostname)) throw new Error(`Invalid hostname "${input}". Pass one exact DNS hostname such as feeds.example.com (no scheme, path, port, or wildcard).`);
6465
+ return hostname;
6466
+ }
6467
+ function lifecycle(domain) {
6468
+ if (domain.live) return pc.green("active");
6469
+ if (domain.cert_status === "active" && domain.ownership_status === "active") return "certificate=active, ownership=active, reconciliation=pending";
6470
+ return `certificate=${domain.cert_status}, ownership=${domain.ownership_status}`;
6471
+ }
6472
+ function printDomain(domain) {
6473
+ console.log(` ${pc.bold(domain.hostname)} ${lifecycle(domain)}`);
6474
+ }
6475
+ function printDnsInstructions(domain) {
6476
+ if (domain.dns_error === true) console.log(pc.yellow(" Automatic DNS publication failed; retry with domains refresh."));
6477
+ if ("cleanup_pending" in domain && domain.cleanup_pending === true) console.log(pc.yellow(" Routing cleanup is incomplete; retry with domains refresh."));
6478
+ if ("dns_autoconfigured" in domain && domain.dns_autoconfigured === true) console.log(pc.dim(` DNS records published automatically${domain.dns_records_published === void 0 ? "" : ` (${domain.dns_records_published})`}.`));
6479
+ if (domain.dns_target) console.log(pc.dim(` DNS target: CNAME ${domain.hostname} → ${domain.dns_target}`));
6480
+ for (const record of domain.ssl_validation ?? []) if (record.txt_name && record.txt_value) console.log(pc.dim(` Certificate TXT: ${record.txt_name} = ${record.txt_value}`));
6481
+ else if (record.http_url && record.http_body) console.log(pc.dim(` Certificate HTTP: ${record.http_url} → ${record.http_body}`));
6482
+ const ownership = domain.ownership_verification;
6483
+ if (ownership?.name && ownership.value) console.log(pc.dim(` Ownership ${ownership.type ?? "TXT"}: ${ownership.name} = ${ownership.value}`));
6484
+ const ownershipHttp = domain.ownership_verification_http;
6485
+ if (ownershipHttp?.http_url && ownershipHttp.http_body) console.log(pc.dim(` Ownership HTTP: ${ownershipHttp.http_url} → ${ownershipHttp.http_body}`));
6486
+ }
6487
+ async function functionDomainsAttachCommand(functionName, hostnameInput) {
6488
+ validateFunctionName(functionName);
6489
+ const hostname = normalizeExactHostname(hostnameInput);
6490
+ const { projectId } = await loadProjectConfig();
6491
+ const result = await attachFunctionDomainViaApi(projectId, functionName, hostname);
6492
+ console.log();
6493
+ console.log(pc.green(" ✓") + ` Attached ${pc.bold(result.data.hostname)} to ${functionName}.`);
6494
+ console.log(pc.dim(` Status: ${lifecycle(result.data)}`));
6495
+ printDnsInstructions(result.data);
6496
+ console.log(pc.dim(" Exact hostname only; no automatic www hostname was created."));
6497
+ if (!result.data.live && result.data.ownership_status !== "active") console.log(pc.dim(" Unverified claims become eligible for reclaim after 24 hours."));
6498
+ console.log();
6499
+ }
6500
+ async function functionDomainsListCommand(functionName) {
6501
+ validateFunctionName(functionName);
6502
+ const { projectId } = await loadProjectConfig();
6503
+ const result = await listFunctionDomainsViaApi(projectId, functionName);
6504
+ console.log();
6505
+ if (result.data.length === 0) console.log(pc.dim(` No custom domains attached to ${functionName}.`));
6506
+ else for (const domain of result.data) printDomain(domain);
6507
+ console.log();
6508
+ }
6509
+ async function functionDomainsRefreshCommand(functionName, hostnameInput) {
6510
+ validateFunctionName(functionName);
6511
+ const hostname = normalizeExactHostname(hostnameInput);
6512
+ const { projectId } = await loadProjectConfig();
6513
+ const result = await refreshFunctionDomainViaApi(projectId, functionName, hostname);
6514
+ console.log();
6515
+ console.log(pc.green(" ✓") + ` Refreshed ${pc.bold(result.data.hostname)}.`);
6516
+ console.log(pc.dim(` Status: ${lifecycle(result.data)}`));
6517
+ printDnsInstructions(result.data);
6518
+ console.log();
6519
+ }
6520
+ async function functionDomainsRemoveCommand(functionName, hostnameInput) {
6521
+ validateFunctionName(functionName);
6522
+ const hostname = normalizeExactHostname(hostnameInput);
6523
+ const { projectId } = await loadProjectConfig();
6524
+ const result = await removeFunctionDomainViaApi(projectId, functionName, hostname);
6525
+ console.log();
6526
+ console.log(pc.green(" ✓") + ` Removed ${pc.bold(result.data.hostname)} from ${functionName}.`);
6527
+ console.log();
6528
+ }
6529
+ //#endregion
6388
6530
  //#region src/commands/ai.ts
6389
6531
  /**
6390
6532
  * `amba ai providers ...` — register / list / remove AI provider API keys.
@@ -6517,8 +6659,8 @@ async function secretsListCommand(options = {}) {
6517
6659
  async function secretsUnsetCommand(name, options) {
6518
6660
  validateSecretName(name);
6519
6661
  await deleteSecretViaApi((await loadProjectConfig()).projectId, name, { function: options.function });
6520
- console.log(pc.green(" ✓") + ` Removed from GCP Secret Manager.`);
6521
- console.log(pc.dim(" Workers Secret on the dispatched script remains until the next deploy — redeploy to clear."));
6662
+ console.log(pc.green(" ✓") + ` Secret removed from the backing store.`);
6663
+ console.log(pc.dim(" Workers binding revocation queued; provider failures retry automatically."));
6522
6664
  }
6523
6665
  function validateSecretName(name) {
6524
6666
  const reason = getBindingReservationReason(name);
@@ -6529,6 +6671,9 @@ function renderStatus(status) {
6529
6671
  case "synced": return pc.green("synced");
6530
6672
  case "syncing": return pc.cyan("syncing");
6531
6673
  case "sync_failed_retrying": return pc.yellow("retrying");
6674
+ case "revocation_pending": return pc.yellow("revocation pending");
6675
+ case "revoking": return pc.cyan("revoking");
6676
+ case "revoke_failed_retrying": return pc.yellow("revocation retrying");
6532
6677
  default: return pc.dim(status);
6533
6678
  }
6534
6679
  }
@@ -6618,7 +6763,10 @@ async function billingStatusCommand() {
6618
6763
  try {
6619
6764
  const { projectId } = await loadProjectConfig();
6620
6765
  const s = (await adminGet(`/projects/${projectId}/billing/status`)).data;
6621
- console.log(` Tier: ${pc.bold(s.tier)}`);
6766
+ const effectiveTier = s.effective_tier ?? s.tier;
6767
+ console.log(` Effective tier: ${pc.bold(effectiveTier)}`);
6768
+ if (effectiveTier !== s.tier) console.log(` Stored tier: ${s.tier}`);
6769
+ if (s.comped === true) console.log(` Billing: ${pc.green("comped")} ${pc.dim("(no customer charge)")}`);
6622
6770
  console.log(` Subscription: ${s.subscription_status ?? pc.dim("—")}`);
6623
6771
  console.log(` Next bill anchor: ${s.current_period_end ? new Date(s.current_period_end).toLocaleDateString() : pc.dim("—")}`);
6624
6772
  const pctCeiling = s.ceiling_pct_consumed === null || s.ceiling_pct_consumed === void 0 ? null : Math.round(s.ceiling_pct_consumed * 100);
@@ -8929,6 +9077,15 @@ async function sitesDomainListCommand(siteName) {
8929
9077
  for (const d of res.data) console.log(` ${pc.bold(d.hostname)} ${formatDomainStatus(d)}`);
8930
9078
  console.log();
8931
9079
  }
9080
+ async function sitesDomainRefreshCommand(hostname, options) {
9081
+ validateSiteName(options.site);
9082
+ validateHostname(hostname);
9083
+ const projectConfig = await loadProjectConfig();
9084
+ const normalisedHostname = hostname.toLowerCase();
9085
+ const res = await refreshSiteDomainViaApi(projectConfig.projectId, options.site, normalisedHostname);
9086
+ console.log(` ${pc.bold(res.data.hostname)} ${formatDomainStatus(res.data)}`);
9087
+ if (res.data.note) console.log(pc.dim(` ${res.data.note}`));
9088
+ }
8932
9089
  async function sitesDomainRemoveCommand(hostname, options) {
8933
9090
  validateSiteName(options.site);
8934
9091
  validateHostname(hostname);
@@ -9530,7 +9687,7 @@ async function diffConfigCommand(file = DEFAULT_BUNDLE_FILE, options = {}) {
9530
9687
  //#endregion
9531
9688
  //#region src/index.ts
9532
9689
  const program = new Command();
9533
- program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.1.1");
9690
+ program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.1.3");
9534
9691
  program.option("--token <pat>", "Use a Personal Access Token for headless / CI / agent use (overrides ~/.amba/credentials.json + AMBA_PAT env)");
9535
9692
  program.hook("preAction", (thisCommand) => {
9536
9693
  setBearerOverride(resolveTokenSource({
@@ -9678,8 +9835,8 @@ functions.command("deploy <target>").description("Bundle a function (a single fi
9678
9835
  functions.command("list").description("List active functions for the current project").action(async () => {
9679
9836
  await runAction(functionsListCommand);
9680
9837
  });
9681
- functions.command("delete <name>").description("Disable + remove a function from the dispatch namespace").action(async (name) => {
9682
- await runAction(() => functionsDeleteCommand(name));
9838
+ functions.command("delete <name>").description("Disable + remove a function from the dispatch namespace (destructive)").option("--confirm <name>", "Pass the function name to confirm deletion").action(async (name, opts) => {
9839
+ await runAction(() => functionsDeleteCommand(name, { confirm: opts.confirm }));
9683
9840
  });
9684
9841
  functions.command("schedule <name> <cron>").description("Register a cron schedule that invokes a deployed function").option("--tz <iana>", "IANA timezone for the schedule (default: UTC)").action(async (name, cron, opts) => {
9685
9842
  await runAction(() => functionsScheduleCommand(name, cron, opts));
@@ -9711,8 +9868,21 @@ consumers.command("list").description("List queue bindings for the current proje
9711
9868
  consumers.command("unbind <queue-name>").description("Remove the binding for a queue (DROP)").action(async (queueName) => {
9712
9869
  await runAction(() => functionsConsumersUnbindCommand(queueName));
9713
9870
  });
9871
+ const functionDomains = functions.command("domains").description("Manage exact custom hostnames for functions");
9872
+ functionDomains.command("attach <name> <hostname>").description("Attach one exact hostname to a deployed function").action(async (name, hostname) => {
9873
+ await runAction(() => functionDomainsAttachCommand(name, hostname));
9874
+ });
9875
+ functionDomains.command("list <name>").description("List custom hostname ownership and certificate status").action(async (name) => {
9876
+ await runAction(() => functionDomainsListCommand(name));
9877
+ });
9878
+ functionDomains.command("refresh <name> <hostname>").description("Refresh DNS ownership and certificate status").action(async (name, hostname) => {
9879
+ await runAction(() => functionDomainsRefreshCommand(name, hostname));
9880
+ });
9881
+ functionDomains.command("remove <name> <hostname>").description("Detach an exact hostname from a function").action(async (name, hostname) => {
9882
+ await runAction(() => functionDomainsRemoveCommand(name, hostname));
9883
+ });
9714
9884
  const secrets = program.command("secrets").description("Function secrets (GCP canonical, Workers-Secret synced)");
9715
- 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. 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) => {
9885
+ 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) => {
9716
9886
  await runAction(() => secretsSetCommand(name, value, {
9717
9887
  function: opts.function,
9718
9888
  env: opts.env ?? "dev",
@@ -9722,7 +9892,7 @@ secrets.command("set <name> [value]").description("Store a secret and queue its
9722
9892
  secrets.command("list").description("List secret sync status for the current project").option("--function <name>", "Filter to secrets for a single function (avoids listing all functions at once)").action(async (opts) => {
9723
9893
  await runAction(() => secretsListCommand({ function: opts.function }));
9724
9894
  });
9725
- secrets.command("unset <name>").description("Remove a secret (Worker binding cleared on next deploy)").option("--function <name>", "Function the secret was bound to. Omit to remove the project-wide secret of this name.").action(async (name, opts) => {
9895
+ secrets.command("unset <name>").description("Remove a secret and revoke its Worker binding").option("--function <name>", "Function the secret was bound to. Omit to remove the project-wide secret of this name. A surviving same-name value at the other scope is restored automatically.").action(async (name, opts) => {
9726
9896
  await runAction(() => secretsUnsetCommand(name, opts));
9727
9897
  });
9728
9898
  const billing = program.command("billing").description("Per-project subscription, headroom, and spend controls");
@@ -9878,6 +10048,9 @@ sitesDomain.command("add <hostname>").description("Attach a custom hostname (DV
9878
10048
  sitesDomain.command("list <site>").description("List custom hostnames attached to a site").action(async (site) => {
9879
10049
  await runAction(() => sitesDomainListCommand(site));
9880
10050
  });
10051
+ sitesDomain.command("refresh <hostname>").description("Re-check hostname ownership and certificate status").requiredOption("--site <name>", "Site name the hostname is attached to").action(async (hostname, opts) => {
10052
+ await runAction(() => sitesDomainRefreshCommand(hostname, { site: opts.site }));
10053
+ });
9881
10054
  sitesDomain.command("remove <hostname>").description("Detach a custom hostname (best-effort edge detach + control-plane row delete)").requiredOption("--site <name>", "Site name the hostname is attached to").option("--zone-id <id>", "DNS zone id (default: env AMBA_DNS_ZONE_ID)").action(async (hostname, opts) => {
9882
10055
  await runAction(() => sitesDomainRemoveCommand(hostname, {
9883
10056
  site: opts.site,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layers/amba",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
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.6",
53
- "@layers/amba-shared": "4.0.4"
52
+ "@layers/amba-shared": "4.0.4",
53
+ "@layers/amba-mcp": "4.0.7"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsdown && tsc --emitDeclarationOnly",
@@ -89,7 +89,7 @@ Check whether a project exists in this directory:
89
89
  > }'
90
90
  > ```
91
91
  > The response is the standard MCP envelope `{"jsonrpc":"2.0","id":"1","result":{"content":[{"type":"text","text":"<JSON body>"}]}}`. You construct calls by hand the same way you would with typed tools — every `amba_*` tool name from the catalog works. This is the same wire the typed-tool surface uses; you just skip the client-side configuration loop.
92
- > 2. **The `amba` CLI via bash.** Already authenticated via `~/.amba/credentials.json`. Coverage: `amba projects {list,create,update,show,delete}`, `amba collections {create,alter,list,drop}`, `amba functions {deploy,list,delete,schedule,dev,logs,consume}`, `amba push test`, `amba secrets {set,list,unset}`, `amba config {list,set}`, `amba sites <subcommands>`, `amba seed`, `amba status --detailed`, `amba logs tail`, `amba analytics export`, `amba schema export`, `amba ai <subcommands>`, `amba db migrate`, `amba billing {status,upgrade,portal,set-ceiling}`, `amba claim <email>` (sandbox → verified_free magic link). Run `amba --help` once if you need to see the full subcommand surface. Use this when you want a typed wrapper for a common op; the JSON-RPC path is strictly more flexible.
92
+ > 2. **The `amba` CLI via bash.** Already authenticated via `~/.amba/credentials.json`. Coverage: `amba projects {list,create,update,show,delete}`, `amba collections {create,alter,list,drop}`, `amba functions {deploy,list,delete,schedule,dev,logs,consume,domains}`, `amba push test`, `amba secrets {set,list,unset}`, `amba config {list,set}`, `amba sites <subcommands>`, `amba seed`, `amba status --detailed`, `amba logs tail`, `amba analytics export`, `amba schema export`, `amba ai <subcommands>`, `amba db migrate`, `amba billing {status,upgrade,portal,set-ceiling}`, `amba claim <email>` (sandbox → verified_free magic link). Run `amba --help` once if you need to see the full subcommand surface. Use this when you want a typed wrapper for a common op; the JSON-RPC path is strictly more flexible.
93
93
  > 3. **Wait for next agent launch.** The MCP entry is already persisted; on the next launch, `mcp__amba__*` tools appear automatically.
94
94
  >
95
95
  > A SEPARATE case: if Amba MCP IS wired in this session but the inbound Bearer is stale (e.g., the user just ran `amba_developer_signup` and got a fresh PAT), pass `pat: "<the new PAT>"` as an inline argument on every subsequent `amba_*` tool call to override the inbound Bearer for that one call. This only helps when the `mcp__amba__*` tools already exist in your tool list — it does not summon them into existence.
@@ -37,6 +37,9 @@ Run user code in a sandbox triggered by HTTP, cron, or webhook. The function get
37
37
  | Tool | Purpose | Example args |
38
38
  | --- | --- | --- |
39
39
  | `amba_functions_deploy` / `amba_deploy_function` | Deploy a function from source (TypeScript / JavaScript / Python). | `{ project_id, name: "send_welcome_email", runtime: "node22", source: "export default async (req) => { ... }", trigger: { type: "http" } }` |
40
+ | `amba_function_domains_attach` | Attach an exact hostname to a function and return DNS validation records. | `{ project_id, name: "feed", hostname: "feeds.example.com" }` |
41
+ | `amba_function_domains_list` / `amba_function_domains_refresh` | Read or refresh ownership/certificate status. | `{ project_id, name: "feed", hostname: "feeds.example.com" }` |
42
+ | `amba_function_domains_remove` | Detach an exact function hostname. | `{ project_id, name: "feed", hostname: "feeds.example.com" }` |
40
43
  | `amba_functions_list` / `amba_list_functions` | List functions. | `{ project_id }` |
41
44
  | `amba_functions_get` / `amba_get_function` | Read function metadata. | `{ project_id, function_id }` |
42
45
  | `amba_functions_get_logs` / `amba_get_function_logs` | Recent invocation logs. | `{ project_id, function_id, limit: 100 }` |
@@ -46,6 +49,11 @@ Run user code in a sandbox triggered by HTTP, cron, or webhook. The function get
46
49
  | `amba_functions_resume_schedule` / `amba_resume_function_schedule` | Resume. | `{ project_id, function_id }` |
47
50
  | `amba_functions_trigger_schedule` / `amba_trigger_function_schedule` | Fire a scheduled function ad-hoc (testing). | `{ project_id, function_id }` |
48
51
 
52
+ Function-domain MVP limits use effective tier: free 1, pro 5, scale 20,
53
+ enterprise/comped 50. Attach is limited to five attempts per project per hour.
54
+ Unverified claims become eligible for exact-host reclaim after 24 hours; Worker
55
+ route/KV allocation waits until provider ownership is active.
56
+
49
57
  ### AI prompts
50
58
 
51
59
  Managed LLM templates: a stored prompt with provider + model + system message, invoked by name from the SDK. The actual LLM call is rewritten server-side per-tenant — the customer's provider API key (Anthropic / OpenAI / Mistral / Gemini) stays server-side, never on the device.
@@ -80,7 +88,7 @@ Managed LLM templates: a stored prompt with provider + model + system message, i
80
88
 
81
89
  | Tool | Purpose | Example args |
82
90
  | --- | --- | --- |
83
- | `amba_secrets_set` / `amba_set_secret` | Set a **function-scoped** secret (encrypted at rest; bound on the next deploy). For third-party API keys called from your functions — NOT AI provider keys (use `amba_ai_providers_set` for those). | `{ project_id, name: "STRIPE_WEBHOOK_SECRET", value: "whsec_..." }` |
91
+ | `amba_secrets_set` / `amba_set_secret` | Set or rotate a function secret. Omit `function` for a project-wide value or pass it for one function. Already-deployed functions receive the new binding asynchronously; a future deploy also reconciles it. For third-party API keys called from your functions — NOT AI provider keys (use `amba_ai_providers_set` for those). | `{ project_id, name: "STRIPE_WEBHOOK_SECRET", value: "whsec_..." }` |
84
92
  | `amba_secrets_get` / `amba_get_secret` | Read a secret (returns `"<redacted>"` unless explicitly requested). | `{ project_id, name }` |
85
93
  | `amba_secrets_list` / `amba_list_secrets` | List secret names. | `{ project_id }` |
86
94
  | `amba_secrets_delete` / `amba_delete_secret` | Delete. | `{ project_id, name }` |