@layers/amba 4.1.2 → 4.1.4

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.
@@ -302,6 +302,12 @@ export interface FunctionDomain {
302
302
  project_id: string;
303
303
  function_name: string;
304
304
  hostname: string;
305
+ /** Required record at authoritative DNS. */
306
+ dns_record_type?: 'CNAME';
307
+ /** True for a registrable root rather than a subdomain. */
308
+ is_apex?: boolean;
309
+ /** Additive provider-neutral DNS warning, populated for apex hostnames. */
310
+ dns_note?: string | null;
305
311
  cert_status: 'pending_validation' | 'pending_issuance' | 'pending_deployment' | 'active' | 'error';
306
312
  ownership_status: 'pending' | 'active' | 'error';
307
313
  /** True only when certificate/ownership are active and routing reconciliation succeeded. */
@@ -403,6 +409,8 @@ export interface FunctionDeployContext {
403
409
  * step skips the storage binding in that case.
404
410
  */
405
411
  r2_bucket_name: string | null;
412
+ /** Per-project key prefix for shared storage; empty for a dedicated bucket. */
413
+ r2_path_prefix: string | null;
406
414
  }
407
415
  export declare function getFunctionDispatchNamespace(projectId: string): Promise<ApiResponse<FunctionDeployContext>>;
408
416
  export interface InternalCredentialsResponse {
@@ -672,6 +680,22 @@ export interface SiteDomainRow {
672
680
  provider_hostname_id: string | null;
673
681
  cert_status: 'pending_validation' | 'pending_issuance' | 'pending_deployment' | 'active' | 'error';
674
682
  ownership_status: 'pending' | 'active' | 'error';
683
+ /** Edge Worker route reconciliation. */
684
+ route_status: 'pending' | 'active' | 'error';
685
+ /** Host-to-site KV reconciliation. */
686
+ kv_status: 'pending' | 'active' | 'error';
687
+ /** Whether Amba owns and manages the authoritative DNS zone. */
688
+ dns_managed: boolean;
689
+ /** Managed-DNS reconciliation, or not_required for externally managed DNS. */
690
+ dns_status: 'not_required' | 'pending' | 'active' | 'error';
691
+ /**
692
+ * Authoritative reachability. True only when certificate, ownership,
693
+ * Worker route, KV, and (when managed) DNS are all active.
694
+ */
695
+ live: boolean;
696
+ dns_record_type?: 'CNAME';
697
+ is_apex?: boolean;
698
+ dns_note?: string | null;
675
699
  created_at: string;
676
700
  updated_at: string;
677
701
  }
@@ -729,8 +753,14 @@ export declare function addSiteDomainViaApi(projectId: string, siteName: string,
729
753
  provider_hostname_id: string;
730
754
  cert_status: SiteDomainRow["cert_status"];
731
755
  ownership_status: SiteDomainRow["ownership_status"];
756
+ route_status: SiteDomainRow["route_status"];
757
+ kv_status: SiteDomainRow["kv_status"];
758
+ dns_managed: SiteDomainRow["dns_managed"];
759
+ dns_status: SiteDomainRow["dns_status"];
760
+ live: boolean;
732
761
  dns_target: string;
733
762
  is_apex: boolean;
763
+ dns_record_type: "CNAME";
734
764
  dns_note: string | null;
735
765
  ssl_validation: DomainValidationRecord[];
736
766
  ownership_verification: DomainOwnershipVerification | null;
@@ -749,6 +779,16 @@ export declare function removeSiteDomainViaApi(projectId: string, siteName: stri
749
779
  hostname: string;
750
780
  deleted: true;
751
781
  }>>;
782
+ /** Force the provider lifecycle to be re-read and persist both site-domain states. */
783
+ export declare function refreshSiteDomainViaApi(projectId: string, siteName: string, hostname: string): Promise<ApiResponse<SiteDomainRow & {
784
+ ssl_validation: DomainValidationRecord[];
785
+ ownership_verification: DomainOwnershipVerification | null;
786
+ ownership_verification_http: {
787
+ http_url?: string;
788
+ http_body?: string;
789
+ } | null;
790
+ note?: string;
791
+ }>>;
752
792
  /**
753
793
  * Roll a live deployment back to a prior `deployment_id`. Rollback creates
754
794
  * a NEW deployment that serves the prior bundle (git-revert semantics,
@@ -797,6 +837,12 @@ export interface DomainPurchaseResult {
797
837
  provisioned?: boolean;
798
838
  cert_status?: string;
799
839
  ownership_status?: string;
840
+ route_status?: string;
841
+ kv_status?: string;
842
+ dns_managed?: boolean;
843
+ dns_status?: string;
844
+ /** Authoritative reachability across provider, route, KV, and managed DNS. */
845
+ hostname_live?: boolean;
800
846
  dns_autoconfigured?: boolean;
801
847
  url?: string;
802
848
  public_url?: string;
@@ -813,7 +859,22 @@ export declare function purchaseDomainViaApi(projectId: string, body: {
813
859
  privacy?: boolean;
814
860
  years?: number;
815
861
  }): Promise<ApiResponse<DomainPurchaseResult>>;
816
- export declare function listPurchasedDomainsViaApi(projectId: string): Promise<ApiListResponse<Record<string, unknown>>>;
862
+ export interface PurchasedDomainStatus extends Record<string, unknown> {
863
+ domain: string;
864
+ status: string;
865
+ site?: string | null;
866
+ provisioned: boolean;
867
+ cert_status?: string;
868
+ ownership_status?: string;
869
+ route_status?: string;
870
+ kv_status?: string;
871
+ dns_managed?: boolean;
872
+ dns_status?: string;
873
+ /** Reachability; deliberately separate from purchase/payment completion. */
874
+ hostname_live: boolean;
875
+ aliases: Array<Record<string, unknown>>;
876
+ }
877
+ export declare function listPurchasedDomainsViaApi(projectId: string): Promise<ApiListResponse<PurchasedDomainStatus>>;
817
878
  /**
818
879
  * Delete a function entirely. Cascade: removes the deployed script
819
880
  * (backend 404 treated as success), then marks every historical
@@ -833,10 +894,19 @@ export declare function attachSiteDomain(projectId: string, siteName: string, in
833
894
  provider_hostname_id?: string;
834
895
  }): Promise<ApiResponse<SiteDomainRow>>;
835
896
  export declare function listSiteDomains(projectId: string, siteName: string): Promise<ApiListResponse<SiteDomainRow>>;
897
+ /** @deprecated Lifecycle is provider-owned; this delegates to authoritative refresh. */
836
898
  export declare function updateSiteDomain(projectId: string, siteName: string, hostname: string, patch: {
837
899
  cert_status: SiteDomainRow['cert_status'];
838
900
  provider_hostname_id?: string;
839
- }): Promise<ApiResponse<SiteDomainRow>>;
901
+ }): Promise<ApiResponse<SiteDomainRow & {
902
+ ssl_validation: DomainValidationRecord[];
903
+ ownership_verification: DomainOwnershipVerification | null;
904
+ ownership_verification_http: {
905
+ http_url?: string;
906
+ http_body?: string;
907
+ } | null;
908
+ note?: string;
909
+ }>>;
840
910
  export declare function detachSiteDomain(projectId: string, siteName: string, hostname: string): Promise<ApiResponse<{
841
911
  deleted: boolean;
842
912
  }>>;
@@ -36,11 +36,14 @@ export interface SitesDomainAddOptions {
36
36
  zoneId?: string;
37
37
  /** Skip the cert poll loop. Caller will check status later. */
38
38
  noWait?: boolean;
39
- /** Max seconds to wait for cert_status='active' before bailing. Default 600. */
39
+ /** Max seconds to wait for both ownership and certificate activation. Default 600. */
40
40
  timeout?: number;
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;
@@ -71,6 +74,10 @@ declare function collectFiles(dir: string): Promise<SiteFile[]>;
71
74
  export declare class SitesStaticOnlyError extends Error {
72
75
  readonly code: "SITES_STATIC_ONLY";
73
76
  }
77
+ declare function isProviderTerminalDomainFailure(d: {
78
+ cert_status: string;
79
+ ownership_status: string;
80
+ }): boolean;
74
81
  /**
75
82
  * Test-only re-export of internal helpers. Tests reach in via this
76
83
  * namespace so the production module surface stays clean. Not a public
@@ -80,5 +87,6 @@ export declare const __testHelpers: {
80
87
  collectFiles: typeof collectFiles;
81
88
  BLOCKED_FILE_NAMES: Set<string>;
82
89
  BLOCKED_DIR_NAMES: Set<string>;
90
+ isProviderTerminalDomainFailure: typeof isProviderTerminalDomainFailure;
83
91
  };
84
92
  export {};
package/dist/index.js CHANGED
@@ -1137,6 +1137,10 @@ async function addSiteDomainViaApi(projectId, siteName, hostname) {
1137
1137
  async function removeSiteDomainViaApi(projectId, siteName, hostname) {
1138
1138
  return request("DELETE", `/projects/${projectId}/sites/${encodeURIComponent(siteName)}/domains/${encodeURIComponent(hostname)}`);
1139
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
+ }
1140
1144
  /**
1141
1145
  * Roll a live deployment back to a prior `deployment_id`. Rollback creates
1142
1146
  * a NEW deployment that serves the prior bundle (git-revert semantics,
@@ -5479,7 +5483,7 @@ async function loadProjectConfig(cwd = process.cwd()) {
5479
5483
  if (!apiUrl) apiUrl = parsed["AMBA_API_URL"];
5480
5484
  if (projectId && apiUrl) break;
5481
5485
  }
5482
- 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.`);
5483
5487
  return {
5484
5488
  projectId,
5485
5489
  apiUrl: apiUrl ?? "https://api.amba.dev"
@@ -6473,6 +6477,7 @@ function printDnsInstructions(domain) {
6473
6477
  if ("cleanup_pending" in domain && domain.cleanup_pending === true) console.log(pc.yellow(" Routing cleanup is incomplete; retry with domains refresh."));
6474
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})`}.`));
6475
6479
  if (domain.dns_target) console.log(pc.dim(` DNS target: CNAME ${domain.hostname} → ${domain.dns_target}`));
6480
+ if (domain.dns_note) console.log(pc.yellow(` DNS requirement: ${domain.dns_note}`));
6476
6481
  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}`));
6477
6482
  else if (record.http_url && record.http_body) console.log(pc.dim(` Certificate HTTP: ${record.http_url} → ${record.http_body}`));
6478
6483
  const ownership = domain.ownership_verification;
@@ -9017,7 +9022,6 @@ async function sitesDomainAddCommand(hostname, options) {
9017
9022
  console.log(pc.green(" ✓") + ` Custom hostname registered`);
9018
9023
  const validation = res.data.ssl_validation ?? [];
9019
9024
  const ownership = res.data.ownership_verification;
9020
- const isApex = res.data.is_apex ?? false;
9021
9025
  if (validation.length > 0 || ownership) {
9022
9026
  console.log();
9023
9027
  console.log(pc.dim(" 1. Publish these DNS records to verify ownership:"));
@@ -9031,10 +9035,8 @@ async function sitesDomainAddCommand(hostname, options) {
9031
9035
  }
9032
9036
  console.log(` ${pc.bold("CNAME")} ${hostname} → ${pc.cyan(res.data.dns_target)}`);
9033
9037
  console.log();
9034
- if (isApex) {
9035
- console.log(pc.dim(` ${hostname} is a root domain — the CNAME above works only if your DNS provider`));
9036
- console.log(pc.dim(` flattens CNAME-at-root. If it doesn't, A-record apex support is coming soon;`));
9037
- console.log(pc.dim(` use a subdomain like app.${hostname} in the meantime.`));
9038
+ if (res.data.dns_note) {
9039
+ console.log(pc.yellow(` DNS requirement: ${res.data.dns_note}`));
9038
9040
  console.log();
9039
9041
  }
9040
9042
  if (options.noWait) {
@@ -9052,11 +9054,11 @@ async function sitesDomainAddCommand(hostname, options) {
9052
9054
  console.log(pc.dim(` ${line}`));
9053
9055
  lastLine = line;
9054
9056
  }
9055
- if (row.cert_status === "active" && row.ownership_status === "active") {
9057
+ if (row.live === true) {
9056
9058
  console.log(pc.green(" ✓") + ` ${hostname} is live with a valid certificate.`);
9057
9059
  return;
9058
9060
  }
9059
- if (row.cert_status === "error" || row.ownership_status === "error") throw new Error(`Domain setup failed for ${hostname}. Confirm the TXT record(s) above are published and the CNAME points at ${res.data.dns_target}.`);
9061
+ if (isProviderTerminalDomainFailure(row)) throw new Error(`Domain setup failed for ${hostname}. Confirm the TXT record(s) above are published and the CNAME points at ${res.data.dns_target}.`);
9060
9062
  await sleep(5e3);
9061
9063
  }
9062
9064
  console.log(pc.yellow(` ! Timed out after ${options.timeout ?? 600}s. Re-run \`amba sites describe ${options.site}\` to check status.`));
@@ -9073,6 +9075,15 @@ async function sitesDomainListCommand(siteName) {
9073
9075
  for (const d of res.data) console.log(` ${pc.bold(d.hostname)} ${formatDomainStatus(d)}`);
9074
9076
  console.log();
9075
9077
  }
9078
+ async function sitesDomainRefreshCommand(hostname, options) {
9079
+ validateSiteName(options.site);
9080
+ validateHostname(hostname);
9081
+ const projectConfig = await loadProjectConfig();
9082
+ const normalisedHostname = hostname.toLowerCase();
9083
+ const res = await refreshSiteDomainViaApi(projectConfig.projectId, options.site, normalisedHostname);
9084
+ console.log(` ${pc.bold(res.data.hostname)} ${formatDomainStatus(res.data)}`);
9085
+ if (res.data.note) console.log(pc.dim(` ${res.data.note}`));
9086
+ }
9076
9087
  async function sitesDomainRemoveCommand(hostname, options) {
9077
9088
  validateSiteName(options.site);
9078
9089
  validateHostname(hostname);
@@ -9180,12 +9191,18 @@ function formatCertStatus(s) {
9180
9191
  return pc.yellow(s);
9181
9192
  }
9182
9193
  /**
9183
- * Render BOTH lifecycle states for a domain row. A domain is only live when
9184
- * ownership AND cert are both `active`, so display paths must show both —
9185
- * showing cert alone reads "live" while ownership is still pending.
9194
+ * Render every serving plane. Only the server-derived `live` field is
9195
+ * authoritative; these states explain what is still converging.
9186
9196
  */
9187
9197
  function formatDomainStatus(d) {
9188
- return `ownership: ${formatCertStatus(d.ownership_status)} cert: ${formatCertStatus(d.cert_status)}`;
9198
+ const states = [`ownership: ${formatCertStatus(d.ownership_status)}`, `cert: ${formatCertStatus(d.cert_status)}`];
9199
+ if (d.route_status) states.push(`route: ${formatCertStatus(d.route_status)}`);
9200
+ if (d.kv_status) states.push(`kv: ${formatCertStatus(d.kv_status)}`);
9201
+ if (d.dns_managed && d.dns_status) states.push(`dns: ${formatCertStatus(d.dns_status)}`);
9202
+ return states.join(" ");
9203
+ }
9204
+ function isProviderTerminalDomainFailure(d) {
9205
+ return d.cert_status === "error" || d.ownership_status === "error";
9189
9206
  }
9190
9207
  function sleep(ms) {
9191
9208
  return new Promise((r) => setTimeout(r, ms));
@@ -9305,9 +9322,13 @@ async function domainsBuyCommand(domain, options) {
9305
9322
  }
9306
9323
  console.log(pc.green(" ✓") + ` Registered ${pc.bold(hostname)}`);
9307
9324
  if (r.provisioned) {
9308
- console.log(pc.green(" ✓") + ` Connected to ${options.site}${r.dns_autoconfigured ? " — no DNS setup needed" : ""}.`);
9309
- if (r.public_url) console.log(pc.green(" ✓") + ` Live at ${pc.underline(r.public_url)}`);
9310
- console.log(pc.dim(" The TLS certificate is issuing now (usually a minute or two)."));
9325
+ console.log(pc.green(" ✓") + ` Binding created for ${options.site}${r.dns_autoconfigured ? " — no DNS setup needed" : ""}.`);
9326
+ const hostnameLive = r.hostname_live === true;
9327
+ if (r.public_url && hostnameLive) console.log(pc.green(" ✓") + ` Live at ${pc.underline(r.public_url)}`);
9328
+ else if (r.public_url) {
9329
+ console.log(pc.yellow(" !") + ` Pending activation at ${pc.underline(r.public_url)}`);
9330
+ console.log(pc.dim(` certificate=${r.cert_status ?? "pending"}, ownership=${r.ownership_status ?? "pending"}, route=${r.route_status ?? "pending"}, kv=${r.kv_status ?? "pending"}, dns=${r.dns_status ?? "pending"}; wait for hostname_live=true.`));
9331
+ }
9311
9332
  } else if (r.message) console.log(pc.dim(` ${r.message}`));
9312
9333
  console.log();
9313
9334
  }
@@ -9674,7 +9695,7 @@ async function diffConfigCommand(file = DEFAULT_BUNDLE_FILE, options = {}) {
9674
9695
  //#endregion
9675
9696
  //#region src/index.ts
9676
9697
  const program = new Command();
9677
- program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.1.2");
9698
+ program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.1.4");
9678
9699
  program.option("--token <pat>", "Use a Personal Access Token for headless / CI / agent use (overrides ~/.amba/credentials.json + AMBA_PAT env)");
9679
9700
  program.hook("preAction", (thisCommand) => {
9680
9701
  setBearerOverride(resolveTokenSource({
@@ -10035,6 +10056,9 @@ sitesDomain.command("add <hostname>").description("Attach a custom hostname (DV
10035
10056
  sitesDomain.command("list <site>").description("List custom hostnames attached to a site").action(async (site) => {
10036
10057
  await runAction(() => sitesDomainListCommand(site));
10037
10058
  });
10059
+ 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) => {
10060
+ await runAction(() => sitesDomainRefreshCommand(hostname, { site: opts.site }));
10061
+ });
10038
10062
  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) => {
10039
10063
  await runAction(() => sitesDomainRemoveCommand(hostname, {
10040
10064
  site: opts.site,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layers/amba",
3
- "version": "4.1.2",
3
+ "version": "4.1.4",
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-shared": "4.0.4",
53
- "@layers/amba-mcp": "4.0.6"
52
+ "@layers/amba-mcp": "4.0.8",
53
+ "@layers/amba-shared": "4.0.4"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsdown && tsc --emitDeclarationOnly",
@@ -40,6 +40,11 @@ Run user code in a sandbox triggered by HTTP, cron, or webhook. The function get
40
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
41
  | `amba_function_domains_list` / `amba_function_domains_refresh` | Read or refresh ownership/certificate status. | `{ project_id, name: "feed", hostname: "feeds.example.com" }` |
42
42
  | `amba_function_domains_remove` | Detach an exact function hostname. | `{ project_id, name: "feed", hostname: "feeds.example.com" }` |
43
+
44
+ For every custom hostname, publish a real provider-stored CNAME to the returned target and heed
45
+ the unconditional `dns_note`. Do not substitute ALIAS, ANAME, or copied A/AAAA addresses. Treat Site and Function hostnames
46
+ as ready only when `live=true` (certificate and hostname activation both active), not from a
47
+ successful TLS handshake or `cert_status=active` alone.
43
48
  | `amba_functions_list` / `amba_list_functions` | List functions. | `{ project_id }` |
44
49
  | `amba_functions_get` / `amba_get_function` | Read function metadata. | `{ project_id, function_id }` |
45
50
  | `amba_functions_get_logs` / `amba_get_function_logs` | Recent invocation logs. | `{ project_id, function_id, limit: 100 }` |