@layers/amba 4.1.1 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,26 @@ export interface ProjectSummary {
23
23
  google_oauth_client_id?: string | null;
24
24
  status?: string;
25
25
  created_at?: string;
26
+ /** Persisted subscription/accounting tier. */
27
+ tier?: 'free' | 'pro' | 'scale' | 'enterprise';
28
+ /** Runtime entitlement tier; differs from `tier` for comped projects. */
29
+ effective_tier?: 'free' | 'pro' | 'scale' | 'enterprise';
30
+ comped?: boolean;
31
+ }
32
+ export interface CreatedProject extends ProjectSummary {
33
+ api_keys?: {
34
+ client: {
35
+ key: string;
36
+ prefix?: string;
37
+ type?: 'client';
38
+ };
39
+ server: {
40
+ key: string;
41
+ prefix?: string;
42
+ type?: 'server';
43
+ };
44
+ };
45
+ provisioning_status?: string;
26
46
  }
27
47
  export declare function listProjects(): Promise<ApiListResponse<ProjectSummary>>;
28
48
  export declare function createProject(input: {
@@ -41,12 +61,7 @@ export declare function createProject(input: {
41
61
  * bootstrap. Defaults to `'production'` when omitted.
42
62
  */
43
63
  environment?: 'development' | 'production';
44
- }): Promise<ApiResponse<{
45
- id: string;
46
- name: string;
47
- platform: string;
48
- environment?: string;
49
- }>>;
64
+ }): Promise<ApiResponse<CreatedProject>>;
50
65
  /**
51
66
  * PATCH /admin/projects/:projectId — update mutable fields. Server
52
67
  * silently ignores unknown keys; we filter to the documented allow-list
@@ -281,6 +296,48 @@ export interface FunctionDeploymentRow {
281
296
  * function so external webhook senders can reach it. Default false. */
282
297
  is_public?: boolean;
283
298
  }
299
+ /** Provider-neutral lifecycle for an exact hostname bound to one function. */
300
+ export interface FunctionDomain {
301
+ id: string;
302
+ project_id: string;
303
+ function_name: string;
304
+ hostname: string;
305
+ cert_status: 'pending_validation' | 'pending_issuance' | 'pending_deployment' | 'active' | 'error';
306
+ ownership_status: 'pending' | 'active' | 'error';
307
+ /** True only when certificate/ownership are active and routing reconciliation succeeded. */
308
+ live: boolean;
309
+ created_at: string;
310
+ updated_at: string;
311
+ }
312
+ export interface FunctionDomainValidationRecord {
313
+ txt_name?: string;
314
+ txt_value?: string;
315
+ http_url?: string;
316
+ http_body?: string;
317
+ }
318
+ export interface FunctionDomainOwnershipVerification {
319
+ type?: string;
320
+ name?: string;
321
+ value?: string;
322
+ }
323
+ /** Attach always returns DNS instructions; refresh may add them when available. */
324
+ export interface AttachedFunctionDomain extends FunctionDomain {
325
+ dns_target: string;
326
+ /** Additive status when Amba controls the purchased domain's DNS zone. */
327
+ dns_autoconfigured?: boolean;
328
+ dns_records_published?: number;
329
+ /** True when automatic DNS publication failed and refresh should be retried. */
330
+ dns_error?: boolean;
331
+ ssl_validation: FunctionDomainValidationRecord[];
332
+ ownership_verification: FunctionDomainOwnershipVerification | null;
333
+ ownership_verification_http: {
334
+ http_url?: string;
335
+ http_body?: string;
336
+ } | null;
337
+ }
338
+ export type RefreshedFunctionDomain = FunctionDomain & Partial<Pick<AttachedFunctionDomain, 'dns_target' | 'dns_autoconfigured' | 'dns_records_published' | 'dns_error' | 'ssl_validation' | 'ownership_verification' | 'ownership_verification_http'>> & {
339
+ cleanup_pending?: boolean;
340
+ };
284
341
  export declare function recordFunctionDeployment(projectId: string, input: {
285
342
  name: string;
286
343
  runtime_script_name: string;
@@ -324,6 +381,17 @@ export declare function deployFunctionViaApi(projectId: string, input: {
324
381
  export declare function listFunctionDeployments(projectId: string, options?: {
325
382
  activeOnly?: boolean;
326
383
  }): Promise<ApiListResponse<FunctionDeploymentRow>>;
384
+ /** Attach one exact hostname to one deployed function. */
385
+ export declare function attachFunctionDomainViaApi(projectId: string, functionName: string, hostname: string): Promise<ApiResponse<AttachedFunctionDomain>>;
386
+ /** List provider-neutral ownership/certificate status for a function's hostnames. */
387
+ export declare function listFunctionDomainsViaApi(projectId: string, functionName: string): Promise<ApiListResponse<FunctionDomain>>;
388
+ /** Re-poll DNS ownership and certificate state for one exact hostname. */
389
+ export declare function refreshFunctionDomainViaApi(projectId: string, functionName: string, hostname: string): Promise<ApiResponse<RefreshedFunctionDomain>>;
390
+ /** Detach one exact hostname from a function. */
391
+ export declare function removeFunctionDomainViaApi(projectId: string, functionName: string, hostname: string): Promise<ApiResponse<{
392
+ hostname: string;
393
+ deleted: true;
394
+ }>>;
327
395
  export interface FunctionDeployContext {
328
396
  /** Dispatch namespace for the script PUT (e.g. `amba_tenant_functions`). */
329
397
  dispatch_namespace: string;
@@ -382,6 +450,11 @@ export interface FunctionLogEvent {
382
450
  export interface FunctionLogsResponse {
383
451
  events: FunctionLogEvent[];
384
452
  truncated: boolean;
453
+ /** Provider-neutral freshness for the underlying log-ingestion stream. */
454
+ ingestion?: {
455
+ status: 'current' | 'delayed' | 'no_batches' | 'unknown' | 'historical';
456
+ latest_batch_at: string | null;
457
+ };
385
458
  since: string;
386
459
  until: string;
387
460
  /** Present when `since_request_id` anchored the range — the anchor event's timestamp. */
@@ -419,7 +492,7 @@ export interface SecretSyncPendingRow {
419
492
  function_name: string | null;
420
493
  secret_name: string;
421
494
  expected_version: number;
422
- status: 'pending' | 'syncing' | 'sync_failed_retrying' | 'synced';
495
+ status: 'pending' | 'syncing' | 'sync_failed_retrying' | 'synced' | 'revocation_pending' | 'revoking' | 'revoke_failed_retrying';
423
496
  last_error: string | null;
424
497
  attempts: number;
425
498
  created_at: string;
@@ -441,7 +514,7 @@ export interface SetSecretResultRow {
441
514
  /** Backend version resource path — exposed for ops debugging. */
442
515
  version_path: string;
443
516
  /** `'pending'` until the secret sync completes. */
444
- sync_status: 'pending' | 'syncing' | 'sync_failed_retrying' | 'synced';
517
+ sync_status: 'pending';
445
518
  created_at: string;
446
519
  }
447
520
  export declare function setSecretViaApi(projectId: string, input: {
@@ -463,7 +536,7 @@ export interface ListSecretsRow {
463
536
  name: string;
464
537
  /** Latest secret-store version number. */
465
538
  version: number;
466
- sync_status: 'pending' | 'syncing' | 'sync_failed_retrying' | 'synced';
539
+ sync_status: 'pending' | 'syncing' | 'sync_failed_retrying' | 'synced' | 'revocation_pending' | 'revoking' | 'revoke_failed_retrying';
467
540
  attempts: number;
468
541
  last_error: string | null;
469
542
  updated_at: string;
@@ -473,11 +546,7 @@ export declare function listSecretsViaApi(projectId: string, options?: {
473
546
  }): Promise<ApiListResponse<ListSecretsRow>>;
474
547
  export declare function deleteSecretViaApi(projectId: string, name: string, options?: {
475
548
  function?: string;
476
- }): Promise<ApiResponse<{
477
- name: string;
478
- function: string | null;
479
- deleted: true;
480
- }>>;
549
+ }): Promise<void>;
481
550
  /** Closed set of column types the server accepts. */
482
551
  export type CollectionColumnType = 'uuid' | 'text' | 'integer' | 'bigint' | 'numeric' | 'boolean' | 'timestamptz' | 'date' | 'jsonb' | 'vector' | 'text[]' | 'integer[]' | 'bigint[]' | 'numeric[]' | 'boolean[]' | 'uuid[]';
483
552
  export interface CollectionColumn {
package/dist/bundle.d.ts CHANGED
@@ -76,8 +76,8 @@ export declare class BundleSizeError extends Error {
76
76
  export declare const HANDLER_SHAPE_FIX_MESSAGE: string;
77
77
  /** Message used when the entry file has no `export default` at all. */
78
78
  export declare const HANDLER_SHAPE_MISSING_DEFAULT_MESSAGE: string;
79
- /** Message used when the default export is present but has no `fetch` method. */
80
- export declare const HANDLER_SHAPE_NO_FETCH_MESSAGE: string;
79
+ /** Back-compatible export name for the no-supported-handler message. */
80
+ export declare const HANDLER_SHAPE_NO_FETCH_MESSAGE = "Function entrypoint must have a callable `fetch` or `scheduled` method on its default export.";
81
81
  /**
82
82
  * Thrown when the bundled entry's `export default` is not the Module Worker
83
83
  * shape (`{ fetch(req, env, ctx) }`). Raised BEFORE any upload so a wrong
@@ -0,0 +1,5 @@
1
+ /** `amba functions domains ...` exact-hostname lifecycle commands. */
2
+ export declare function functionDomainsAttachCommand(functionName: string, hostnameInput: string): Promise<void>;
3
+ export declare function functionDomainsListCommand(functionName: string): Promise<void>;
4
+ export declare function functionDomainsRefreshCommand(functionName: string, hostnameInput: string): Promise<void>;
5
+ export declare function functionDomainsRemoveCommand(functionName: string, hostnameInput: string): Promise<void>;
package/dist/index.js CHANGED
@@ -864,6 +864,7 @@ async function request(method, path, body) {
864
864
  } catch {}
865
865
  throw new ApiClientError(errorMessage, res.status, errorCode);
866
866
  }
867
+ if (res.status === 204) return void 0;
867
868
  return await res.json();
868
869
  }
869
870
  /**
@@ -880,7 +881,13 @@ async function listProjects() {
880
881
  return request("GET", "/projects");
881
882
  }
882
883
  async function createProject(input) {
883
- return request("POST", "/projects", input);
884
+ const response = await request("POST", "/projects", input);
885
+ if ("project" in response.data) return { data: {
886
+ ...response.data.project,
887
+ api_keys: response.data.api_keys,
888
+ provisioning_status: response.data.provisioning_status
889
+ } };
890
+ return response;
884
891
  }
885
892
  /**
886
893
  * PATCH /admin/projects/:projectId — update mutable fields. Server
@@ -1019,6 +1026,25 @@ async function deployFunctionViaApi(projectId, input) {
1019
1026
  async function listFunctionDeployments(projectId, options = {}) {
1020
1027
  return request("GET", `/projects/${projectId}/functions/deployments${options.activeOnly ? "?active=1" : ""}`);
1021
1028
  }
1029
+ function functionDomainsPath(projectId, functionName) {
1030
+ return `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionName)}/domains`;
1031
+ }
1032
+ /** Attach one exact hostname to one deployed function. */
1033
+ async function attachFunctionDomainViaApi(projectId, functionName, hostname) {
1034
+ return request("POST", functionDomainsPath(projectId, functionName), { hostname });
1035
+ }
1036
+ /** List provider-neutral ownership/certificate status for a function's hostnames. */
1037
+ async function listFunctionDomainsViaApi(projectId, functionName) {
1038
+ return request("GET", functionDomainsPath(projectId, functionName));
1039
+ }
1040
+ /** Re-poll DNS ownership and certificate state for one exact hostname. */
1041
+ async function refreshFunctionDomainViaApi(projectId, functionName, hostname) {
1042
+ return request("POST", `${functionDomainsPath(projectId, functionName)}/${encodeURIComponent(hostname)}/refresh`);
1043
+ }
1044
+ /** Detach one exact hostname from a function. */
1045
+ async function removeFunctionDomainViaApi(projectId, functionName, hostname) {
1046
+ return request("DELETE", `${functionDomainsPath(projectId, functionName)}/${encodeURIComponent(hostname)}`);
1047
+ }
1022
1048
  async function scheduleFunction(projectId, input) {
1023
1049
  return request("POST", `/projects/${projectId}/functions/schedules`, input);
1024
1050
  }
@@ -2002,7 +2028,11 @@ async function ensureProjectForCwd(cwd, options) {
2002
2028
  name: uniqueName,
2003
2029
  environment
2004
2030
  });
2005
- const { clientKey, serverKey } = await mintProjectKeyPair(project.data.id, environment);
2031
+ const initialKeys = project.data.api_keys;
2032
+ const { clientKey, serverKey } = initialKeys ? {
2033
+ clientKey: initialKeys.client.key,
2034
+ serverKey: initialKeys.server.key
2035
+ } : await mintProjectKeyPair(project.data.id, environment);
2006
2036
  const creds = {
2007
2037
  version: 1,
2008
2038
  project_id: project.data.id,
@@ -2924,7 +2954,7 @@ That's the entire setup. The CLI:
2924
2954
  detects on disk — Claude Code, Cursor, Windsurf.
2925
2955
  6. Verifies the PAT against the API and confirms it's good.
2926
2956
 
2927
- The Amba MCP toolset (\`amba_*\` tools — ~130 of them) is available to
2957
+ The Amba MCP toolset (\`amba_*\` tools — 500+ of them) is available to
2928
2958
  the agent immediately: pass the freshly-minted \`pat\` as an inline
2929
2959
  argument on every \`amba_*\` call in the current session. The next time
2930
2960
  your MCP client starts it picks the PAT up from the config as the
@@ -3874,6 +3904,7 @@ async function initCommand(options = {}) {
3874
3904
  const defaultProjectName = basename(cwd) || "amba-project";
3875
3905
  let projectId;
3876
3906
  let projectName;
3907
+ let createdKeys = null;
3877
3908
  if (linkedProject) {
3878
3909
  projectId = linkedProject.project_id;
3879
3910
  projectName = linkedProject.project_name;
@@ -3910,20 +3941,30 @@ async function initCommand(options = {}) {
3910
3941
  } else {
3911
3942
  const name = await prompt(` Project name (default: ${defaultProjectName}): `);
3912
3943
  const finalName = name.length > 0 ? name : defaultProjectName;
3913
- projectId = (await createProject({
3944
+ const res = await createProject({
3914
3945
  name: finalName,
3915
3946
  environment
3916
- })).data.id;
3947
+ });
3948
+ projectId = res.data.id;
3917
3949
  projectName = finalName;
3950
+ if (res.data.api_keys) createdKeys = {
3951
+ clientKey: res.data.api_keys.client.key,
3952
+ serverKey: res.data.api_keys.server.key
3953
+ };
3918
3954
  }
3919
3955
  } else {
3920
3956
  const name = await prompt(` Project name (default: ${defaultProjectName}): `);
3921
3957
  const finalName = name.length > 0 ? name : defaultProjectName;
3922
- projectId = (await createProject({
3958
+ const res = await createProject({
3923
3959
  name: finalName,
3924
3960
  environment
3925
- })).data.id;
3961
+ });
3962
+ projectId = res.data.id;
3926
3963
  projectName = finalName;
3964
+ if (res.data.api_keys) createdKeys = {
3965
+ clientKey: res.data.api_keys.client.key,
3966
+ serverKey: res.data.api_keys.server.key
3967
+ };
3927
3968
  }
3928
3969
  }
3929
3970
  if (spinner.stopped) spinner = createSpinner("minting keys");
@@ -3935,6 +3976,9 @@ async function initCommand(options = {}) {
3935
3976
  clientKey = linkedProject.client_key;
3936
3977
  if (linkedProject.server_key) serverKey = linkedProject.server_key;
3937
3978
  else serverKey = (await createApiKey(projectId, "server", environment)).data.key;
3979
+ } else if (createdKeys) {
3980
+ clientKey = createdKeys.clientKey;
3981
+ serverKey = createdKeys.serverKey;
3938
3982
  } else {
3939
3983
  const clientRes = await createApiKey(projectId, "client", environment);
3940
3984
  const serverRes = await createApiKey(projectId, "server", environment);
@@ -5562,11 +5606,11 @@ var BundleSizeError = class extends Error {
5562
5606
  * service-worker shape (`export default async function (req) { … }`) is
5563
5607
  * rejected by the runtime at upload, so we reject it before the round-trip.
5564
5608
  */
5565
- const HANDLER_SHAPE_FIX_MESSAGE = "Function entrypoint must `export default { async fetch(req, env, ctx) { … } }` (the Module Worker shape). Found a default-exported function instead.";
5609
+ const HANDLER_SHAPE_FIX_MESSAGE = "Function entrypoint must `export default { async fetch(req, env, ctx) { … } }` or `export default { async scheduled(controller, env, ctx) { … } }` (the Module Worker shape). Found a default-exported function instead.";
5566
5610
  /** Message used when the entry file has no `export default` at all. */
5567
- const HANDLER_SHAPE_MISSING_DEFAULT_MESSAGE = "Function entrypoint must `export default { async fetch(req, env, ctx) { … } }` (the Module Worker shape). No default export was found.";
5568
- /** Message used when the default export is present but has no `fetch` method. */
5569
- const HANDLER_SHAPE_NO_FETCH_MESSAGE = "Function entrypoint must `export default { async fetch(req, env, ctx) { … } }` (the Module Worker shape). The default export has no `fetch` method.";
5611
+ const HANDLER_SHAPE_MISSING_DEFAULT_MESSAGE = "Function entrypoint must `export default { async fetch(req, env, ctx) { … } }` or `export default { async scheduled(controller, env, ctx) { … } }` (the Module Worker shape). No default export was found.";
5612
+ /** Back-compatible export name for the no-supported-handler message. */
5613
+ const HANDLER_SHAPE_NO_FETCH_MESSAGE = "Function entrypoint must have a callable `fetch` or `scheduled` method on its default export.";
5570
5614
  /**
5571
5615
  * Thrown when the bundled entry's `export default` is not the Module Worker
5572
5616
  * shape (`{ fetch(req, env, ctx) }`). Raised BEFORE any upload so a wrong
@@ -5600,11 +5644,14 @@ function assertHandlerShape(mod) {
5600
5644
  const def = mod.default;
5601
5645
  if (def === void 0 || def === null) throw new HandlerShapeError(HANDLER_SHAPE_MISSING_DEFAULT_MESSAGE, "missing-default");
5602
5646
  if (typeof def === "function") {
5603
- if (typeof def.fetch !== "function") throw new HandlerShapeError(HANDLER_SHAPE_FIX_MESSAGE, "plain-function");
5647
+ const fetchOnFn = def.fetch;
5648
+ const scheduledOnFn = def.scheduled;
5649
+ if (typeof fetchOnFn !== "function" && typeof scheduledOnFn !== "function") throw new HandlerShapeError(HANDLER_SHAPE_FIX_MESSAGE, "plain-function");
5604
5650
  return;
5605
5651
  }
5606
5652
  if (typeof def !== "object") throw new HandlerShapeError(HANDLER_SHAPE_NO_FETCH_MESSAGE, "no-fetch");
5607
- if (typeof def.fetch !== "function") throw new HandlerShapeError(HANDLER_SHAPE_NO_FETCH_MESSAGE, "no-fetch");
5653
+ const handler = def;
5654
+ if (typeof handler.fetch !== "function" && typeof handler.scheduled !== "function") throw new HandlerShapeError(HANDLER_SHAPE_NO_FETCH_MESSAGE, "no-fetch");
5608
5655
  }
5609
5656
  /**
5610
5657
  * Validate that the bundled customer code exports the Module Worker handler
@@ -5727,7 +5774,7 @@ async function functionsDeployCommand(target, options = {}) {
5727
5774
  const entryPoint = await resolveFunctionEntry(target, options.entry);
5728
5775
  const defaultName = targetIsDir ? basename(target.replace(/[/\\]+$/, "")) : basename(entryPoint).replace(/\.[^.]+$/, "");
5729
5776
  const functionName = options.name ?? defaultName;
5730
- validateFunctionName(functionName);
5777
+ validateFunctionName$1(functionName);
5731
5778
  const rateLimit = parseRateLimitFlags(options);
5732
5779
  console.log();
5733
5780
  console.log(pc.bold(` amba functions deploy ${pc.cyan(functionName)}`));
@@ -5775,7 +5822,7 @@ async function functionsListCommand() {
5775
5822
  console.log();
5776
5823
  }
5777
5824
  async function functionsDeleteCommand(name, options = {}) {
5778
- validateFunctionName(name);
5825
+ validateFunctionName$1(name);
5779
5826
  if (!options.confirm || options.confirm !== name) throw new Error(`Delete is destructive. Pass --confirm ${name} to proceed. Clients calling this function will start 404'ing immediately.`);
5780
5827
  const cascade = (await deleteFunctionViaApi((await loadProjectConfig()).projectId, name, { confirm: name })).data.cascade;
5781
5828
  console.log(pc.green(" ✓") + ` Deleted ${pc.bold(name)}.`);
@@ -5783,7 +5830,7 @@ async function functionsDeleteCommand(name, options = {}) {
5783
5830
  }
5784
5831
  async function functionsScheduleCommand(name, cron, options = {}) {
5785
5832
  const projectConfig = await loadProjectConfig();
5786
- validateFunctionName(name);
5833
+ validateFunctionName$1(name);
5787
5834
  const timezone = options.tz ?? "UTC";
5788
5835
  const res = await scheduleFunction(projectConfig.projectId, {
5789
5836
  name,
@@ -6115,6 +6162,14 @@ try {
6115
6162
  };
6116
6163
  return fetchFn(req, env, ctx);
6117
6164
  };
6165
+ } else if (mod.default && typeof mod.default.scheduled === 'function') {
6166
+ // Scheduled-only modules are valid in production. This command exposes
6167
+ // an HTTP server, so return a clear 405 instead of rejecting a deployable
6168
+ // handler shape during local startup.
6169
+ handler = () => new Response(
6170
+ 'This function only exports scheduled(); use amba functions schedule/run-now to invoke it.',
6171
+ { status: 405, headers: { 'content-type': 'text/plain; charset=utf-8' } },
6172
+ );
6118
6173
  } else if (typeof mod.default === 'function') {
6119
6174
  // Legacy service-worker shape (\`export default async function (req) { ... }\`).
6120
6175
  // REJECTED — production runs functions as Module Workers and the runtime
@@ -6124,7 +6179,7 @@ try {
6124
6179
  process.stderr.write(' Error: Function entrypoint must \`export default { async fetch(req, env, ctx) { … } }\` (the Module Worker shape). Found a default-exported function instead.\\n');
6125
6180
  process.exit(2);
6126
6181
  } else {
6127
- process.stderr.write(' Error: Function entrypoint must \`export default { async fetch(req, env, ctx) { … } }\` (the Module Worker shape). The default export has no \`fetch\` method.\\n');
6182
+ process.stderr.write(' Error: Function entrypoint default export must have a callable \`fetch\` or \`scheduled\` method.\\n');
6128
6183
  process.exit(2);
6129
6184
  }
6130
6185
  } catch (err) {
@@ -6194,8 +6249,8 @@ process.on('SIGINT', () => shutdown('SIGINT'));
6194
6249
  * function-name overwrites — same upsert semantics as `amba secrets set`.
6195
6250
  */
6196
6251
  async function functionsConsumeCommand(queueName, functionName, options = {}) {
6197
- validateFunctionName(functionName);
6198
- validateFunctionName(queueName);
6252
+ validateFunctionName$1(functionName);
6253
+ validateFunctionName$1(queueName);
6199
6254
  const res = await upsertQueueBinding((await loadProjectConfig()).projectId, {
6200
6255
  queue_name: queueName,
6201
6256
  function_name: functionName,
@@ -6225,11 +6280,11 @@ async function functionsConsumersListCommand() {
6225
6280
  console.log();
6226
6281
  }
6227
6282
  async function functionsConsumersUnbindCommand(queueName) {
6228
- validateFunctionName(queueName);
6283
+ validateFunctionName$1(queueName);
6229
6284
  await deleteQueueBinding((await loadProjectConfig()).projectId, queueName);
6230
6285
  console.log(pc.green(" ✓") + ` Unbound queue ${pc.cyan(queueName)}.`);
6231
6286
  }
6232
- function validateFunctionName(name) {
6287
+ function validateFunctionName$1(name) {
6233
6288
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Invalid function name '${name}'. Must match /^[a-z][a-z0-9_-]*$/ (lowercase ASCII, digits, underscore or hyphen; must start with a letter).`);
6234
6289
  if (name.length > 58) throw new Error("Function name must be 58 characters or fewer.");
6235
6290
  }
@@ -6302,6 +6357,10 @@ async function functionsLogsCommand(functionName, options = {}) {
6302
6357
  });
6303
6358
  if (res.data.anchor && !options.json) console.error(pc.dim(` Anchored at ${res.data.anchor} (--since-request-id).`));
6304
6359
  printEvents(res.data.events, options.json);
6360
+ if (res.data.events.length === 0 && !options.json) {
6361
+ if (res.data.ingestion?.status === "delayed") console.error(pc.yellow(` ! Log ingestion is delayed${res.data.ingestion.latest_batch_at ? `; latest batch arrived ${res.data.ingestion.latest_batch_at}` : ""}. Retry shortly.`));
6362
+ else if (res.data.ingestion?.status === "no_batches") console.error(pc.yellow(" ! No log batches reached this window yet. Retry shortly."));
6363
+ }
6305
6364
  if (res.data.truncated) if (options.json) process.stderr.write(`{"truncated":true}\n`);
6306
6365
  else console.error(pc.yellow(` ! Result truncated at limit; narrow --since / --until or raise --limit.`));
6307
6366
  }
@@ -6385,6 +6444,85 @@ function sleep$1(ms) {
6385
6444
  return new Promise((r) => setTimeout(r, ms));
6386
6445
  }
6387
6446
  //#endregion
6447
+ //#region src/commands/function-domains.ts
6448
+ /** `amba functions domains ...` exact-hostname lifecycle commands. */
6449
+ const FUNCTION_NAME_RE = /^[a-z][a-z0-9_-]{0,57}$/;
6450
+ function validateFunctionName(name) {
6451
+ if (!FUNCTION_NAME_RE.test(name)) throw new Error(`Invalid function name "${name}". Use lowercase letters, numbers, hyphens, or underscores (max 58 chars; start with a letter).`);
6452
+ }
6453
+ /**
6454
+ * Normalize case/trailing-dot spelling without trying to replace the API's
6455
+ * authoritative DNS validation. This command accepts one exact hostname only:
6456
+ * schemes, paths, ports, and wildcard labels are never meaningful here.
6457
+ */
6458
+ function normalizeExactHostname(input) {
6459
+ const hostname = input.trim().toLowerCase().replace(/\.$/, "");
6460
+ if (hostname.length === 0 || hostname.length > 253 || !hostname.includes(".") || hostname.includes("*") || hostname.includes("/") || hostname.includes(":") || /\s/.test(hostname)) throw new Error(`Invalid hostname "${input}". Pass one exact DNS hostname such as feeds.example.com (no scheme, path, port, or wildcard).`);
6461
+ return hostname;
6462
+ }
6463
+ function lifecycle(domain) {
6464
+ if (domain.live) return pc.green("active");
6465
+ if (domain.cert_status === "active" && domain.ownership_status === "active") return "certificate=active, ownership=active, reconciliation=pending";
6466
+ return `certificate=${domain.cert_status}, ownership=${domain.ownership_status}`;
6467
+ }
6468
+ function printDomain(domain) {
6469
+ console.log(` ${pc.bold(domain.hostname)} ${lifecycle(domain)}`);
6470
+ }
6471
+ function printDnsInstructions(domain) {
6472
+ if (domain.dns_error === true) console.log(pc.yellow(" Automatic DNS publication failed; retry with domains refresh."));
6473
+ if ("cleanup_pending" in domain && domain.cleanup_pending === true) console.log(pc.yellow(" Routing cleanup is incomplete; retry with domains refresh."));
6474
+ 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
+ if (domain.dns_target) console.log(pc.dim(` DNS target: CNAME ${domain.hostname} → ${domain.dns_target}`));
6476
+ 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
+ else if (record.http_url && record.http_body) console.log(pc.dim(` Certificate HTTP: ${record.http_url} → ${record.http_body}`));
6478
+ const ownership = domain.ownership_verification;
6479
+ if (ownership?.name && ownership.value) console.log(pc.dim(` Ownership ${ownership.type ?? "TXT"}: ${ownership.name} = ${ownership.value}`));
6480
+ const ownershipHttp = domain.ownership_verification_http;
6481
+ if (ownershipHttp?.http_url && ownershipHttp.http_body) console.log(pc.dim(` Ownership HTTP: ${ownershipHttp.http_url} → ${ownershipHttp.http_body}`));
6482
+ }
6483
+ async function functionDomainsAttachCommand(functionName, hostnameInput) {
6484
+ validateFunctionName(functionName);
6485
+ const hostname = normalizeExactHostname(hostnameInput);
6486
+ const { projectId } = await loadProjectConfig();
6487
+ const result = await attachFunctionDomainViaApi(projectId, functionName, hostname);
6488
+ console.log();
6489
+ console.log(pc.green(" ✓") + ` Attached ${pc.bold(result.data.hostname)} to ${functionName}.`);
6490
+ console.log(pc.dim(` Status: ${lifecycle(result.data)}`));
6491
+ printDnsInstructions(result.data);
6492
+ console.log(pc.dim(" Exact hostname only; no automatic www hostname was created."));
6493
+ if (!result.data.live && result.data.ownership_status !== "active") console.log(pc.dim(" Unverified claims become eligible for reclaim after 24 hours."));
6494
+ console.log();
6495
+ }
6496
+ async function functionDomainsListCommand(functionName) {
6497
+ validateFunctionName(functionName);
6498
+ const { projectId } = await loadProjectConfig();
6499
+ const result = await listFunctionDomainsViaApi(projectId, functionName);
6500
+ console.log();
6501
+ if (result.data.length === 0) console.log(pc.dim(` No custom domains attached to ${functionName}.`));
6502
+ else for (const domain of result.data) printDomain(domain);
6503
+ console.log();
6504
+ }
6505
+ async function functionDomainsRefreshCommand(functionName, hostnameInput) {
6506
+ validateFunctionName(functionName);
6507
+ const hostname = normalizeExactHostname(hostnameInput);
6508
+ const { projectId } = await loadProjectConfig();
6509
+ const result = await refreshFunctionDomainViaApi(projectId, functionName, hostname);
6510
+ console.log();
6511
+ console.log(pc.green(" ✓") + ` Refreshed ${pc.bold(result.data.hostname)}.`);
6512
+ console.log(pc.dim(` Status: ${lifecycle(result.data)}`));
6513
+ printDnsInstructions(result.data);
6514
+ console.log();
6515
+ }
6516
+ async function functionDomainsRemoveCommand(functionName, hostnameInput) {
6517
+ validateFunctionName(functionName);
6518
+ const hostname = normalizeExactHostname(hostnameInput);
6519
+ const { projectId } = await loadProjectConfig();
6520
+ const result = await removeFunctionDomainViaApi(projectId, functionName, hostname);
6521
+ console.log();
6522
+ console.log(pc.green(" ✓") + ` Removed ${pc.bold(result.data.hostname)} from ${functionName}.`);
6523
+ console.log();
6524
+ }
6525
+ //#endregion
6388
6526
  //#region src/commands/ai.ts
6389
6527
  /**
6390
6528
  * `amba ai providers ...` — register / list / remove AI provider API keys.
@@ -6517,8 +6655,8 @@ async function secretsListCommand(options = {}) {
6517
6655
  async function secretsUnsetCommand(name, options) {
6518
6656
  validateSecretName(name);
6519
6657
  await deleteSecretViaApi((await loadProjectConfig()).projectId, name, { function: options.function });
6520
- console.log(pc.green(" ✓") + ` Removed from GCP Secret Manager.`);
6521
- console.log(pc.dim(" Workers Secret on the dispatched script remains until the next deploy — redeploy to clear."));
6658
+ console.log(pc.green(" ✓") + ` Secret removed from the backing store.`);
6659
+ console.log(pc.dim(" Workers binding revocation queued; provider failures retry automatically."));
6522
6660
  }
6523
6661
  function validateSecretName(name) {
6524
6662
  const reason = getBindingReservationReason(name);
@@ -6529,6 +6667,9 @@ function renderStatus(status) {
6529
6667
  case "synced": return pc.green("synced");
6530
6668
  case "syncing": return pc.cyan("syncing");
6531
6669
  case "sync_failed_retrying": return pc.yellow("retrying");
6670
+ case "revocation_pending": return pc.yellow("revocation pending");
6671
+ case "revoking": return pc.cyan("revoking");
6672
+ case "revoke_failed_retrying": return pc.yellow("revocation retrying");
6532
6673
  default: return pc.dim(status);
6533
6674
  }
6534
6675
  }
@@ -6618,7 +6759,10 @@ async function billingStatusCommand() {
6618
6759
  try {
6619
6760
  const { projectId } = await loadProjectConfig();
6620
6761
  const s = (await adminGet(`/projects/${projectId}/billing/status`)).data;
6621
- console.log(` Tier: ${pc.bold(s.tier)}`);
6762
+ const effectiveTier = s.effective_tier ?? s.tier;
6763
+ console.log(` Effective tier: ${pc.bold(effectiveTier)}`);
6764
+ if (effectiveTier !== s.tier) console.log(` Stored tier: ${s.tier}`);
6765
+ if (s.comped === true) console.log(` Billing: ${pc.green("comped")} ${pc.dim("(no customer charge)")}`);
6622
6766
  console.log(` Subscription: ${s.subscription_status ?? pc.dim("—")}`);
6623
6767
  console.log(` Next bill anchor: ${s.current_period_end ? new Date(s.current_period_end).toLocaleDateString() : pc.dim("—")}`);
6624
6768
  const pctCeiling = s.ceiling_pct_consumed === null || s.ceiling_pct_consumed === void 0 ? null : Math.round(s.ceiling_pct_consumed * 100);
@@ -9530,7 +9674,7 @@ async function diffConfigCommand(file = DEFAULT_BUNDLE_FILE, options = {}) {
9530
9674
  //#endregion
9531
9675
  //#region src/index.ts
9532
9676
  const program = new Command();
9533
- program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.1.1");
9677
+ program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.1.2");
9534
9678
  program.option("--token <pat>", "Use a Personal Access Token for headless / CI / agent use (overrides ~/.amba/credentials.json + AMBA_PAT env)");
9535
9679
  program.hook("preAction", (thisCommand) => {
9536
9680
  setBearerOverride(resolveTokenSource({
@@ -9678,8 +9822,8 @@ functions.command("deploy <target>").description("Bundle a function (a single fi
9678
9822
  functions.command("list").description("List active functions for the current project").action(async () => {
9679
9823
  await runAction(functionsListCommand);
9680
9824
  });
9681
- functions.command("delete <name>").description("Disable + remove a function from the dispatch namespace").action(async (name) => {
9682
- await runAction(() => functionsDeleteCommand(name));
9825
+ 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) => {
9826
+ await runAction(() => functionsDeleteCommand(name, { confirm: opts.confirm }));
9683
9827
  });
9684
9828
  functions.command("schedule <name> <cron>").description("Register a cron schedule that invokes a deployed function").option("--tz <iana>", "IANA timezone for the schedule (default: UTC)").action(async (name, cron, opts) => {
9685
9829
  await runAction(() => functionsScheduleCommand(name, cron, opts));
@@ -9711,8 +9855,21 @@ consumers.command("list").description("List queue bindings for the current proje
9711
9855
  consumers.command("unbind <queue-name>").description("Remove the binding for a queue (DROP)").action(async (queueName) => {
9712
9856
  await runAction(() => functionsConsumersUnbindCommand(queueName));
9713
9857
  });
9858
+ const functionDomains = functions.command("domains").description("Manage exact custom hostnames for functions");
9859
+ functionDomains.command("attach <name> <hostname>").description("Attach one exact hostname to a deployed function").action(async (name, hostname) => {
9860
+ await runAction(() => functionDomainsAttachCommand(name, hostname));
9861
+ });
9862
+ functionDomains.command("list <name>").description("List custom hostname ownership and certificate status").action(async (name) => {
9863
+ await runAction(() => functionDomainsListCommand(name));
9864
+ });
9865
+ functionDomains.command("refresh <name> <hostname>").description("Refresh DNS ownership and certificate status").action(async (name, hostname) => {
9866
+ await runAction(() => functionDomainsRefreshCommand(name, hostname));
9867
+ });
9868
+ functionDomains.command("remove <name> <hostname>").description("Detach an exact hostname from a function").action(async (name, hostname) => {
9869
+ await runAction(() => functionDomainsRemoveCommand(name, hostname));
9870
+ });
9714
9871
  const secrets = program.command("secrets").description("Function secrets (GCP canonical, Workers-Secret synced)");
9715
- secrets.command("set <name> [value]").description("Store a secret and queue its sync to your deployed function(s). Omit --function for a project-wide secret. Use --from-stdin to keep the value out of shell history.").option("--function <name>", "Function the secret binds to. OMIT this flag to set a PROJECT-WIDE secret — one value visible to every function in the project (and to any function deployed later). With it, the secret is scoped to just that function. Use the same name as the entry passed to 'amba functions deploy'. Secrets may be set before the function is deployed; the sync drains once you deploy.").option("--env <env>", "'dev' | 'prod' — INFORMATIONAL ONLY. Both share one secret namespace per project. Use distinct secret names (e.g. STRIPE_KEY_DEV / STRIPE_KEY_PROD) or two amba projects for real env isolation.").option("--from-stdin", "Read the secret value from stdin instead of the positional arg. Mutually exclusive with <value>. Pipe in: `echo $KEY | amba secrets set NAME --from-stdin`.").action(async (name, value, opts) => {
9872
+ secrets.command("set <name> [value]").description("Store a secret and queue its sync to your deployed function(s). Omit --function for a project-wide secret. Use --from-stdin to keep the value out of shell history.").option("--function <name>", "Function the secret binds to. OMIT this flag to set a PROJECT-WIDE secret — one value visible to every function in the project (and to any function deployed later). With it, the secret is scoped to just that function and overrides a same-name project-wide value. Use the same name as the entry passed to 'amba functions deploy'. Secrets may be set before the function is deployed; the sync drains once you deploy.").option("--env <env>", "'dev' | 'prod' — INFORMATIONAL ONLY. Both share one secret namespace per project. Use distinct secret names (e.g. STRIPE_KEY_DEV / STRIPE_KEY_PROD) or two amba projects for real env isolation.").option("--from-stdin", "Read the secret value from stdin instead of the positional arg. Mutually exclusive with <value>. Pipe in: `echo $KEY | amba secrets set NAME --from-stdin`.").action(async (name, value, opts) => {
9716
9873
  await runAction(() => secretsSetCommand(name, value, {
9717
9874
  function: opts.function,
9718
9875
  env: opts.env ?? "dev",
@@ -9722,7 +9879,7 @@ secrets.command("set <name> [value]").description("Store a secret and queue its
9722
9879
  secrets.command("list").description("List secret sync status for the current project").option("--function <name>", "Filter to secrets for a single function (avoids listing all functions at once)").action(async (opts) => {
9723
9880
  await runAction(() => secretsListCommand({ function: opts.function }));
9724
9881
  });
9725
- secrets.command("unset <name>").description("Remove a secret (Worker binding cleared on next deploy)").option("--function <name>", "Function the secret was bound to. Omit to remove the project-wide secret of this name.").action(async (name, opts) => {
9882
+ secrets.command("unset <name>").description("Remove a secret and revoke its Worker binding").option("--function <name>", "Function the secret was bound to. Omit to remove the project-wide secret of this name. A surviving same-name value at the other scope is restored automatically.").action(async (name, opts) => {
9726
9883
  await runAction(() => secretsUnsetCommand(name, opts));
9727
9884
  });
9728
9885
  const billing = program.command("billing").description("Per-project subscription, headroom, and spend controls");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layers/amba",
3
- "version": "4.1.1",
3
+ "version": "4.1.2",
4
4
  "description": "amba — agent-native backend-as-a-service. Functions, collections, storage, AI, email, queues, sites, and `amba ship` to take an Expo app live on the App Store + Google Play. `npx @layers/amba init` to start.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,8 +49,8 @@
49
49
  "tsdown": "^0.12.5",
50
50
  "typescript": "^5.8.3",
51
51
  "vitest": "^3.2.4",
52
- "@layers/amba-mcp": "4.0.6",
53
- "@layers/amba-shared": "4.0.4"
52
+ "@layers/amba-shared": "4.0.4",
53
+ "@layers/amba-mcp": "4.0.6"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsdown && tsc --emitDeclarationOnly",
@@ -89,7 +89,7 @@ Check whether a project exists in this directory:
89
89
  > }'
90
90
  > ```
91
91
  > The response is the standard MCP envelope `{"jsonrpc":"2.0","id":"1","result":{"content":[{"type":"text","text":"<JSON body>"}]}}`. You construct calls by hand the same way you would with typed tools — every `amba_*` tool name from the catalog works. This is the same wire the typed-tool surface uses; you just skip the client-side configuration loop.
92
- > 2. **The `amba` CLI via bash.** Already authenticated via `~/.amba/credentials.json`. Coverage: `amba projects {list,create,update,show,delete}`, `amba collections {create,alter,list,drop}`, `amba functions {deploy,list,delete,schedule,dev,logs,consume}`, `amba push test`, `amba secrets {set,list,unset}`, `amba config {list,set}`, `amba sites <subcommands>`, `amba seed`, `amba status --detailed`, `amba logs tail`, `amba analytics export`, `amba schema export`, `amba ai <subcommands>`, `amba db migrate`, `amba billing {status,upgrade,portal,set-ceiling}`, `amba claim <email>` (sandbox → verified_free magic link). Run `amba --help` once if you need to see the full subcommand surface. Use this when you want a typed wrapper for a common op; the JSON-RPC path is strictly more flexible.
92
+ > 2. **The `amba` CLI via bash.** Already authenticated via `~/.amba/credentials.json`. Coverage: `amba projects {list,create,update,show,delete}`, `amba collections {create,alter,list,drop}`, `amba functions {deploy,list,delete,schedule,dev,logs,consume,domains}`, `amba push test`, `amba secrets {set,list,unset}`, `amba config {list,set}`, `amba sites <subcommands>`, `amba seed`, `amba status --detailed`, `amba logs tail`, `amba analytics export`, `amba schema export`, `amba ai <subcommands>`, `amba db migrate`, `amba billing {status,upgrade,portal,set-ceiling}`, `amba claim <email>` (sandbox → verified_free magic link). Run `amba --help` once if you need to see the full subcommand surface. Use this when you want a typed wrapper for a common op; the JSON-RPC path is strictly more flexible.
93
93
  > 3. **Wait for next agent launch.** The MCP entry is already persisted; on the next launch, `mcp__amba__*` tools appear automatically.
94
94
  >
95
95
  > A SEPARATE case: if Amba MCP IS wired in this session but the inbound Bearer is stale (e.g., the user just ran `amba_developer_signup` and got a fresh PAT), pass `pat: "<the new PAT>"` as an inline argument on every subsequent `amba_*` tool call to override the inbound Bearer for that one call. This only helps when the `mcp__amba__*` tools already exist in your tool list — it does not summon them into existence.
@@ -37,6 +37,9 @@ Run user code in a sandbox triggered by HTTP, cron, or webhook. The function get
37
37
  | Tool | Purpose | Example args |
38
38
  | --- | --- | --- |
39
39
  | `amba_functions_deploy` / `amba_deploy_function` | Deploy a function from source (TypeScript / JavaScript / Python). | `{ project_id, name: "send_welcome_email", runtime: "node22", source: "export default async (req) => { ... }", trigger: { type: "http" } }` |
40
+ | `amba_function_domains_attach` | Attach an exact hostname to a function and return DNS validation records. | `{ project_id, name: "feed", hostname: "feeds.example.com" }` |
41
+ | `amba_function_domains_list` / `amba_function_domains_refresh` | Read or refresh ownership/certificate status. | `{ project_id, name: "feed", hostname: "feeds.example.com" }` |
42
+ | `amba_function_domains_remove` | Detach an exact function hostname. | `{ project_id, name: "feed", hostname: "feeds.example.com" }` |
40
43
  | `amba_functions_list` / `amba_list_functions` | List functions. | `{ project_id }` |
41
44
  | `amba_functions_get` / `amba_get_function` | Read function metadata. | `{ project_id, function_id }` |
42
45
  | `amba_functions_get_logs` / `amba_get_function_logs` | Recent invocation logs. | `{ project_id, function_id, limit: 100 }` |
@@ -46,6 +49,11 @@ Run user code in a sandbox triggered by HTTP, cron, or webhook. The function get
46
49
  | `amba_functions_resume_schedule` / `amba_resume_function_schedule` | Resume. | `{ project_id, function_id }` |
47
50
  | `amba_functions_trigger_schedule` / `amba_trigger_function_schedule` | Fire a scheduled function ad-hoc (testing). | `{ project_id, function_id }` |
48
51
 
52
+ Function-domain MVP limits use effective tier: free 1, pro 5, scale 20,
53
+ enterprise/comped 50. Attach is limited to five attempts per project per hour.
54
+ Unverified claims become eligible for exact-host reclaim after 24 hours; Worker
55
+ route/KV allocation waits until provider ownership is active.
56
+
49
57
  ### AI prompts
50
58
 
51
59
  Managed LLM templates: a stored prompt with provider + model + system message, invoked by name from the SDK. The actual LLM call is rewritten server-side per-tenant — the customer's provider API key (Anthropic / OpenAI / Mistral / Gemini) stays server-side, never on the device.
@@ -80,7 +88,7 @@ Managed LLM templates: a stored prompt with provider + model + system message, i
80
88
 
81
89
  | Tool | Purpose | Example args |
82
90
  | --- | --- | --- |
83
- | `amba_secrets_set` / `amba_set_secret` | Set a **function-scoped** secret (encrypted at rest; bound on the next deploy). For third-party API keys called from your functions — NOT AI provider keys (use `amba_ai_providers_set` for those). | `{ project_id, name: "STRIPE_WEBHOOK_SECRET", value: "whsec_..." }` |
91
+ | `amba_secrets_set` / `amba_set_secret` | Set or rotate a function secret. Omit `function` for a project-wide value or pass it for one function. Already-deployed functions receive the new binding asynchronously; a future deploy also reconciles it. For third-party API keys called from your functions — NOT AI provider keys (use `amba_ai_providers_set` for those). | `{ project_id, name: "STRIPE_WEBHOOK_SECRET", value: "whsec_..." }` |
84
92
  | `amba_secrets_get` / `amba_get_secret` | Read a secret (returns `"<redacted>"` unless explicitly requested). | `{ project_id, name }` |
85
93
  | `amba_secrets_list` / `amba_list_secrets` | List secret names. | `{ project_id }` |
86
94
  | `amba_secrets_delete` / `amba_delete_secret` | Delete. | `{ project_id, name }` |