@layers/amba 4.1.4 → 4.1.6

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.
@@ -21,6 +21,8 @@ export interface ProjectSummary {
21
21
  environment?: string;
22
22
  bundle_id?: string | null;
23
23
  google_oauth_client_id?: string | null;
24
+ magic_link_redirect_url?: string | null;
25
+ auth_email_brand_name?: string | null;
24
26
  status?: string;
25
27
  created_at?: string;
26
28
  /** Persisted subscription/accounting tier. */
@@ -71,6 +73,8 @@ export declare function updateProject(projectId: string, patch: {
71
73
  name?: string;
72
74
  bundle_id?: string | null;
73
75
  google_oauth_client_id?: string | null;
76
+ magic_link_redirect_url?: string | null;
77
+ auth_email_brand_name?: string | null;
74
78
  platform?: string;
75
79
  environment?: string;
76
80
  }): Promise<ApiResponse<{
@@ -78,6 +82,8 @@ export declare function updateProject(projectId: string, patch: {
78
82
  name: string;
79
83
  bundle_id: string | null;
80
84
  google_oauth_client_id: string | null;
85
+ magic_link_redirect_url: string | null;
86
+ auth_email_brand_name: string | null;
81
87
  platform: string;
82
88
  environment: string;
83
89
  }>>;
@@ -87,6 +93,8 @@ export declare function getProject(projectId: string): Promise<ApiResponse<{
87
93
  platform: string;
88
94
  environment: string;
89
95
  bundle_id: string | null;
96
+ magic_link_redirect_url: string | null;
97
+ auth_email_brand_name: string | null;
90
98
  created_at: string;
91
99
  }>>;
92
100
  export declare function deleteProject(projectId: string): Promise<ApiResponse<{
@@ -295,6 +303,8 @@ export interface FunctionDeploymentRow {
295
303
  /** Opt-in public flag (#17). True = X-Api-Key gate skipped for this
296
304
  * function so external webhook senders can reach it. Default false. */
297
305
  is_public?: boolean;
306
+ /** Explicit per-deployment opt-in. Default false. */
307
+ forward_authorization?: boolean;
298
308
  }
299
309
  /** Provider-neutral lifecycle for an exact hostname bound to one function. */
300
310
  export interface FunctionDomain {
@@ -310,6 +320,9 @@ export interface FunctionDomain {
310
320
  dns_note?: string | null;
311
321
  cert_status: 'pending_validation' | 'pending_issuance' | 'pending_deployment' | 'active' | 'error';
312
322
  ownership_status: 'pending' | 'active' | 'error';
323
+ /** Raw point-in-time upstream states returned by attach/refresh. */
324
+ provider_status?: string | null;
325
+ provider_cert_status?: string | null;
313
326
  /** True only when certificate/ownership are active and routing reconciliation succeeded. */
314
327
  live: boolean;
315
328
  created_at: string;
@@ -383,10 +396,23 @@ export declare function deployFunctionViaApi(projectId: string, input: {
383
396
  * HMAC inside the handler.
384
397
  */
385
398
  public?: boolean;
399
+ /** Forward a verified client Authorization header to customer code. */
400
+ forward_authorization?: boolean;
386
401
  }): Promise<DeployFunctionResult>;
387
402
  export declare function listFunctionDeployments(projectId: string, options?: {
388
403
  activeOnly?: boolean;
389
404
  }): Promise<ApiListResponse<FunctionDeploymentRow>>;
405
+ export interface FunctionDeployStatus {
406
+ status: 'available' | 'scheduled' | 'paused';
407
+ available: boolean;
408
+ paused: boolean;
409
+ retry_after_seconds: number | null;
410
+ retry_after_is_poll_interval: boolean;
411
+ maintenance_start_at: string | null;
412
+ estimated_resume_at: string | null;
413
+ reason: string | null;
414
+ }
415
+ export declare function getFunctionDeployStatus(projectId: string): Promise<ApiResponse<FunctionDeployStatus>>;
390
416
  /** Attach one exact hostname to one deployed function. */
391
417
  export declare function attachFunctionDomainViaApi(projectId: string, functionName: string, hostname: string): Promise<ApiResponse<AttachedFunctionDomain>>;
392
418
  /** List provider-neutral ownership/certificate status for a function's hostnames. */
@@ -433,6 +459,17 @@ export declare function scheduleFunction(projectId: string, input: {
433
459
  }): Promise<ApiResponse<{
434
460
  schedule_id: string;
435
461
  }>>;
462
+ export interface FunctionScheduleRow {
463
+ schedule_id: string;
464
+ function_name: string;
465
+ enriched: boolean;
466
+ cron: string | null;
467
+ timezone: string | null;
468
+ next_fire_at: string | null;
469
+ last_fire_at: string | null;
470
+ paused: boolean | null;
471
+ }
472
+ export declare function listFunctionSchedules(projectId: string): Promise<ApiResponse<FunctionScheduleRow[]>>;
436
473
  export interface FunctionLogEvent {
437
474
  /** Sanitized log row — see `r2-log-reader.ts:SanitizedLogEvent`. Field-shape stable. */
438
475
  Event?: {
@@ -645,7 +682,7 @@ export declare function dropCollection(projectId: string, name: string, confirm:
645
682
  deleted: boolean;
646
683
  version: number;
647
684
  }>>;
648
- export type AiProviderName = 'anthropic' | 'openai';
685
+ export type AiProviderName = 'anthropic' | 'openai' | 'mistral' | 'gemini';
649
686
  export interface AiProviderRow {
650
687
  name: AiProviderName;
651
688
  /** Whether an API key is currently configured for this provider. */
@@ -680,6 +717,9 @@ export interface SiteDomainRow {
680
717
  provider_hostname_id: string | null;
681
718
  cert_status: 'pending_validation' | 'pending_issuance' | 'pending_deployment' | 'active' | 'error';
682
719
  ownership_status: 'pending' | 'active' | 'error';
720
+ /** Raw point-in-time upstream values returned by attach/refresh. */
721
+ provider_status?: string | null;
722
+ provider_cert_status?: string | null;
683
723
  /** Edge Worker route reconciliation. */
684
724
  route_status: 'pending' | 'active' | 'error';
685
725
  /** Host-to-site KV reconciliation. */
@@ -740,6 +780,7 @@ export declare function archiveSite(projectId: string, name: string, confirm: st
740
780
  export interface DeleteCascadeSummary {
741
781
  domains_removed?: number;
742
782
  site_runtime_removed?: boolean;
783
+ function_schedule_removed?: boolean;
743
784
  runtime_script_removed?: boolean;
744
785
  function_deployments_marked_disabled?: number;
745
786
  }
@@ -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;
@@ -1,3 +1,4 @@
1
+ import { updateProject } from '../api-client.js';
1
2
  export declare function projectsListCommand(): Promise<void>;
2
3
  export declare function projectsCreateCommand(input: {
3
4
  name: string;
@@ -6,13 +7,19 @@ export declare function projectsCreateCommand(input: {
6
7
  googleOauthClientId?: string;
7
8
  platform?: string;
8
9
  }): Promise<void>;
9
- export declare function projectsUpdateCommand(projectId: string, input: {
10
+ export interface ProjectUpdateCommandInput {
10
11
  name?: string;
11
12
  bundleId?: string;
12
13
  googleOauthClientId?: string;
14
+ magicLinkRedirectUrl?: string;
15
+ authEmailBrandName?: string;
16
+ clearMagicLinkRedirectUrl?: boolean;
17
+ clearAuthEmailBrandName?: boolean;
13
18
  platform?: string;
14
19
  environment?: string;
15
- }): Promise<void>;
20
+ }
21
+ export declare function buildProjectUpdatePatch(input: ProjectUpdateCommandInput): Parameters<typeof updateProject>[1];
22
+ export declare function projectsUpdateCommand(projectId: string, input: ProjectUpdateCommandInput): Promise<void>;
16
23
  export declare function projectsShowCommand(projectId: string): Promise<void>;
17
24
  export declare function projectsDeleteCommand(projectId: string, opts?: {
18
25
  yes?: boolean;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from "commander";
2
+ import { Command, Option } from "commander";
3
3
  import pc from "picocolors";
4
4
  import { access, chmod, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
5
5
  import { homedir, tmpdir } from "node:os";
@@ -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);
@@ -4731,22 +4738,34 @@ async function projectsCreateCommand(input) {
4731
4738
  handleError$2(err);
4732
4739
  }
4733
4740
  }
4734
- async function projectsUpdateCommand(projectId, input) {
4735
- console.log();
4736
- console.log(pc.bold(` amba projects update ${projectId}`));
4737
- console.log(pc.dim(" ─────────────────────────────────"));
4738
- console.log();
4741
+ function buildProjectUpdatePatch(input) {
4742
+ if (input.magicLinkRedirectUrl !== void 0 && input.clearMagicLinkRedirectUrl) throw new Error("--magic-link-redirect-url conflicts with --clear-magic-link-redirect-url");
4743
+ if (input.authEmailBrandName !== void 0 && input.clearAuthEmailBrandName) throw new Error("--auth-email-brand-name conflicts with --clear-auth-email-brand-name");
4739
4744
  const patch = {};
4740
4745
  if (input.name !== void 0) patch.name = input.name;
4741
4746
  if (input.bundleId !== void 0) patch.bundle_id = input.bundleId;
4742
4747
  if (input.googleOauthClientId !== void 0) patch.google_oauth_client_id = input.googleOauthClientId;
4743
4748
  if (input.platform !== void 0) patch.platform = input.platform;
4744
4749
  if (input.environment !== void 0) patch.environment = input.environment;
4750
+ if (input.magicLinkRedirectUrl !== void 0) patch.magic_link_redirect_url = input.magicLinkRedirectUrl;
4751
+ if (input.clearMagicLinkRedirectUrl) patch.magic_link_redirect_url = null;
4752
+ if (input.authEmailBrandName !== void 0) patch.auth_email_brand_name = input.authEmailBrandName;
4753
+ if (input.clearAuthEmailBrandName) patch.auth_email_brand_name = null;
4754
+ return patch;
4755
+ }
4756
+ async function projectsUpdateCommand(projectId, input) {
4757
+ console.log();
4758
+ console.log(pc.bold(` amba projects update ${projectId}`));
4759
+ console.log(pc.dim(" ─────────────────────────────────"));
4760
+ console.log();
4761
+ const patch = buildProjectUpdatePatch(input);
4745
4762
  try {
4746
4763
  const res = await updateProject(projectId, patch);
4747
4764
  console.log(pc.green(" ✓") + ` Updated ${pc.bold(res.data.name)} ${pc.dim(`(${res.data.id})`)}`);
4748
4765
  if (res.data.bundle_id) console.log(pc.dim(` Bundle ID: ${res.data.bundle_id}`));
4749
4766
  if (res.data.google_oauth_client_id) console.log(pc.dim(` Google OAuth client id: ${res.data.google_oauth_client_id}`));
4767
+ if (res.data.magic_link_redirect_url) console.log(pc.dim(` Magic-link callback: ${res.data.magic_link_redirect_url}`));
4768
+ if (res.data.auth_email_brand_name) console.log(pc.dim(` Auth email brand: ${res.data.auth_email_brand_name}`));
4750
4769
  console.log();
4751
4770
  } catch (err) {
4752
4771
  if (err instanceof ApiClientError && err.statusCode === 404) {
@@ -5784,6 +5803,16 @@ async function functionsDeployCommand(target, options = {}) {
5784
5803
  console.log(pc.bold(` amba functions deploy ${pc.cyan(functionName)}`));
5785
5804
  console.log(pc.dim(" ─────────────────────────────────"));
5786
5805
  console.log();
5806
+ if (!options.dryRun) {
5807
+ let deployStatus = null;
5808
+ try {
5809
+ deployStatus = (await getFunctionDeployStatus(projectId)).data;
5810
+ } catch (err) {
5811
+ if (!(err instanceof ApiClientError && err.statusCode === 404)) throw err;
5812
+ }
5813
+ if (deployStatus && !deployStatus.available) throw new Error(formatDeployUnavailableMessage(deployStatus));
5814
+ if (deployStatus?.status === "scheduled") console.log(pc.yellow(" !") + ` Maintenance is scheduled for ${deployStatus.maintenance_start_at ?? "a forthcoming window"}.`);
5815
+ }
5787
5816
  console.log(pc.dim(` Bundling ${entryPoint}…`));
5788
5817
  const bundle = await bundleFunction({ entryPoint });
5789
5818
  printBundleReport(bundle);
@@ -5798,13 +5827,20 @@ async function functionsDeployCommand(target, options = {}) {
5798
5827
  name: functionName,
5799
5828
  bundleCode: bundle.code,
5800
5829
  rate_limit: rateLimit,
5801
- public: options.public === true ? true : void 0
5830
+ public: options.public === true ? true : void 0,
5831
+ forward_authorization: options.forwardAuthorization === true ? true : void 0
5802
5832
  });
5803
5833
  } catch (err) {
5804
5834
  if (err instanceof ApiClientError && err.statusCode === 503) {
5835
+ try {
5836
+ const status = (await getFunctionDeployStatus(projectId)).data;
5837
+ if (!status.available) throw new Error(formatDeployUnavailableMessage(status));
5838
+ } catch (statusErr) {
5839
+ if (!(statusErr instanceof ApiClientError)) throw statusErr;
5840
+ }
5805
5841
  const base = err.message.trimEnd();
5806
5842
  const sep = /[.!?]$/.test(base) ? " " : ". ";
5807
- throw new Error(`${base}${sep}The function platform is temporarily unavailable; retry in ~30s.`);
5843
+ throw new Error(`${base}${sep}Check \`amba functions deploy-status\` before retrying.`);
5808
5844
  }
5809
5845
  throw err;
5810
5846
  }
@@ -5812,8 +5848,15 @@ async function functionsDeployCommand(target, options = {}) {
5812
5848
  console.log(pc.green(" ✓") + ` URL: ${pc.underline(result.fn_url)}`);
5813
5849
  if (rateLimit) console.log(pc.dim(` Rate limit: ${rateLimit.max} per ${rateLimit.window} (key=${rateLimit.key}) — enforced pre-dispatch`));
5814
5850
  if (options.public === true) console.log(pc.yellow(" ! Public") + pc.dim(" — no X-Api-Key required. Verify the webhook signature inside your handler."));
5851
+ 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
5852
  console.log();
5816
5853
  }
5854
+ function formatDeployUnavailableMessage(status) {
5855
+ const reason = status.reason ? ` (${status.reason})` : "";
5856
+ if (status.estimated_resume_at) return `Function deployments are paused${reason}. Estimated resume: ${status.estimated_resume_at}.`;
5857
+ 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.`;
5858
+ return `Function deployments are paused${reason}. Check \`amba functions deploy-status\` before retrying.`;
5859
+ }
5817
5860
  async function functionsListCommand() {
5818
5861
  const res = await listFunctionDeployments((await loadProjectConfig()).projectId, { activeOnly: true });
5819
5862
  console.log();
@@ -5825,12 +5868,25 @@ async function functionsListCommand() {
5825
5868
  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
5869
  console.log();
5827
5870
  }
5871
+ async function functionsDeployStatusCommand() {
5872
+ const res = await getFunctionDeployStatus((await loadProjectConfig()).projectId);
5873
+ if (res.data.available) {
5874
+ console.log(pc.green(" ✓") + " Function deployments are available.");
5875
+ if (res.data.status === "scheduled") {
5876
+ console.log(pc.yellow(" !") + ` Maintenance is scheduled for ${res.data.maintenance_start_at ?? "a forthcoming window"}.`);
5877
+ if (res.data.estimated_resume_at) console.log(pc.dim(` Estimated resume: ${res.data.estimated_resume_at}.`));
5878
+ }
5879
+ return;
5880
+ }
5881
+ console.log(pc.yellow(" !") + " Function deployments are paused for platform maintenance.");
5882
+ 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).`));
5883
+ }
5828
5884
  async function functionsDeleteCommand(name, options = {}) {
5829
5885
  validateFunctionName$1(name);
5830
5886
  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
5887
  const cascade = (await deleteFunctionViaApi((await loadProjectConfig()).projectId, name, { confirm: name })).data.cascade;
5832
5888
  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}`));
5889
+ 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
5890
  }
5835
5891
  async function functionsScheduleCommand(name, cron, options = {}) {
5836
5892
  const projectConfig = await loadProjectConfig();
@@ -5846,6 +5902,25 @@ async function functionsScheduleCommand(name, cron, options = {}) {
5846
5902
  console.log(pc.dim(` schedule_id: ${res.data.schedule_id}`));
5847
5903
  console.log();
5848
5904
  }
5905
+ async function functionsSchedulesCommand() {
5906
+ const res = await listFunctionSchedules((await loadProjectConfig()).projectId);
5907
+ console.log();
5908
+ if (res.data.length === 0) {
5909
+ console.log(pc.dim(" No function schedules registered."));
5910
+ console.log();
5911
+ return;
5912
+ }
5913
+ for (const schedule of res.data) {
5914
+ if (!schedule.enriched) {
5915
+ console.log(`${pc.bold(schedule.function_name)} ${pc.yellow("metadata unavailable")}`);
5916
+ continue;
5917
+ }
5918
+ const state = schedule.paused ? pc.yellow("paused") : pc.green("active");
5919
+ console.log(` ${pc.bold(schedule.function_name)} ${pc.cyan(schedule.cron ?? "?")} ${pc.dim(schedule.timezone ?? "UTC")} ${state}`);
5920
+ console.log(pc.dim(` last=${schedule.last_fire_at ?? "never"} next=${schedule.next_fire_at ?? "unknown"}`));
5921
+ }
5922
+ console.log();
5923
+ }
5849
5924
  /**
5850
5925
  * `amba functions dev <file>` — run the function locally with file-change
5851
5926
  * hot reload.
@@ -6420,7 +6495,10 @@ function printEvents(events, asJson) {
6420
6495
  console.log(`${pc.dim(ts)} ${pc.cyan(script)} ${level} ${msg}`);
6421
6496
  }
6422
6497
  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}`);
6498
+ if ((!e.Logs || e.Logs.length === 0) && (!e.Exceptions || e.Exceptions.length === 0) && e.Outcome) {
6499
+ const outcome = e.Outcome === "ok" ? pc.green("[outcome]") : pc.yellow("[outcome]");
6500
+ console.log(`${pc.dim(ts)} ${pc.cyan(script)} ${outcome} ${e.Outcome}`);
6501
+ }
6424
6502
  }
6425
6503
  }
6426
6504
  function renderLevel(level) {
@@ -6536,7 +6614,12 @@ async function functionDomainsRemoveCommand(functionName, hostnameInput) {
6536
6614
  * server-side; the CLI never echoes plaintext back. The API returns a
6537
6615
  * `api_key_preview` (first-6 + last-4) for confirmation.
6538
6616
  */
6539
- const VALID_PROVIDERS = new Set(["anthropic", "openai"]);
6617
+ const VALID_PROVIDERS = new Set([
6618
+ "anthropic",
6619
+ "openai",
6620
+ "mistral",
6621
+ "gemini"
6622
+ ]);
6540
6623
  function assertValidProvider(name) {
6541
6624
  if (!VALID_PROVIDERS.has(name)) throw new Error(`Unknown AI provider '${name}'. Supported: ${[...VALID_PROVIDERS].join(", ")}.`);
6542
6625
  }
@@ -9695,7 +9778,7 @@ async function diffConfigCommand(file = DEFAULT_BUNDLE_FILE, options = {}) {
9695
9778
  //#endregion
9696
9779
  //#region src/index.ts
9697
9780
  const program = new Command();
9698
- program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.1.4");
9781
+ program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.1.6");
9699
9782
  program.option("--token <pat>", "Use a Personal Access Token for headless / CI / agent use (overrides ~/.amba/credentials.json + AMBA_PAT env)");
9700
9783
  program.hook("preAction", (thisCommand) => {
9701
9784
  setBearerOverride(resolveTokenSource({
@@ -9784,13 +9867,17 @@ projects.command("create").description("Create a new project").requiredOption("-
9784
9867
  platform: opts.platform
9785
9868
  }));
9786
9869
  });
9787
- projects.command("update <projectId>").description("Update mutable fields on a project").option("--name <name>", "Display name").option("--bundle-id <id>", "Bundle identifier (Apple Sign In audience)").option("--google-oauth-client-id <id>", "Google OAuth 2.0 client id (Google Sign In audience). Public identifier, not a secret.").option("--platform <platform>", "Platform: 'ios' | 'android' | 'all'").option("--environment <env>", "Environment label: 'development' | 'production'").action(async (projectId, opts) => {
9870
+ projects.command("update <projectId>").description("Update mutable fields on a project").option("--name <name>", "Display name").option("--bundle-id <id>", "Bundle identifier (Apple Sign In audience)").option("--google-oauth-client-id <id>", "Google OAuth 2.0 client id (Google Sign In audience). Public identifier, not a secret.").option("--platform <platform>", "Platform: 'ios' | 'android' | 'all'").option("--environment <env>", "Environment label: 'development' | 'production'").addOption(new Option("--magic-link-redirect-url <url>", "Exact HTTPS callback URL for magic-link sign-in (Amba appends the token query parameter)").conflicts("clearMagicLinkRedirectUrl")).addOption(new Option("--clear-magic-link-redirect-url", "Clear the project callback and restore the platform fallback").conflicts("magicLinkRedirectUrl")).addOption(new Option("--auth-email-brand-name <name>", "Brand shown in auth emails and sender attribution").conflicts("clearAuthEmailBrandName")).addOption(new Option("--clear-auth-email-brand-name", "Clear the auth-email brand and fall back to the project name").conflicts("authEmailBrandName")).action(async (projectId, opts) => {
9788
9871
  await runAction(() => projectsUpdateCommand(projectId, {
9789
9872
  name: opts.name,
9790
9873
  bundleId: opts.bundleId,
9791
9874
  googleOauthClientId: opts.googleOauthClientId,
9792
9875
  platform: opts.platform,
9793
- environment: opts.environment
9876
+ environment: opts.environment,
9877
+ magicLinkRedirectUrl: opts.magicLinkRedirectUrl,
9878
+ authEmailBrandName: opts.authEmailBrandName,
9879
+ clearMagicLinkRedirectUrl: opts.clearMagicLinkRedirectUrl,
9880
+ clearAuthEmailBrandName: opts.clearAuthEmailBrandName
9794
9881
  }));
9795
9882
  });
9796
9883
  projects.command("show <projectId>").description("Show full project details as JSON").action(async (projectId) => {
@@ -9837,18 +9924,24 @@ program.command("schema").description("Schema export commands").command("export"
9837
9924
  }));
9838
9925
  });
9839
9926
  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) => {
9927
+ 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
9928
  await runAction(() => functionsDeployCommand(target, opts));
9842
9929
  });
9843
9930
  functions.command("list").description("List active functions for the current project").action(async () => {
9844
9931
  await runAction(functionsListCommand);
9845
9932
  });
9933
+ functions.command("deploy-status").description("Show whether function deployments are available or paused for maintenance").action(async () => {
9934
+ await runAction(functionsDeployStatusCommand);
9935
+ });
9846
9936
  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
9937
  await runAction(() => functionsDeleteCommand(name, { confirm: opts.confirm }));
9848
9938
  });
9849
9939
  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
9940
  await runAction(() => functionsScheduleCommand(name, cron, opts));
9851
9941
  });
9942
+ functions.command("schedules").description("List function schedules with cron, timezone, state, and last/next fire times").action(async () => {
9943
+ await runAction(functionsSchedulesCommand);
9944
+ });
9852
9945
  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
9946
  await runAction(() => functionsDevCommand(file, {
9854
9947
  port: opts.port,
@@ -10084,7 +10177,7 @@ domains.command("buy <domain>").description("Buy a domain + connect it to a site
10084
10177
  domains.command("list").description("List domains this project has purchased through Amba").action(async () => {
10085
10178
  await runAction(domainsListCommand);
10086
10179
  });
10087
- const aiProviders = program.command("ai").description("AI gateway — manage provider keys + prompts").command("providers").description("Per-project provider key registration (Anthropic, OpenAI)");
10180
+ const aiProviders = program.command("ai").description("AI gateway — manage provider keys + prompts").command("providers").description("Per-project provider key registration (Anthropic, OpenAI, Mistral, Gemini)");
10088
10181
  aiProviders.command("add <provider>").description("Register an AI provider key. Plaintext stored securely server-side; a preview (first-6+last-4) is printed back. Use --from-stdin to keep the key out of shell history.").option("--key <value>", "Provider API key plaintext (alternative: --from-stdin)").option("--from-stdin", "Read the key from stdin instead of --key").action(async (provider, opts) => {
10089
10182
  await runAction(() => aiProvidersAddCommand(provider, opts));
10090
10183
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layers/amba",
3
- "version": "4.1.4",
3
+ "version": "4.1.6",
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.8",
53
- "@layers/amba-shared": "4.0.4"
52
+ "@layers/amba-mcp": "4.0.10",
53
+ "@layers/amba-shared": "4.0.5"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsdown && tsc --emitDeclarationOnly",
@@ -9,8 +9,8 @@ There is no separate "identity provisioning" step — `auth` is the default surf
9
9
  | Tool | Purpose | Example args |
10
10
  | --- | --- | --- |
11
11
  | `amba_developer_me` | Verify the developer PAT and read the developer's profile. Pre-flight check before any provisioning. | `{}` |
12
- | `amba_projects_get` | Read a project's config (bundle id, OAuth client id, platform). | `{ project_id: "p_…" }` |
13
- | `amba_projects_update` | Set `bundle_id` (Apple audience) and `google_oauth_client_id` (Google audience). Required before Sign in with Apple / Google works. | `{ project_id, bundle_id: "com.example.fitness", google_oauth_client_id: "1234.apps.googleusercontent.com" }` |
12
+ | `amba_projects_get` | Read a project's config (social audiences, magic-link callback, auth-email brand, platform). | `{ project_id: "p_…" }` |
13
+ | `amba_projects_update` | Set social audiences plus the exact magic-link callback and auth-email brand. Pass null for an auth field to restore its fallback. | `{ project_id, magic_link_redirect_url: "https://app.example.com/auth/callback", auth_email_brand_name: "My App" }` |
14
14
  | `amba_users_list` | Browse end-users (app_users) of the project — useful as a smoke check after the first sign-in. | `{ project_id, limit: 20 }` |
15
15
  | `amba_users_get` | Fetch a single app_user by id. | `{ project_id, user_id }` |
16
16
  | `amba_users_bulk_update` | Set custom properties on many users at once (e.g. backfilling a `tier` property used for segmentation). | `{ project_id, user_ids: [...], properties: { tier: "trial" } }` |
@@ -18,7 +18,7 @@ There is no separate "identity provisioning" step — `auth` is the default surf
18
18
  | `amba_delete_api_key` / `amba_api_keys_delete` | Revoke a leaked key. | `{ project_id, api_key_id }` |
19
19
  | `amba_assign_role` / `amba_roles_assign` | Grant an RBAC role to an app_user (admin / moderator / etc.). | `{ project_id, user_id, role_id }` |
20
20
 
21
- There's no `amba_auth_*` namespace — auth is owned by the SDK on the client side, and there are no provisioning calls for it beyond setting the project's audience identifiers. If the user wants Apple/Google sign-in, the **mandatory** preflight is:
21
+ There's no `amba_auth_*` namespace — auth is owned by the SDK on the client side. Project-level routing, branding, and social audience configuration use `amba_projects_update`. If the user wants Apple/Google sign-in, the **mandatory** preflight is:
22
22
 
23
23
  ```
24
24
  mcp__amba__amba_projects_update({
@@ -30,6 +30,18 @@ mcp__amba__amba_projects_update({
30
30
 
31
31
  Without this, the server rejects identity tokens with `AUDIENCE_NOT_CONFIGURED` and the user thinks Amba is broken. If they don't know their bundle id, ask; if they don't have a Google OAuth client yet, tell them to create one at console.cloud.google.com and link it later.
32
32
 
33
+ For magic-link sign-in, configure the exact HTTPS callback before sending a production email:
34
+
35
+ ```
36
+ mcp__amba__amba_projects_update({
37
+ project_id: process.env.AMBA_PROJECT_ID,
38
+ magic_link_redirect_url: "https://app.example.com/auth/callback",
39
+ auth_email_brand_name: "Example App"
40
+ })
41
+ ```
42
+
43
+ Amba appends the one-time `token` query parameter to that exact URL. Auth-email copy uses the project brand; sends from the shared platform mailbox retain visible `via Amba` attribution. A pure customer-branded From address requires a separately verified sender domain.
44
+
33
45
  ## SDK init per stack
34
46
 
35
47
  ### Expo