@layers/amba 4.1.4 → 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 {
@@ -310,6 +312,9 @@ export interface FunctionDomain {
310
312
  dns_note?: string | null;
311
313
  cert_status: 'pending_validation' | 'pending_issuance' | 'pending_deployment' | 'active' | 'error';
312
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;
313
318
  /** True only when certificate/ownership are active and routing reconciliation succeeded. */
314
319
  live: boolean;
315
320
  created_at: string;
@@ -383,10 +388,23 @@ export declare function deployFunctionViaApi(projectId: string, input: {
383
388
  * HMAC inside the handler.
384
389
  */
385
390
  public?: boolean;
391
+ /** Forward a verified client Authorization header to customer code. */
392
+ forward_authorization?: boolean;
386
393
  }): Promise<DeployFunctionResult>;
387
394
  export declare function listFunctionDeployments(projectId: string, options?: {
388
395
  activeOnly?: boolean;
389
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>>;
390
408
  /** Attach one exact hostname to one deployed function. */
391
409
  export declare function attachFunctionDomainViaApi(projectId: string, functionName: string, hostname: string): Promise<ApiResponse<AttachedFunctionDomain>>;
392
410
  /** List provider-neutral ownership/certificate status for a function's hostnames. */
@@ -433,6 +451,17 @@ export declare function scheduleFunction(projectId: string, input: {
433
451
  }): Promise<ApiResponse<{
434
452
  schedule_id: string;
435
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[]>>;
436
465
  export interface FunctionLogEvent {
437
466
  /** Sanitized log row — see `r2-log-reader.ts:SanitizedLogEvent`. Field-shape stable. */
438
467
  Event?: {
@@ -680,6 +709,9 @@ export interface SiteDomainRow {
680
709
  provider_hostname_id: string | null;
681
710
  cert_status: 'pending_validation' | 'pending_issuance' | 'pending_deployment' | 'active' | 'error';
682
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;
683
715
  /** Edge Worker route reconciliation. */
684
716
  route_status: 'pending' | 'active' | 'error';
685
717
  /** Host-to-site KV reconciliation. */
@@ -740,6 +772,7 @@ export declare function archiveSite(projectId: string, name: string, confirm: st
740
772
  export interface DeleteCascadeSummary {
741
773
  domains_removed?: number;
742
774
  site_runtime_removed?: boolean;
775
+ function_schedule_removed?: boolean;
743
776
  runtime_script_removed?: boolean;
744
777
  function_deployments_marked_disabled?: number;
745
778
  }
@@ -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;
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) {
@@ -9695,7 +9761,7 @@ async function diffConfigCommand(file = DEFAULT_BUNDLE_FILE, options = {}) {
9695
9761
  //#endregion
9696
9762
  //#region src/index.ts
9697
9763
  const program = new Command();
9698
- program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.1.4");
9764
+ program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.1.5");
9699
9765
  program.option("--token <pat>", "Use a Personal Access Token for headless / CI / agent use (overrides ~/.amba/credentials.json + AMBA_PAT env)");
9700
9766
  program.hook("preAction", (thisCommand) => {
9701
9767
  setBearerOverride(resolveTokenSource({
@@ -9837,18 +9903,24 @@ program.command("schema").description("Schema export commands").command("export"
9837
9903
  }));
9838
9904
  });
9839
9905
  const functions = program.command("functions").description("Customer serverless functions deployed to the edge");
9840
- 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) => {
9841
9907
  await runAction(() => functionsDeployCommand(target, opts));
9842
9908
  });
9843
9909
  functions.command("list").description("List active functions for the current project").action(async () => {
9844
9910
  await runAction(functionsListCommand);
9845
9911
  });
9912
+ functions.command("deploy-status").description("Show whether function deployments are available or paused for maintenance").action(async () => {
9913
+ await runAction(functionsDeployStatusCommand);
9914
+ });
9846
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) => {
9847
9916
  await runAction(() => functionsDeleteCommand(name, { confirm: opts.confirm }));
9848
9917
  });
9849
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) => {
9850
9919
  await runAction(() => functionsScheduleCommand(name, cron, opts));
9851
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
+ });
9852
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) => {
9853
9925
  await runAction(() => functionsDevCommand(file, {
9854
9926
  port: opts.port,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layers/amba",
3
- "version": "4.1.4",
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,7 +49,7 @@
49
49
  "tsdown": "^0.12.5",
50
50
  "typescript": "^5.8.3",
51
51
  "vitest": "^3.2.4",
52
- "@layers/amba-mcp": "4.0.8",
52
+ "@layers/amba-mcp": "4.0.9",
53
53
  "@layers/amba-shared": "4.0.4"
54
54
  },
55
55
  "scripts": {