@layers/amba 4.1.3 → 4.1.5

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.
@@ -295,6 +295,8 @@ export interface FunctionDeploymentRow {
295
295
  /** Opt-in public flag (#17). True = X-Api-Key gate skipped for this
296
296
  * function so external webhook senders can reach it. Default false. */
297
297
  is_public?: boolean;
298
+ /** Explicit per-deployment opt-in. Default false. */
299
+ forward_authorization?: boolean;
298
300
  }
299
301
  /** Provider-neutral lifecycle for an exact hostname bound to one function. */
300
302
  export interface FunctionDomain {
@@ -302,8 +304,17 @@ export interface FunctionDomain {
302
304
  project_id: string;
303
305
  function_name: string;
304
306
  hostname: string;
307
+ /** Required record at authoritative DNS. */
308
+ dns_record_type?: 'CNAME';
309
+ /** True for a registrable root rather than a subdomain. */
310
+ is_apex?: boolean;
311
+ /** Additive provider-neutral DNS warning, populated for apex hostnames. */
312
+ dns_note?: string | null;
305
313
  cert_status: 'pending_validation' | 'pending_issuance' | 'pending_deployment' | 'active' | 'error';
306
314
  ownership_status: 'pending' | 'active' | 'error';
315
+ /** Raw point-in-time upstream states returned by attach/refresh. */
316
+ provider_status?: string | null;
317
+ provider_cert_status?: string | null;
307
318
  /** True only when certificate/ownership are active and routing reconciliation succeeded. */
308
319
  live: boolean;
309
320
  created_at: string;
@@ -377,10 +388,23 @@ export declare function deployFunctionViaApi(projectId: string, input: {
377
388
  * HMAC inside the handler.
378
389
  */
379
390
  public?: boolean;
391
+ /** Forward a verified client Authorization header to customer code. */
392
+ forward_authorization?: boolean;
380
393
  }): Promise<DeployFunctionResult>;
381
394
  export declare function listFunctionDeployments(projectId: string, options?: {
382
395
  activeOnly?: boolean;
383
396
  }): Promise<ApiListResponse<FunctionDeploymentRow>>;
397
+ export interface FunctionDeployStatus {
398
+ status: 'available' | 'scheduled' | 'paused';
399
+ available: boolean;
400
+ paused: boolean;
401
+ retry_after_seconds: number | null;
402
+ retry_after_is_poll_interval: boolean;
403
+ maintenance_start_at: string | null;
404
+ estimated_resume_at: string | null;
405
+ reason: string | null;
406
+ }
407
+ export declare function getFunctionDeployStatus(projectId: string): Promise<ApiResponse<FunctionDeployStatus>>;
384
408
  /** Attach one exact hostname to one deployed function. */
385
409
  export declare function attachFunctionDomainViaApi(projectId: string, functionName: string, hostname: string): Promise<ApiResponse<AttachedFunctionDomain>>;
386
410
  /** List provider-neutral ownership/certificate status for a function's hostnames. */
@@ -427,6 +451,17 @@ export declare function scheduleFunction(projectId: string, input: {
427
451
  }): Promise<ApiResponse<{
428
452
  schedule_id: string;
429
453
  }>>;
454
+ export interface FunctionScheduleRow {
455
+ schedule_id: string;
456
+ function_name: string;
457
+ enriched: boolean;
458
+ cron: string | null;
459
+ timezone: string | null;
460
+ next_fire_at: string | null;
461
+ last_fire_at: string | null;
462
+ paused: boolean | null;
463
+ }
464
+ export declare function listFunctionSchedules(projectId: string): Promise<ApiResponse<FunctionScheduleRow[]>>;
430
465
  export interface FunctionLogEvent {
431
466
  /** Sanitized log row — see `r2-log-reader.ts:SanitizedLogEvent`. Field-shape stable. */
432
467
  Event?: {
@@ -674,6 +709,25 @@ export interface SiteDomainRow {
674
709
  provider_hostname_id: string | null;
675
710
  cert_status: 'pending_validation' | 'pending_issuance' | 'pending_deployment' | 'active' | 'error';
676
711
  ownership_status: 'pending' | 'active' | 'error';
712
+ /** Raw point-in-time upstream values returned by attach/refresh. */
713
+ provider_status?: string | null;
714
+ provider_cert_status?: string | null;
715
+ /** Edge Worker route reconciliation. */
716
+ route_status: 'pending' | 'active' | 'error';
717
+ /** Host-to-site KV reconciliation. */
718
+ kv_status: 'pending' | 'active' | 'error';
719
+ /** Whether Amba owns and manages the authoritative DNS zone. */
720
+ dns_managed: boolean;
721
+ /** Managed-DNS reconciliation, or not_required for externally managed DNS. */
722
+ dns_status: 'not_required' | 'pending' | 'active' | 'error';
723
+ /**
724
+ * Authoritative reachability. True only when certificate, ownership,
725
+ * Worker route, KV, and (when managed) DNS are all active.
726
+ */
727
+ live: boolean;
728
+ dns_record_type?: 'CNAME';
729
+ is_apex?: boolean;
730
+ dns_note?: string | null;
677
731
  created_at: string;
678
732
  updated_at: string;
679
733
  }
@@ -718,6 +772,7 @@ export declare function archiveSite(projectId: string, name: string, confirm: st
718
772
  export interface DeleteCascadeSummary {
719
773
  domains_removed?: number;
720
774
  site_runtime_removed?: boolean;
775
+ function_schedule_removed?: boolean;
721
776
  runtime_script_removed?: boolean;
722
777
  function_deployments_marked_disabled?: number;
723
778
  }
@@ -731,8 +786,14 @@ export declare function addSiteDomainViaApi(projectId: string, siteName: string,
731
786
  provider_hostname_id: string;
732
787
  cert_status: SiteDomainRow["cert_status"];
733
788
  ownership_status: SiteDomainRow["ownership_status"];
789
+ route_status: SiteDomainRow["route_status"];
790
+ kv_status: SiteDomainRow["kv_status"];
791
+ dns_managed: SiteDomainRow["dns_managed"];
792
+ dns_status: SiteDomainRow["dns_status"];
793
+ live: boolean;
734
794
  dns_target: string;
735
795
  is_apex: boolean;
796
+ dns_record_type: "CNAME";
736
797
  dns_note: string | null;
737
798
  ssl_validation: DomainValidationRecord[];
738
799
  ownership_verification: DomainOwnershipVerification | null;
@@ -809,6 +870,12 @@ export interface DomainPurchaseResult {
809
870
  provisioned?: boolean;
810
871
  cert_status?: string;
811
872
  ownership_status?: string;
873
+ route_status?: string;
874
+ kv_status?: string;
875
+ dns_managed?: boolean;
876
+ dns_status?: string;
877
+ /** Authoritative reachability across provider, route, KV, and managed DNS. */
878
+ hostname_live?: boolean;
812
879
  dns_autoconfigured?: boolean;
813
880
  url?: string;
814
881
  public_url?: string;
@@ -825,7 +892,22 @@ export declare function purchaseDomainViaApi(projectId: string, body: {
825
892
  privacy?: boolean;
826
893
  years?: number;
827
894
  }): Promise<ApiResponse<DomainPurchaseResult>>;
828
- export declare function listPurchasedDomainsViaApi(projectId: string): Promise<ApiListResponse<Record<string, unknown>>>;
895
+ export interface PurchasedDomainStatus extends Record<string, unknown> {
896
+ domain: string;
897
+ status: string;
898
+ site?: string | null;
899
+ provisioned: boolean;
900
+ cert_status?: string;
901
+ ownership_status?: string;
902
+ route_status?: string;
903
+ kv_status?: string;
904
+ dns_managed?: boolean;
905
+ dns_status?: string;
906
+ /** Reachability; deliberately separate from purchase/payment completion. */
907
+ hostname_live: boolean;
908
+ aliases: Array<Record<string, unknown>>;
909
+ }
910
+ export declare function listPurchasedDomainsViaApi(projectId: string): Promise<ApiListResponse<PurchasedDomainStatus>>;
829
911
  /**
830
912
  * Delete a function entirely. Cascade: removes the deployed script
831
913
  * (backend 404 treated as success), then marks every historical
@@ -845,10 +927,19 @@ export declare function attachSiteDomain(projectId: string, siteName: string, in
845
927
  provider_hostname_id?: string;
846
928
  }): Promise<ApiResponse<SiteDomainRow>>;
847
929
  export declare function listSiteDomains(projectId: string, siteName: string): Promise<ApiListResponse<SiteDomainRow>>;
930
+ /** @deprecated Lifecycle is provider-owned; this delegates to authoritative refresh. */
848
931
  export declare function updateSiteDomain(projectId: string, siteName: string, hostname: string, patch: {
849
932
  cert_status: SiteDomainRow['cert_status'];
850
933
  provider_hostname_id?: string;
851
- }): Promise<ApiResponse<SiteDomainRow>>;
934
+ }): Promise<ApiResponse<SiteDomainRow & {
935
+ ssl_validation: DomainValidationRecord[];
936
+ ownership_verification: DomainOwnershipVerification | null;
937
+ ownership_verification_http: {
938
+ http_url?: string;
939
+ http_body?: string;
940
+ } | null;
941
+ note?: string;
942
+ }>>;
852
943
  export declare function detachSiteDomain(projectId: string, siteName: string, hostname: string): Promise<ApiResponse<{
853
944
  deleted: boolean;
854
945
  }>>;
@@ -42,15 +42,19 @@ export interface DeployOptions {
42
42
  * the handler — public skips the credential gate only.
43
43
  */
44
44
  public?: boolean;
45
+ /** Opt in to forwarding verified client Authorization to this function. */
46
+ forwardAuthorization?: boolean;
45
47
  }
46
48
  export declare function functionsDeployCommand(target: string, options?: DeployOptions): Promise<void>;
47
49
  export declare function functionsListCommand(): Promise<void>;
50
+ export declare function functionsDeployStatusCommand(): Promise<void>;
48
51
  export declare function functionsDeleteCommand(name: string, options?: {
49
52
  confirm?: string;
50
53
  }): Promise<void>;
51
54
  export declare function functionsScheduleCommand(name: string, cron: string, options?: {
52
55
  tz?: string;
53
56
  }): Promise<void>;
57
+ export declare function functionsSchedulesCommand(): Promise<void>;
54
58
  export interface DevOptions {
55
59
  /** Local port. Default 8787 (overridable per project). */
56
60
  port?: number;
@@ -36,7 +36,7 @@ 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>;
@@ -74,6 +74,10 @@ declare function collectFiles(dir: string): Promise<SiteFile[]>;
74
74
  export declare class SitesStaticOnlyError extends Error {
75
75
  readonly code: "SITES_STATIC_ONLY";
76
76
  }
77
+ declare function isProviderTerminalDomainFailure(d: {
78
+ cert_status: string;
79
+ ownership_status: string;
80
+ }): boolean;
77
81
  /**
78
82
  * Test-only re-export of internal helpers. Tests reach in via this
79
83
  * namespace so the production module surface stays clean. Not a public
@@ -83,5 +87,6 @@ export declare const __testHelpers: {
83
87
  collectFiles: typeof collectFiles;
84
88
  BLOCKED_FILE_NAMES: Set<string>;
85
89
  BLOCKED_DIR_NAMES: Set<string>;
90
+ isProviderTerminalDomainFailure: typeof isProviderTerminalDomainFailure;
86
91
  };
87
92
  export {};
package/dist/index.js CHANGED
@@ -1018,7 +1018,8 @@ async function deployFunctionViaApi(projectId, input) {
1018
1018
  const metadata = {
1019
1019
  name: input.name,
1020
1020
  ...input.rate_limit !== void 0 ? { rate_limit: input.rate_limit } : {},
1021
- ...input.public === true ? { public: true } : {}
1021
+ ...input.public === true ? { public: true } : {},
1022
+ ...input.forward_authorization === true ? { forward_authorization: true } : {}
1022
1023
  };
1023
1024
  fd.set("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }), "metadata.json");
1024
1025
  return request("POST", `/projects/${projectId}/functions/deploy`, fd);
@@ -1026,6 +1027,9 @@ async function deployFunctionViaApi(projectId, input) {
1026
1027
  async function listFunctionDeployments(projectId, options = {}) {
1027
1028
  return request("GET", `/projects/${projectId}/functions/deployments${options.activeOnly ? "?active=1" : ""}`);
1028
1029
  }
1030
+ async function getFunctionDeployStatus(projectId) {
1031
+ return request("GET", `/projects/${projectId}/functions/deploy-status`);
1032
+ }
1029
1033
  function functionDomainsPath(projectId, functionName) {
1030
1034
  return `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionName)}/domains`;
1031
1035
  }
@@ -1048,6 +1052,9 @@ async function removeFunctionDomainViaApi(projectId, functionName, hostname) {
1048
1052
  async function scheduleFunction(projectId, input) {
1049
1053
  return request("POST", `/projects/${projectId}/functions/schedules`, input);
1050
1054
  }
1055
+ async function listFunctionSchedules(projectId) {
1056
+ return request("GET", `/projects/${projectId}/functions/schedules`);
1057
+ }
1051
1058
  async function getFunctionLogs(projectId, functionName, options = {}) {
1052
1059
  const params = new URLSearchParams();
1053
1060
  if (options.since) params.set("since", options.since);
@@ -5784,6 +5791,16 @@ async function functionsDeployCommand(target, options = {}) {
5784
5791
  console.log(pc.bold(` amba functions deploy ${pc.cyan(functionName)}`));
5785
5792
  console.log(pc.dim(" ─────────────────────────────────"));
5786
5793
  console.log();
5794
+ if (!options.dryRun) {
5795
+ let deployStatus = null;
5796
+ try {
5797
+ deployStatus = (await getFunctionDeployStatus(projectId)).data;
5798
+ } catch (err) {
5799
+ if (!(err instanceof ApiClientError && err.statusCode === 404)) throw err;
5800
+ }
5801
+ if (deployStatus && !deployStatus.available) throw new Error(formatDeployUnavailableMessage(deployStatus));
5802
+ if (deployStatus?.status === "scheduled") console.log(pc.yellow(" !") + ` Maintenance is scheduled for ${deployStatus.maintenance_start_at ?? "a forthcoming window"}.`);
5803
+ }
5787
5804
  console.log(pc.dim(` Bundling ${entryPoint}…`));
5788
5805
  const bundle = await bundleFunction({ entryPoint });
5789
5806
  printBundleReport(bundle);
@@ -5798,13 +5815,20 @@ async function functionsDeployCommand(target, options = {}) {
5798
5815
  name: functionName,
5799
5816
  bundleCode: bundle.code,
5800
5817
  rate_limit: rateLimit,
5801
- public: options.public === true ? true : void 0
5818
+ public: options.public === true ? true : void 0,
5819
+ forward_authorization: options.forwardAuthorization === true ? true : void 0
5802
5820
  });
5803
5821
  } catch (err) {
5804
5822
  if (err instanceof ApiClientError && err.statusCode === 503) {
5823
+ try {
5824
+ const status = (await getFunctionDeployStatus(projectId)).data;
5825
+ if (!status.available) throw new Error(formatDeployUnavailableMessage(status));
5826
+ } catch (statusErr) {
5827
+ if (!(statusErr instanceof ApiClientError)) throw statusErr;
5828
+ }
5805
5829
  const base = err.message.trimEnd();
5806
5830
  const sep = /[.!?]$/.test(base) ? " " : ". ";
5807
- throw new Error(`${base}${sep}The function platform is temporarily unavailable; retry in ~30s.`);
5831
+ throw new Error(`${base}${sep}Check \`amba functions deploy-status\` before retrying.`);
5808
5832
  }
5809
5833
  throw err;
5810
5834
  }
@@ -5812,8 +5836,15 @@ async function functionsDeployCommand(target, options = {}) {
5812
5836
  console.log(pc.green(" ✓") + ` URL: ${pc.underline(result.fn_url)}`);
5813
5837
  if (rateLimit) console.log(pc.dim(` Rate limit: ${rateLimit.max} per ${rateLimit.window} (key=${rateLimit.key}) — enforced pre-dispatch`));
5814
5838
  if (options.public === true) console.log(pc.yellow(" ! Public") + pc.dim(" — no X-Api-Key required. Verify the webhook signature inside your handler."));
5839
+ if (options.forwardAuthorization === true) console.log(pc.yellow(" ! Authorization forwarding enabled") + pc.dim(" — the verified client Bearer is visible to this function; X-Api-Key is never forwarded."));
5815
5840
  console.log();
5816
5841
  }
5842
+ function formatDeployUnavailableMessage(status) {
5843
+ const reason = status.reason ? ` (${status.reason})` : "";
5844
+ if (status.estimated_resume_at) return `Function deployments are paused${reason}. Estimated resume: ${status.estimated_resume_at}.`;
5845
+ if (status.retry_after_seconds !== null) return `Function deployments are paused${reason}. ${status.retry_after_is_poll_interval ? "Poll deploy-status again" : "Retry"} in ${status.retry_after_seconds}s; no end time is known yet.`;
5846
+ return `Function deployments are paused${reason}. Check \`amba functions deploy-status\` before retrying.`;
5847
+ }
5817
5848
  async function functionsListCommand() {
5818
5849
  const res = await listFunctionDeployments((await loadProjectConfig()).projectId, { activeOnly: true });
5819
5850
  console.log();
@@ -5825,12 +5856,25 @@ async function functionsListCommand() {
5825
5856
  for (const d of res.data) console.log(` ${pc.bold(d.name)} ${pc.dim("v" + d.version)} ` + pc.dim(`sha=${d.bundle_sha.slice(0, 12)}… ${d.created_at}`));
5826
5857
  console.log();
5827
5858
  }
5859
+ async function functionsDeployStatusCommand() {
5860
+ const res = await getFunctionDeployStatus((await loadProjectConfig()).projectId);
5861
+ if (res.data.available) {
5862
+ console.log(pc.green(" ✓") + " Function deployments are available.");
5863
+ if (res.data.status === "scheduled") {
5864
+ console.log(pc.yellow(" !") + ` Maintenance is scheduled for ${res.data.maintenance_start_at ?? "a forthcoming window"}.`);
5865
+ if (res.data.estimated_resume_at) console.log(pc.dim(` Estimated resume: ${res.data.estimated_resume_at}.`));
5866
+ }
5867
+ return;
5868
+ }
5869
+ console.log(pc.yellow(" !") + " Function deployments are paused for platform maintenance.");
5870
+ if (res.data.retry_after_seconds !== null) console.log(pc.dim(res.data.retry_after_is_poll_interval ? ` Poll deploy-status again in ${res.data.retry_after_seconds}s; no end time is known yet.` : ` Estimated resume: ${res.data.estimated_resume_at} (Retry-After ${res.data.retry_after_seconds}s).`));
5871
+ }
5828
5872
  async function functionsDeleteCommand(name, options = {}) {
5829
5873
  validateFunctionName$1(name);
5830
5874
  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.`);
5831
5875
  const cascade = (await deleteFunctionViaApi((await loadProjectConfig()).projectId, name, { confirm: name })).data.cascade;
5832
5876
  console.log(pc.green(" ✓") + ` Deleted ${pc.bold(name)}.`);
5833
- console.log(pc.dim(` Cascade: runtime_script_removed=${cascade.runtime_script_removed ?? false}, function_deployments_marked_disabled=${cascade.function_deployments_marked_disabled ?? 0}`));
5877
+ console.log(pc.dim(` Cascade: function_schedule_removed=${cascade.function_schedule_removed ?? false}, runtime_script_removed=${cascade.runtime_script_removed ?? false}, function_deployments_marked_disabled=${cascade.function_deployments_marked_disabled ?? 0}`));
5834
5878
  }
5835
5879
  async function functionsScheduleCommand(name, cron, options = {}) {
5836
5880
  const projectConfig = await loadProjectConfig();
@@ -5846,6 +5890,25 @@ async function functionsScheduleCommand(name, cron, options = {}) {
5846
5890
  console.log(pc.dim(` schedule_id: ${res.data.schedule_id}`));
5847
5891
  console.log();
5848
5892
  }
5893
+ async function functionsSchedulesCommand() {
5894
+ const res = await listFunctionSchedules((await loadProjectConfig()).projectId);
5895
+ console.log();
5896
+ if (res.data.length === 0) {
5897
+ console.log(pc.dim(" No function schedules registered."));
5898
+ console.log();
5899
+ return;
5900
+ }
5901
+ for (const schedule of res.data) {
5902
+ if (!schedule.enriched) {
5903
+ console.log(`${pc.bold(schedule.function_name)} ${pc.yellow("metadata unavailable")}`);
5904
+ continue;
5905
+ }
5906
+ const state = schedule.paused ? pc.yellow("paused") : pc.green("active");
5907
+ console.log(` ${pc.bold(schedule.function_name)} ${pc.cyan(schedule.cron ?? "?")} ${pc.dim(schedule.timezone ?? "UTC")} ${state}`);
5908
+ console.log(pc.dim(` last=${schedule.last_fire_at ?? "never"} next=${schedule.next_fire_at ?? "unknown"}`));
5909
+ }
5910
+ console.log();
5911
+ }
5849
5912
  /**
5850
5913
  * `amba functions dev <file>` — run the function locally with file-change
5851
5914
  * hot reload.
@@ -6420,7 +6483,10 @@ function printEvents(events, asJson) {
6420
6483
  console.log(`${pc.dim(ts)} ${pc.cyan(script)} ${level} ${msg}`);
6421
6484
  }
6422
6485
  if (e.Exceptions && e.Exceptions.length > 0) for (const ex of e.Exceptions) console.log(`${pc.dim(ts)} ${pc.cyan(script)} ${pc.red("[exception]")} ${ex.Name ?? "Error"}: ${ex.Message ?? ""}`);
6423
- if ((!e.Logs || e.Logs.length === 0) && (!e.Exceptions || e.Exceptions.length === 0) && e.Outcome && e.Outcome !== "ok") console.log(`${pc.dim(ts)} ${pc.cyan(script)} ${pc.yellow("[outcome]")} ${e.Outcome}`);
6486
+ if ((!e.Logs || e.Logs.length === 0) && (!e.Exceptions || e.Exceptions.length === 0) && e.Outcome) {
6487
+ const outcome = e.Outcome === "ok" ? pc.green("[outcome]") : pc.yellow("[outcome]");
6488
+ console.log(`${pc.dim(ts)} ${pc.cyan(script)} ${outcome} ${e.Outcome}`);
6489
+ }
6424
6490
  }
6425
6491
  }
6426
6492
  function renderLevel(level) {
@@ -6477,6 +6543,7 @@ function printDnsInstructions(domain) {
6477
6543
  if ("cleanup_pending" in domain && domain.cleanup_pending === true) console.log(pc.yellow(" Routing cleanup is incomplete; retry with domains refresh."));
6478
6544
  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
6545
  if (domain.dns_target) console.log(pc.dim(` DNS target: CNAME ${domain.hostname} → ${domain.dns_target}`));
6546
+ if (domain.dns_note) console.log(pc.yellow(` DNS requirement: ${domain.dns_note}`));
6480
6547
  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
6548
  else if (record.http_url && record.http_body) console.log(pc.dim(` Certificate HTTP: ${record.http_url} → ${record.http_body}`));
6482
6549
  const ownership = domain.ownership_verification;
@@ -9021,7 +9088,6 @@ async function sitesDomainAddCommand(hostname, options) {
9021
9088
  console.log(pc.green(" ✓") + ` Custom hostname registered`);
9022
9089
  const validation = res.data.ssl_validation ?? [];
9023
9090
  const ownership = res.data.ownership_verification;
9024
- const isApex = res.data.is_apex ?? false;
9025
9091
  if (validation.length > 0 || ownership) {
9026
9092
  console.log();
9027
9093
  console.log(pc.dim(" 1. Publish these DNS records to verify ownership:"));
@@ -9035,10 +9101,8 @@ async function sitesDomainAddCommand(hostname, options) {
9035
9101
  }
9036
9102
  console.log(` ${pc.bold("CNAME")} ${hostname} → ${pc.cyan(res.data.dns_target)}`);
9037
9103
  console.log();
9038
- if (isApex) {
9039
- console.log(pc.dim(` ${hostname} is a root domain — the CNAME above works only if your DNS provider`));
9040
- console.log(pc.dim(` flattens CNAME-at-root. If it doesn't, A-record apex support is coming soon;`));
9041
- console.log(pc.dim(` use a subdomain like app.${hostname} in the meantime.`));
9104
+ if (res.data.dns_note) {
9105
+ console.log(pc.yellow(` DNS requirement: ${res.data.dns_note}`));
9042
9106
  console.log();
9043
9107
  }
9044
9108
  if (options.noWait) {
@@ -9056,11 +9120,11 @@ async function sitesDomainAddCommand(hostname, options) {
9056
9120
  console.log(pc.dim(` ${line}`));
9057
9121
  lastLine = line;
9058
9122
  }
9059
- if (row.cert_status === "active" && row.ownership_status === "active") {
9123
+ if (row.live === true) {
9060
9124
  console.log(pc.green(" ✓") + ` ${hostname} is live with a valid certificate.`);
9061
9125
  return;
9062
9126
  }
9063
- 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}.`);
9127
+ 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}.`);
9064
9128
  await sleep(5e3);
9065
9129
  }
9066
9130
  console.log(pc.yellow(` ! Timed out after ${options.timeout ?? 600}s. Re-run \`amba sites describe ${options.site}\` to check status.`));
@@ -9193,12 +9257,18 @@ function formatCertStatus(s) {
9193
9257
  return pc.yellow(s);
9194
9258
  }
9195
9259
  /**
9196
- * Render BOTH lifecycle states for a domain row. A domain is only live when
9197
- * ownership AND cert are both `active`, so display paths must show both —
9198
- * showing cert alone reads "live" while ownership is still pending.
9260
+ * Render every serving plane. Only the server-derived `live` field is
9261
+ * authoritative; these states explain what is still converging.
9199
9262
  */
9200
9263
  function formatDomainStatus(d) {
9201
- return `ownership: ${formatCertStatus(d.ownership_status)} cert: ${formatCertStatus(d.cert_status)}`;
9264
+ const states = [`ownership: ${formatCertStatus(d.ownership_status)}`, `cert: ${formatCertStatus(d.cert_status)}`];
9265
+ if (d.route_status) states.push(`route: ${formatCertStatus(d.route_status)}`);
9266
+ if (d.kv_status) states.push(`kv: ${formatCertStatus(d.kv_status)}`);
9267
+ if (d.dns_managed && d.dns_status) states.push(`dns: ${formatCertStatus(d.dns_status)}`);
9268
+ return states.join(" ");
9269
+ }
9270
+ function isProviderTerminalDomainFailure(d) {
9271
+ return d.cert_status === "error" || d.ownership_status === "error";
9202
9272
  }
9203
9273
  function sleep(ms) {
9204
9274
  return new Promise((r) => setTimeout(r, ms));
@@ -9318,9 +9388,13 @@ async function domainsBuyCommand(domain, options) {
9318
9388
  }
9319
9389
  console.log(pc.green(" ✓") + ` Registered ${pc.bold(hostname)}`);
9320
9390
  if (r.provisioned) {
9321
- console.log(pc.green(" ✓") + ` Connected to ${options.site}${r.dns_autoconfigured ? " — no DNS setup needed" : ""}.`);
9322
- if (r.public_url) console.log(pc.green(" ✓") + ` Live at ${pc.underline(r.public_url)}`);
9323
- console.log(pc.dim(" The TLS certificate is issuing now (usually a minute or two)."));
9391
+ console.log(pc.green(" ✓") + ` Binding created for ${options.site}${r.dns_autoconfigured ? " — no DNS setup needed" : ""}.`);
9392
+ const hostnameLive = r.hostname_live === true;
9393
+ if (r.public_url && hostnameLive) console.log(pc.green(" ✓") + ` Live at ${pc.underline(r.public_url)}`);
9394
+ else if (r.public_url) {
9395
+ console.log(pc.yellow(" !") + ` Pending activation at ${pc.underline(r.public_url)}`);
9396
+ 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.`));
9397
+ }
9324
9398
  } else if (r.message) console.log(pc.dim(` ${r.message}`));
9325
9399
  console.log();
9326
9400
  }
@@ -9687,7 +9761,7 @@ async function diffConfigCommand(file = DEFAULT_BUNDLE_FILE, options = {}) {
9687
9761
  //#endregion
9688
9762
  //#region src/index.ts
9689
9763
  const program = new Command();
9690
- program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.1.3");
9764
+ program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.1.5");
9691
9765
  program.option("--token <pat>", "Use a Personal Access Token for headless / CI / agent use (overrides ~/.amba/credentials.json + AMBA_PAT env)");
9692
9766
  program.hook("preAction", (thisCommand) => {
9693
9767
  setBearerOverride(resolveTokenSource({
@@ -9829,18 +9903,24 @@ program.command("schema").description("Schema export commands").command("export"
9829
9903
  }));
9830
9904
  });
9831
9905
  const functions = program.command("functions").description("Customer serverless functions deployed to the edge");
9832
- functions.command("deploy <target>").description("Bundle a function (a single file OR a directory) and deploy to the dispatch namespace. A directory resolves its entry by convention (index.ts/js/mjs) or via --entry.").option("--name <name>", "Function name (default: file name without extension, or the directory leaf name)").option("--entry <file>", "Entry file when <target> is a directory (relative to it). Default: index.ts/js/mjs.").option("--dry-run", "Bundle and report size without uploading").option("--rate-limit-window <duration>", "Rate-limit window: 60s | 5m | 1h").option("--rate-limit-max <int>", "Rate-limit max requests per window", (v) => Number.parseInt(v, 10)).option("--rate-limit-key <kind>", "Rate-limit bucket key: user_id | ip").option("--public", "Deploy WITHOUT the X-Api-Key gate so external webhook senders can call it. You MUST verify the webhook signature in your handler. Default: private.").action(async (target, opts) => {
9906
+ functions.command("deploy <target>").description("Bundle a function (a single file OR a directory) and deploy to the dispatch namespace. A directory resolves its entry by convention (index.ts/js/mjs) or via --entry.").option("--name <name>", "Function name (default: file name without extension, or the directory leaf name)").option("--entry <file>", "Entry file when <target> is a directory (relative to it). Default: index.ts/js/mjs.").option("--dry-run", "Bundle and report size without uploading").option("--rate-limit-window <duration>", "Rate-limit window: 60s | 5m | 1h").option("--rate-limit-max <int>", "Rate-limit max requests per window", (v) => Number.parseInt(v, 10)).option("--rate-limit-key <kind>", "Rate-limit bucket key: user_id | ip").option("--public", "Deploy WITHOUT the X-Api-Key gate so external webhook senders can call it. You MUST verify the webhook signature in your handler. Default: private.").option("--forward-authorization", "Forward a verified client Authorization Bearer to this function. X-Api-Key and internal trigger credentials remain stripped.").action(async (target, opts) => {
9833
9907
  await runAction(() => functionsDeployCommand(target, opts));
9834
9908
  });
9835
9909
  functions.command("list").description("List active functions for the current project").action(async () => {
9836
9910
  await runAction(functionsListCommand);
9837
9911
  });
9912
+ functions.command("deploy-status").description("Show whether function deployments are available or paused for maintenance").action(async () => {
9913
+ await runAction(functionsDeployStatusCommand);
9914
+ });
9838
9915
  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
9916
  await runAction(() => functionsDeleteCommand(name, { confirm: opts.confirm }));
9840
9917
  });
9841
9918
  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) => {
9842
9919
  await runAction(() => functionsScheduleCommand(name, cron, opts));
9843
9920
  });
9921
+ functions.command("schedules").description("List function schedules with cron, timezone, state, and last/next fire times").action(async () => {
9922
+ await runAction(functionsSchedulesCommand);
9923
+ });
9844
9924
  functions.command("dev <file>").description("Run a local dev server for your function with hot reload on file changes").option("--port <n>", "Port to listen on (default 8787)", (v) => parseInt(v, 10)).option("--no-watch", "Disable file-change hot reload").action(async (file, opts) => {
9845
9925
  await runAction(() => functionsDevCommand(file, {
9846
9926
  port: opts.port,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layers/amba",
3
- "version": "4.1.3",
3
+ "version": "4.1.5",
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.7"
52
+ "@layers/amba-mcp": "4.0.9",
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 }` |