@layers/amba 4.0.2 → 4.0.3

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.
@@ -348,11 +348,14 @@ export declare function scheduleFunction(projectId: string, input: {
348
348
  schedule_id: string;
349
349
  }>>;
350
350
  export interface FunctionLogEvent {
351
- /** Logpush row — see `r2-log-reader.ts:LogpushTraceEvent`. Field-shape stable. */
351
+ /** Sanitized log row — see `r2-log-reader.ts:SanitizedLogEvent`. Field-shape stable. */
352
352
  Event?: {
353
353
  Timestamp?: string;
354
354
  };
355
- ScriptName?: string;
355
+ /** The function's name (parsed server-side from the internal script name). */
356
+ function_name?: string;
357
+ /** The deploy version this event came from. */
358
+ version?: number;
356
359
  Outcome?: string;
357
360
  Logs?: Array<{
358
361
  Level?: string;
@@ -417,8 +420,8 @@ export declare function listSecretSyncStatus(projectId: string): Promise<ApiList
417
420
  export interface SetSecretResultRow {
418
421
  /** Secret name as set by the customer. */
419
422
  name: string;
420
- /** Function name the secret binds to. */
421
- function: string;
423
+ /** Function the secret binds to, or `null` for a project-wide secret. */
424
+ function: string | null;
422
425
  /** Secret store version number (1, 2, 3, ...). */
423
426
  version: number;
424
427
  /** Backend version resource path — exposed for ops debugging. */
@@ -432,12 +435,16 @@ export declare function setSecretViaApi(projectId: string, input: {
432
435
  name: string;
433
436
  /** Plaintext secret value. <=64KiB. */
434
437
  value: string;
435
- /** Function name the secret binds to. v1 has no project-wide secrets. */
436
- function: string;
438
+ /**
439
+ * Function the secret binds to. OMIT for a PROJECT-WIDE secret
440
+ * (applied to every function in the project, including ones deployed
441
+ * later).
442
+ */
443
+ function?: string;
437
444
  }): Promise<ApiResponse<SetSecretResultRow>>;
438
445
  export interface ListSecretsRow {
439
- /** Function name the secret binds to. */
440
- function: string;
446
+ /** Function the secret binds to, or `null` for a project-wide secret. */
447
+ function: string | null;
441
448
  /** Secret name. */
442
449
  name: string;
443
450
  /** Latest secret-store version number. */
@@ -447,12 +454,14 @@ export interface ListSecretsRow {
447
454
  last_error: string | null;
448
455
  updated_at: string;
449
456
  }
450
- export declare function listSecretsViaApi(projectId: string): Promise<ApiListResponse<ListSecretsRow>>;
451
- export declare function deleteSecretViaApi(projectId: string, name: string, options: {
452
- function: string;
457
+ export declare function listSecretsViaApi(projectId: string, options?: {
458
+ function?: string;
459
+ }): Promise<ApiListResponse<ListSecretsRow>>;
460
+ export declare function deleteSecretViaApi(projectId: string, name: string, options?: {
461
+ function?: string;
453
462
  }): Promise<ApiResponse<{
454
463
  name: string;
455
- function: string;
464
+ function: string | null;
456
465
  deleted: true;
457
466
  }>>;
458
467
  /** Closed set of column types the server accepts. */
@@ -579,9 +588,23 @@ export interface SiteDomainRow {
579
588
  hostname: string;
580
589
  provider_hostname_id: string | null;
581
590
  cert_status: 'pending_validation' | 'pending_issuance' | 'pending_deployment' | 'active' | 'error';
591
+ ownership_status: 'pending' | 'active' | 'error';
582
592
  created_at: string;
583
593
  updated_at: string;
584
594
  }
595
+ /** A DNS record the customer must publish to validate / prove ownership. */
596
+ export interface DomainValidationRecord {
597
+ txt_name?: string;
598
+ txt_value?: string;
599
+ http_url?: string;
600
+ http_body?: string;
601
+ }
602
+ /** Hostname-ownership token returned as a DNS TXT record. */
603
+ export interface DomainOwnershipVerification {
604
+ type?: string;
605
+ name?: string;
606
+ value?: string;
607
+ }
585
608
  export declare function createSite(projectId: string, input: {
586
609
  name: string;
587
610
  }): Promise<ApiResponse<SiteRow>>;
@@ -622,7 +645,16 @@ export declare function addSiteDomainViaApi(projectId: string, siteName: string,
622
645
  hostname: string;
623
646
  provider_hostname_id: string;
624
647
  cert_status: SiteDomainRow["cert_status"];
648
+ ownership_status: SiteDomainRow["ownership_status"];
625
649
  dns_target: string;
650
+ is_apex: boolean;
651
+ dns_note: string | null;
652
+ ssl_validation: DomainValidationRecord[];
653
+ ownership_verification: DomainOwnershipVerification | null;
654
+ ownership_verification_http: {
655
+ http_url?: string;
656
+ http_body?: string;
657
+ } | null;
626
658
  created_at: string;
627
659
  }>>;
628
660
  /**
@@ -654,6 +686,51 @@ export declare function deleteSiteViaApi(projectId: string, siteName: string, op
654
686
  deleted: true;
655
687
  cascade: DeleteCascadeSummary;
656
688
  }>>;
689
+ /** A candidate domain from a search. */
690
+ export interface DomainSuggestion {
691
+ domain: string;
692
+ available: boolean;
693
+ price_usd: number | null;
694
+ currency: string;
695
+ premium: boolean;
696
+ }
697
+ /** Authoritative availability + price for a specific domain. */
698
+ export interface DomainAvailability extends DomainSuggestion {
699
+ supported: boolean;
700
+ }
701
+ /** Purchase response — either a quote (executed=false) or a result. */
702
+ export interface DomainPurchaseResult {
703
+ domain: string;
704
+ site: string;
705
+ executed: boolean;
706
+ available?: boolean;
707
+ price_usd?: number | null;
708
+ currency?: string;
709
+ premium?: boolean;
710
+ confirmation_required?: boolean;
711
+ gated?: boolean;
712
+ live?: boolean;
713
+ registered?: boolean;
714
+ provisioned?: boolean;
715
+ cert_status?: string;
716
+ ownership_status?: string;
717
+ dns_autoconfigured?: boolean;
718
+ url?: string;
719
+ public_url?: string;
720
+ message?: string;
721
+ }
722
+ export declare function searchDomainsViaApi(projectId: string, query: string, limit?: number): Promise<ApiListResponse<DomainSuggestion>>;
723
+ export declare function checkDomainsViaApi(projectId: string, domains: string[]): Promise<ApiListResponse<DomainAvailability>>;
724
+ export declare function purchaseDomainViaApi(projectId: string, body: {
725
+ domain: string;
726
+ site: string;
727
+ accept_price_usd?: number;
728
+ confirm?: boolean;
729
+ auto_renew?: boolean;
730
+ privacy?: boolean;
731
+ years?: number;
732
+ }): Promise<ApiResponse<DomainPurchaseResult>>;
733
+ export declare function listPurchasedDomainsViaApi(projectId: string): Promise<ApiListResponse<Record<string, unknown>>>;
657
734
  /**
658
735
  * Delete a function entirely. Cascade: removes the deployed script
659
736
  * (backend 404 treated as success), then marks every historical
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `amba domains ...` commands — buy a domain through Amba.
3
+ *
4
+ * amba domains search <query> find available domains + prices
5
+ * amba domains check <domain...> authoritative availability + price
6
+ * amba domains buy <domain> --site s buy + connect to a site
7
+ * amba domains list domains this project has purchased
8
+ *
9
+ * Search + check are free. `buy` surfaces the cost and requires explicit
10
+ * confirmation (interactive y/N, or `--yes`) before it submits a purchase —
11
+ * and the API itself only executes a real, paid registration when the
12
+ * platform has live purchasing enabled. A registered domain is auto-
13
+ * connected to the chosen site with no DNS setup by the customer.
14
+ */
15
+ export declare function domainsSearchCommand(query: string, options?: {
16
+ limit?: number;
17
+ }): Promise<void>;
18
+ export declare function domainsCheckCommand(domains: string[]): Promise<void>;
19
+ export interface DomainsBuyOptions {
20
+ site: string;
21
+ yes?: boolean;
22
+ noPrivacy?: boolean;
23
+ noAutoRenew?: boolean;
24
+ years?: number;
25
+ }
26
+ export declare function domainsBuyCommand(domain: string, options: DomainsBuyOptions): Promise<void>;
27
+ export declare function domainsListCommand(): Promise<void>;
@@ -9,8 +9,8 @@
9
9
  * Output formatting:
10
10
  * - `--json`: NDJSON to stdout (one event per line) so `| jq` works.
11
11
  * - default: human-readable lines:
12
- * 2026-05-08T12:34:56.789Z scan-letter-v3 [info] "scanning"
13
- * 2026-05-08T12:34:57.012Z scan-letter-v3 [exception] TypeError: …
12
+ * 2026-05-08T12:34:56.789Z scan-letter@v3 [info] "scanning"
13
+ * 2026-05-08T12:34:57.012Z scan-letter@v3 [exception] TypeError: …
14
14
  *
15
15
  * Server-side scoping is enforced — the API filters by ScriptName
16
16
  * prefix so a developer can never read another tenant's logs.
@@ -55,11 +55,12 @@ export interface DevOptions {
55
55
  * — externalization rules, size cap, and source-map handling are
56
56
  * identical so dev → prod parity is automatic.
57
57
  *
58
- * Handler shape: `export default async function (req: Request): Promise<Response>`.
58
+ * Handler shape: `export default { async fetch(req, env, ctx) { ... } }`.
59
59
  * `.env.local` values in the customer's working directory are passed
60
- * through to the child process so the handler can read them via
61
- * `process.env.MY_KEY`. Existing values in the parent's env take
62
- * priority (so `MY_KEY=x amba functions dev …` wins).
60
+ * through to the child process and bridged into the `env` parameter so
61
+ * the handler reads them the same way as in production (`env.MY_KEY`).
62
+ * Existing values in the parent's env take priority (so
63
+ * `MY_KEY=x amba functions dev …` wins).
63
64
  */
64
65
  export declare function functionsDevCommand(entryPoint: string, options?: DevOptions): Promise<void>;
65
66
  /**
@@ -1,13 +1,25 @@
1
1
  /**
2
- * `amba secrets ...` — write per-function secrets through the platform
3
- * API. The CLI sends `{name, value, function}` to the admin endpoint;
4
- * the server stores the value, validates reserved binding names, and
5
- * propagates it to the deployed Worker's binding. Sync is eventual —
6
- * `amba secrets list` shows progress.
2
+ * `amba secrets ...` — write secrets through the platform API. The CLI
3
+ * sends `{name, value, function?}` to the admin endpoint; the server
4
+ * stores the value, validates reserved binding names, and propagates it to
5
+ * the deployed Worker binding(s). Sync is eventual — `amba secrets list`
6
+ * shows progress.
7
+ *
8
+ * Two scopes:
9
+ * - `--function <name>` → the secret binds to that one function.
10
+ * - no `--function` → a PROJECT-WIDE secret, visible to every
11
+ * function in the project (and to any function deployed later). Set it
12
+ * once instead of repeating the same value per function.
13
+ *
14
+ * Secrets may be set BEFORE the function is deployed. The sync drains once
15
+ * a deployment lands (and `amba functions deploy` re-syncs at the end).
7
16
  */
8
17
  export interface SecretsSetOptions {
9
- /** Function name the secret binds to. Required — there are no project-wide secrets. */
10
- function: string;
18
+ /**
19
+ * Function the secret binds to. Omit for a PROJECT-WIDE secret (applied
20
+ * to every function in the project, including ones deployed later).
21
+ */
22
+ function?: string;
11
23
  /**
12
24
  * Environment selector. Currently informational: both `dev` and `prod`
13
25
  * write to the same secret namespace per project. Workarounds:
@@ -23,7 +35,15 @@ export interface SecretsSetOptions {
23
35
  fromStdin?: boolean;
24
36
  }
25
37
  export declare function secretsSetCommand(name: string, value: string | undefined, options: SecretsSetOptions): Promise<void>;
26
- export declare function secretsListCommand(): Promise<void>;
38
+ export interface SecretsListOptions {
39
+ /**
40
+ * Filter to secrets for a single function. Without this, all secrets
41
+ * across all functions in the project are listed — noisy when a
42
+ * project has 10+ functions.
43
+ */
44
+ function?: string;
45
+ }
46
+ export declare function secretsListCommand(options?: SecretsListOptions): Promise<void>;
27
47
  export declare function secretsUnsetCommand(name: string, options: {
28
- function: string;
48
+ function?: string;
29
49
  }): Promise<void>;
package/dist/index.js CHANGED
@@ -377,7 +377,7 @@ async function request(method, path, body) {
377
377
  try {
378
378
  const errorBody = await res.json();
379
379
  if (errorBody.error?.message) {
380
- errorMessage = errorBody.error.message;
380
+ errorMessage = `${res.status} — ${errorBody.error.message}`;
381
381
  errorCode = errorBody.error.code;
382
382
  }
383
383
  } catch {}
@@ -498,7 +498,7 @@ async function streamUsersExport(projectId, query = {}) {
498
498
  try {
499
499
  const errorBody = await res.json();
500
500
  if (errorBody.error?.message) {
501
- errorMessage = errorBody.error.message;
501
+ errorMessage = `${res.status} — ${errorBody.error.message}`;
502
502
  errorCode = errorBody.error.code;
503
503
  }
504
504
  } catch {}
@@ -560,12 +560,12 @@ async function deleteQueueBinding(projectId, queueName) {
560
560
  async function setSecretViaApi(projectId, input) {
561
561
  return request("POST", `/projects/${projectId}/secrets`, input);
562
562
  }
563
- async function listSecretsViaApi(projectId) {
564
- return request("GET", `/projects/${projectId}/secrets`);
563
+ async function listSecretsViaApi(projectId, options = {}) {
564
+ return request("GET", `/projects/${projectId}/secrets${options.function ? `?${new URLSearchParams({ function: options.function }).toString()}` : ""}`);
565
565
  }
566
- async function deleteSecretViaApi(projectId, name, options) {
567
- const qs = new URLSearchParams({ function: options.function });
568
- return request("DELETE", `/projects/${projectId}/secrets/${encodeURIComponent(name)}?${qs.toString()}`);
566
+ async function deleteSecretViaApi(projectId, name, options = {}) {
567
+ const qs = options.function ? `?${new URLSearchParams({ function: options.function }).toString()}` : "";
568
+ return request("DELETE", `/projects/${projectId}/secrets/${encodeURIComponent(name)}${qs}`);
569
569
  }
570
570
  async function createCollection(projectId, input) {
571
571
  return request("POST", `/projects/${projectId}/collections`, input);
@@ -645,6 +645,20 @@ async function rollbackSiteViaApi(projectId, siteName, deploymentId) {
645
645
  async function deleteSiteViaApi(projectId, siteName, options) {
646
646
  return request("DELETE", `/projects/${projectId}/sites/${encodeURIComponent(siteName)}?confirm=${encodeURIComponent(options.confirm)}`);
647
647
  }
648
+ async function searchDomainsViaApi(projectId, query, limit) {
649
+ const params = new URLSearchParams({ q: query });
650
+ if (limit !== void 0) params.set("limit", String(limit));
651
+ return request("GET", `/projects/${projectId}/domains/search?${params.toString()}`);
652
+ }
653
+ async function checkDomainsViaApi(projectId, domains) {
654
+ return request("POST", `/projects/${projectId}/domains/check`, { domains });
655
+ }
656
+ async function purchaseDomainViaApi(projectId, body) {
657
+ return request("POST", `/projects/${projectId}/domains/purchase`, body);
658
+ }
659
+ async function listPurchasedDomainsViaApi(projectId) {
660
+ return request("GET", `/projects/${projectId}/domains`);
661
+ }
648
662
  /**
649
663
  * Delete a function entirely. Cascade: removes the deployed script
650
664
  * (backend 404 treated as success), then marks every historical
@@ -2318,7 +2332,7 @@ All SDKs expose the same surface: \`Amba.configure({ projectId, apiKey })\`, the
2318
2332
  - **challenges** — time-bounded goals. Define with \`amba_challenges_create\`; progress via SDK.
2319
2333
 
2320
2334
  ### Economy
2321
- - **currencies** — virtual currencies (coins, gems). Define with \`amba_currencies_create\`; grant via \`amba_currencies_grant\` or event rules via \`amba_currency_grant_rules_create\`.
2335
+ - **currencies** — virtual currencies (coins, gems). Define with \`amba_currencies_create\`; grant via \`amba_currencies_grant\` or event rules via \`amba_currency_grant_rules_create\`; debit via \`amba_currencies_spend\` (atomic, rejects on insufficient funds).
2322
2336
  - **catalog + stores** — purchasable items + storefronts. Chain: \`amba_catalog_items_create\` → \`amba_catalog_items_set_price\` → \`amba_stores_create\` → \`amba_stores_add_listing\`. (Define currency first.)
2323
2337
  - **inventory** — items users own. Read via SDK \`Amba.inventory.*\`; grant with \`amba_inventory_grant_item\`.
2324
2338
 
@@ -2341,7 +2355,7 @@ All SDKs expose the same surface: \`Amba.configure({ projectId, apiKey })\`, the
2341
2355
  - **sites** — static site hosting at \`*.app.amba.host\`. Deploy with \`amba_sites_deploy\`.
2342
2356
  - **media** — file storage + CDN. Upload via \`amba_media_upload\`.
2343
2357
  - **secrets** — env vars for functions. Set via \`amba_secrets_set\`.
2344
- - **configs** — remote config flags. Define with \`amba_configs_create\`.
2358
+ - **configs** — remote config flags. Define with \`amba_remote_configs_create\`.
2345
2359
  - **integrations** — third-party webhooks (RevenueCat, Superwall, AppsFlyer, etc.). Configure with \`amba_integrations_configure\`.
2346
2360
  - **ai_prompts** — versioned LLM prompts callable from SDK. Define with \`amba_ai_prompts_create\`; call via \`amba_ai_prompts_invoke\`.
2347
2361
  `;
@@ -4055,7 +4069,7 @@ async function configSetCommand(key, rawValue) {
4055
4069
  }
4056
4070
  //#endregion
4057
4071
  //#region src/commands/projects.ts
4058
- function confirm(question) {
4072
+ function confirm$1(question) {
4059
4073
  const rl = createInterface({
4060
4074
  input: process.stdin,
4061
4075
  output: process.stdout
@@ -4216,7 +4230,7 @@ async function projectsDeleteCommand(projectId, opts = {}) {
4216
4230
  console.log(pc.dim(" ─────────────────────────────────"));
4217
4231
  console.log();
4218
4232
  if (!opts.yes) {
4219
- if (!await confirm(pc.yellow(` Delete project ${projectId}? This is irreversible. (y/N) `))) {
4233
+ if (!await confirm$1(pc.yellow(` Delete project ${projectId}? This is irreversible. (y/N) `))) {
4220
4234
  console.log(pc.dim(" Aborted."));
4221
4235
  console.log();
4222
4236
  return;
@@ -5094,11 +5108,21 @@ async function functionsDeployCommand(entryPoint, options = {}) {
5094
5108
  return;
5095
5109
  }
5096
5110
  console.log(pc.dim(" Uploading…"));
5097
- const result = await deployFunctionViaApi(projectId, {
5098
- name: functionName,
5099
- bundleCode: bundle.code,
5100
- rate_limit: rateLimit
5101
- });
5111
+ let result;
5112
+ try {
5113
+ result = await deployFunctionViaApi(projectId, {
5114
+ name: functionName,
5115
+ bundleCode: bundle.code,
5116
+ rate_limit: rateLimit
5117
+ });
5118
+ } catch (err) {
5119
+ if (err instanceof ApiClientError && err.statusCode === 503) {
5120
+ const base = err.message.trimEnd();
5121
+ const sep = /[.!?]$/.test(base) ? " " : ". ";
5122
+ throw new Error(`${base}${sep}The function platform is temporarily unavailable; retry in ~30s.`);
5123
+ }
5124
+ throw err;
5125
+ }
5102
5126
  console.log(pc.green(" ✓") + ` Deployed ${pc.cyan(functionName)} ${pc.dim(`v${result.data.version}`)}`);
5103
5127
  console.log(pc.green(" ✓") + ` URL: ${pc.underline(result.fn_url)}`);
5104
5128
  if (rateLimit) console.log(pc.dim(` Rate limit: ${rateLimit.max} per ${rateLimit.window} (key=${rateLimit.key}) — enforced pre-dispatch`));
@@ -5153,11 +5177,12 @@ async function functionsScheduleCommand(name, cron, options = {}) {
5153
5177
  * — externalization rules, size cap, and source-map handling are
5154
5178
  * identical so dev → prod parity is automatic.
5155
5179
  *
5156
- * Handler shape: `export default async function (req: Request): Promise<Response>`.
5180
+ * Handler shape: `export default { async fetch(req, env, ctx) { ... } }`.
5157
5181
  * `.env.local` values in the customer's working directory are passed
5158
- * through to the child process so the handler can read them via
5159
- * `process.env.MY_KEY`. Existing values in the parent's env take
5160
- * priority (so `MY_KEY=x amba functions dev …` wins).
5182
+ * through to the child process and bridged into the `env` parameter so
5183
+ * the handler reads them the same way as in production (`env.MY_KEY`).
5184
+ * Existing values in the parent's env take priority (so
5185
+ * `MY_KEY=x amba functions dev …` wins).
5161
5186
  */
5162
5187
  async function functionsDevCommand(entryPoint, options = {}) {
5163
5188
  const port = validatePort(options.port);
@@ -5432,11 +5457,37 @@ try {
5432
5457
  // \`pathToFileURL\` is a no-op equivalent on POSIX (returns file:///...).
5433
5458
  const bundleUrl = pathToFileURL(bundlePath).href;
5434
5459
  const mod = await import(bundleUrl);
5435
- if (typeof mod.default !== 'function') {
5436
- process.stderr.write(' Error: entry file must export default async function (req: Request): Promise<Response>\\n');
5460
+ // Support the canonical modules export shape: export default { async fetch(req, env, ctx) { ... } }
5461
+ // .env.local values are already in process.env (parent passes childEnv to the child process).
5462
+ // Bridge them into the env parameter so dev/prod parity holds — production injects the same
5463
+ // names as env bindings; local dev reads them from .env.local via process.env.
5464
+ if (mod.default && typeof mod.default.fetch === 'function') {
5465
+ const env = Object.assign(Object.create(null), process.env);
5466
+ const fetchFn = mod.default.fetch.bind(mod.default);
5467
+ handler = (req) => {
5468
+ // Fresh ctx per request — production gives each request its own
5469
+ // ExecutionContext; a shared stub would leak per-request state
5470
+ // across concurrent requests in dev but not in prod.
5471
+ const ctx = {
5472
+ waitUntil: (p) => {
5473
+ // Swallow rejections from background promises — prod handles
5474
+ // waitUntil rejections without crashing. Leaving the rejection
5475
+ // unhandled would cause Node 15+ to crash the child process
5476
+ // even though the HTTP response was already sent.
5477
+ Promise.resolve(p).catch(() => {});
5478
+ },
5479
+ passThroughOnException: () => {},
5480
+ };
5481
+ return fetchFn(req, env, ctx);
5482
+ };
5483
+ } else if (typeof mod.default === 'function') {
5484
+ // Legacy plain-function shape — still accepted for backward compat but env
5485
+ // parameter is not available (secrets only reachable via process.env).
5486
+ handler = mod.default;
5487
+ } else {
5488
+ process.stderr.write(' Error: entry file must export default { async fetch(req, env, ctx) { ... } }\\n');
5437
5489
  process.exit(2);
5438
5490
  }
5439
- handler = mod.default;
5440
5491
  } catch (err) {
5441
5492
  process.stderr.write(' Bundle import failed: ' + (err && err.stack ? err.stack : err) + '\\n');
5442
5493
  process.exit(2);
@@ -5586,8 +5637,8 @@ function parseRateLimitFlags(options) {
5586
5637
  * Output formatting:
5587
5638
  * - `--json`: NDJSON to stdout (one event per line) so `| jq` works.
5588
5639
  * - default: human-readable lines:
5589
- * 2026-05-08T12:34:56.789Z scan-letter-v3 [info] "scanning"
5590
- * 2026-05-08T12:34:57.012Z scan-letter-v3 [exception] TypeError: …
5640
+ * 2026-05-08T12:34:56.789Z scan-letter@v3 [info] "scanning"
5641
+ * 2026-05-08T12:34:57.012Z scan-letter@v3 [exception] TypeError: …
5591
5642
  *
5592
5643
  * Server-side scoping is enforced — the API filters by ScriptName
5593
5644
  * prefix so a developer can never read another tenant's logs.
@@ -5640,7 +5691,7 @@ function printEvents(events, asJson) {
5640
5691
  }
5641
5692
  for (const e of ordered) {
5642
5693
  const ts = e.EventTimestampMs ? new Date(e.EventTimestampMs).toISOString() : "????-??-??T??:??:??Z";
5643
- const script = e.ScriptName ?? "<unknown-script>";
5694
+ const script = e.function_name ? `${e.function_name}@v${e.version ?? "?"}` : "<unknown-function>";
5644
5695
  if (e.Logs && e.Logs.length > 0) for (const logLine of e.Logs) {
5645
5696
  const level = renderLevel(logLine.Level);
5646
5697
  const msg = renderMessage(logLine.Message);
@@ -5746,11 +5797,20 @@ async function resolveSecretValue(opts) {
5746
5797
  //#endregion
5747
5798
  //#region src/commands/secrets.ts
5748
5799
  /**
5749
- * `amba secrets ...` — write per-function secrets through the platform
5750
- * API. The CLI sends `{name, value, function}` to the admin endpoint;
5751
- * the server stores the value, validates reserved binding names, and
5752
- * propagates it to the deployed Worker's binding. Sync is eventual —
5753
- * `amba secrets list` shows progress.
5800
+ * `amba secrets ...` — write secrets through the platform API. The CLI
5801
+ * sends `{name, value, function?}` to the admin endpoint; the server
5802
+ * stores the value, validates reserved binding names, and propagates it to
5803
+ * the deployed Worker binding(s). Sync is eventual — `amba secrets list`
5804
+ * shows progress.
5805
+ *
5806
+ * Two scopes:
5807
+ * - `--function <name>` → the secret binds to that one function.
5808
+ * - no `--function` → a PROJECT-WIDE secret, visible to every
5809
+ * function in the project (and to any function deployed later). Set it
5810
+ * once instead of repeating the same value per function.
5811
+ *
5812
+ * Secrets may be set BEFORE the function is deployed. The sync drains once
5813
+ * a deployment lands (and `amba functions deploy` re-syncs at the end).
5754
5814
  */
5755
5815
  async function secretsSetCommand(name, value, options) {
5756
5816
  validateSecretName(name);
@@ -5760,14 +5820,15 @@ async function secretsSetCommand(name, value, options) {
5760
5820
  });
5761
5821
  const projectConfig = await loadProjectConfig();
5762
5822
  if (options.env === "prod") console.log(pc.dim(" Note: --env is informational; prod and dev share one secret namespace per project."));
5823
+ const scopeLabel = options.function ? options.function : "project-wide";
5763
5824
  console.log();
5764
5825
  console.log(pc.bold(` amba secrets set ${pc.cyan(name)}`));
5765
- console.log(pc.dim(` function=${options.function} env=${options.env ?? "dev"}`));
5826
+ console.log(pc.dim(` scope=${scopeLabel} env=${options.env ?? "dev"}`));
5766
5827
  console.log();
5767
5828
  const res = await setSecretViaApi(projectConfig.projectId, {
5768
5829
  name,
5769
5830
  value: resolvedValue,
5770
- function: options.function
5831
+ ...options.function ? { function: options.function } : {}
5771
5832
  });
5772
5833
  console.log(pc.green(" ✓") + ` Secret ${pc.cyan(name)} stored ${pc.dim(`v${res.data.version}`)}`);
5773
5834
  console.log(pc.green(" ✓") + ` Workers Secret sync ${pc.dim(`(status=${res.data.sync_status})`)} queued`);
@@ -5775,17 +5836,22 @@ async function secretsSetCommand(name, value, options) {
5775
5836
  console.log(pc.dim(" Workers Secret will be live within ~30s. Run `amba secrets list` to check sync status."));
5776
5837
  console.log();
5777
5838
  }
5778
- async function secretsListCommand() {
5779
- const res = await listSecretsViaApi((await loadProjectConfig()).projectId);
5839
+ async function secretsListCommand(options = {}) {
5840
+ const res = await listSecretsViaApi((await loadProjectConfig()).projectId, { function: options.function });
5780
5841
  console.log();
5842
+ if (options.function) {
5843
+ console.log(pc.dim(` Secrets for function ${pc.bold(options.function)}:`));
5844
+ console.log();
5845
+ }
5781
5846
  if (res.data.length === 0) {
5782
- console.log(pc.dim(" No secrets configured."));
5847
+ console.log(pc.dim(options.function ? ` No secrets configured for ${options.function}.` : " No secrets configured."));
5783
5848
  console.log();
5784
5849
  return;
5785
5850
  }
5786
5851
  for (const row of res.data) {
5787
5852
  const status = renderStatus(row.sync_status);
5788
- console.log(` ${pc.bold(row.name)} ` + pc.dim(`(${row.function})`) + ` v${row.version} ${status}` + (row.last_error ? pc.dim(` — ${row.last_error}`) : ""));
5853
+ const scope = row.function ?? "project-wide";
5854
+ console.log(` ${pc.bold(row.name)} ` + pc.dim(`(${scope})`) + ` v${row.version} ${status}` + (row.last_error ? pc.dim(` — ${row.last_error}`) : ""));
5789
5855
  }
5790
5856
  console.log();
5791
5857
  }
@@ -6391,13 +6457,21 @@ function makeCodegenHttpClient() {
6391
6457
  * directory (mirrors `wrangler pages deploy ./out` semantics). Dynamic
6392
6458
  * logic belongs in `amba functions deploy`, not in a site directory.
6393
6459
  */
6394
- const SITE_NAME_RE = /^[a-z][a-z0-9_-]{0,49}$/;
6395
- const HOSTNAME_RE = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i;
6460
+ const SITE_NAME_RE$1 = /^[a-z][a-z0-9_-]{0,49}$/;
6461
+ const HOSTNAME_RE$1 = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i;
6462
+ const MAX_CERT_CN_LENGTH = 64;
6463
+ const PLATFORM_DOMAINS = ["amba.host", "amba.dev"];
6464
+ function isPlatformDomain(host) {
6465
+ return PLATFORM_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
6466
+ }
6396
6467
  function validateSiteName(name) {
6397
- if (!SITE_NAME_RE.test(name)) throw new Error(`Invalid site name '${name}'. Must match /^[a-z][a-z0-9_-]{0,49}$/ (lowercase ASCII, digits, underscore or hyphen; ≤50 chars).`);
6468
+ if (!SITE_NAME_RE$1.test(name)) throw new Error(`Invalid site name '${name}'. Must match /^[a-z][a-z0-9_-]{0,49}$/ (lowercase ASCII, digits, underscore or hyphen; ≤50 chars).`);
6398
6469
  }
6399
6470
  function validateHostname(host) {
6400
- if (!HOSTNAME_RE.test(host)) throw new Error(`Invalid hostname '${host}'. Must be a DNS-shaped name (e.g. site.example.com).`);
6471
+ const h = host.trim().toLowerCase();
6472
+ if (!HOSTNAME_RE$1.test(h)) throw new Error(`Invalid hostname '${host}'. Must be a DNS-shaped name (e.g. site.example.com).`);
6473
+ if (h.length > MAX_CERT_CN_LENGTH) throw new Error(`Hostname '${host}' is too long for certificate issuance (max ${MAX_CERT_CN_LENGTH} characters). Use a shorter subdomain.`);
6474
+ if (isPlatformDomain(h)) throw new Error(`'${host}' is reserved by the platform. Attach a domain you own, e.g. app.yourdomain.com.`);
6401
6475
  }
6402
6476
  /**
6403
6477
  * Per-deploy size cap. The hosting provider enforces 25 MiB per file +
@@ -6446,7 +6520,7 @@ async function sitesDeployCommand(inputDir, options = {}) {
6446
6520
  if (domains.data.length > 0) {
6447
6521
  console.log();
6448
6522
  console.log(pc.dim(" Domains:"));
6449
- for (const d of domains.data) console.log(` ${pc.bold(d.hostname)} ${formatCertStatus(d.cert_status)}`);
6523
+ for (const d of domains.data) console.log(` ${pc.bold(d.hostname)} ${formatDomainStatus(d)}`);
6450
6524
  }
6451
6525
  console.log();
6452
6526
  }
@@ -6496,32 +6570,51 @@ async function sitesDomainAddCommand(hostname, options) {
6496
6570
  console.log();
6497
6571
  const res = await addSiteDomainViaApi(projectId, options.site, hostname);
6498
6572
  console.log(pc.green(" ✓") + ` Custom hostname registered`);
6573
+ const validation = res.data.ssl_validation ?? [];
6574
+ const ownership = res.data.ownership_verification;
6575
+ const isApex = res.data.is_apex ?? false;
6576
+ if (validation.length > 0 || ownership) {
6577
+ console.log();
6578
+ console.log(pc.dim(" 1. Publish these DNS records to verify ownership:"));
6579
+ for (const rec of validation) if (rec.txt_name && rec.txt_value) console.log(` ${pc.bold("TXT")} ${rec.txt_name} → ${pc.cyan(rec.txt_value)}`);
6580
+ if (ownership?.name && ownership.value) console.log(` ${pc.bold("TXT")} ${ownership.name} → ${pc.cyan(ownership.value)}`);
6581
+ console.log();
6582
+ console.log(pc.dim(" 2. Point your hostname at the Amba target:"));
6583
+ } else {
6584
+ console.log();
6585
+ console.log(pc.dim(" Point your hostname at the Amba target:"));
6586
+ }
6587
+ console.log(` ${pc.bold("CNAME")} ${hostname} → ${pc.cyan(res.data.dns_target)}`);
6499
6588
  console.log();
6500
- console.log(pc.dim(" Point your DNS at:"));
6501
- console.log(` ${pc.bold("CNAME")} ${hostname} → ${pc.cyan(res.data.dns_target)}`);
6502
- console.log();
6589
+ if (isApex) {
6590
+ console.log(pc.dim(` ${hostname} is a root domain — the CNAME above works only if your DNS provider`));
6591
+ console.log(pc.dim(` flattens CNAME-at-root. If it doesn't, A-record apex support is coming soon;`));
6592
+ console.log(pc.dim(` use a subdomain like app.${hostname} in the meantime.`));
6593
+ console.log();
6594
+ }
6503
6595
  if (options.noWait) {
6504
- console.log(pc.yellow(" ! --no-wait — skipping cert poll. Run `amba sites describe` later."));
6596
+ console.log(pc.yellow(" ! --no-wait — skipping status poll. Run `amba sites describe` later."));
6505
6597
  return;
6506
6598
  }
6507
6599
  const timeout = (options.timeout ?? 600) * 1e3;
6508
6600
  const start = Date.now();
6509
- let lastStatus = "";
6601
+ let lastLine = "";
6510
6602
  while (Date.now() - start < timeout) {
6511
6603
  const row = (await listSiteDomains(projectId, options.site)).data.find((d) => d.hostname === hostname);
6512
6604
  if (!row) throw new Error(`Domain ${hostname} disappeared from listing — check API state.`);
6513
- if (row.cert_status !== lastStatus) {
6514
- console.log(pc.dim(` cert_status: ${formatCertStatus(row.cert_status)}`));
6515
- lastStatus = row.cert_status;
6605
+ const line = formatDomainStatus(row);
6606
+ if (line !== lastLine) {
6607
+ console.log(pc.dim(` ${line}`));
6608
+ lastLine = line;
6516
6609
  }
6517
- if (row.cert_status === "active") {
6518
- console.log(pc.green(" ✓") + ` ${hostname} live with valid cert.`);
6610
+ if (row.cert_status === "active" && row.ownership_status === "active") {
6611
+ console.log(pc.green(" ✓") + ` ${hostname} is live with a valid certificate.`);
6519
6612
  return;
6520
6613
  }
6521
- if (row.cert_status === "error") throw new Error(`Cert provisioning failed for ${hostname}. Check that the CNAME points at ${res.data.dns_target}.`);
6614
+ if (row.cert_status === "error" || row.ownership_status === "error") throw new Error(`Domain setup failed for ${hostname}. Confirm the TXT record(s) above are published and the CNAME points at ${res.data.dns_target}.`);
6522
6615
  await sleep(5e3);
6523
6616
  }
6524
- console.log(pc.yellow(` ! Timed out after ${options.timeout ?? 600}s waiting for cert. Re-run \`amba sites describe ${options.site}\` to check status.`));
6617
+ console.log(pc.yellow(` ! Timed out after ${options.timeout ?? 600}s. Re-run \`amba sites describe ${options.site}\` to check status.`));
6525
6618
  }
6526
6619
  async function sitesDomainListCommand(siteName) {
6527
6620
  validateSiteName(siteName);
@@ -6532,7 +6625,7 @@ async function sitesDomainListCommand(siteName) {
6532
6625
  console.log();
6533
6626
  return;
6534
6627
  }
6535
- for (const d of res.data) console.log(` ${pc.bold(d.hostname)} ${formatCertStatus(d.cert_status)}`);
6628
+ for (const d of res.data) console.log(` ${pc.bold(d.hostname)} ${formatDomainStatus(d)}`);
6536
6629
  console.log();
6537
6630
  }
6538
6631
  async function sitesDomainRemoveCommand(hostname, options) {
@@ -6641,10 +6734,157 @@ function formatCertStatus(s) {
6641
6734
  if (s === "error") return pc.red(s);
6642
6735
  return pc.yellow(s);
6643
6736
  }
6737
+ /**
6738
+ * Render BOTH lifecycle states for a domain row. A domain is only live when
6739
+ * ownership AND cert are both `active`, so display paths must show both —
6740
+ * showing cert alone reads "live" while ownership is still pending.
6741
+ */
6742
+ function formatDomainStatus(d) {
6743
+ return `ownership: ${formatCertStatus(d.ownership_status)} cert: ${formatCertStatus(d.cert_status)}`;
6744
+ }
6644
6745
  function sleep(ms) {
6645
6746
  return new Promise((r) => setTimeout(r, ms));
6646
6747
  }
6647
6748
  //#endregion
6749
+ //#region src/commands/domains.ts
6750
+ /**
6751
+ * `amba domains ...` commands — buy a domain through Amba.
6752
+ *
6753
+ * amba domains search <query> find available domains + prices
6754
+ * amba domains check <domain...> authoritative availability + price
6755
+ * amba domains buy <domain> --site s buy + connect to a site
6756
+ * amba domains list domains this project has purchased
6757
+ *
6758
+ * Search + check are free. `buy` surfaces the cost and requires explicit
6759
+ * confirmation (interactive y/N, or `--yes`) before it submits a purchase —
6760
+ * and the API itself only executes a real, paid registration when the
6761
+ * platform has live purchasing enabled. A registered domain is auto-
6762
+ * connected to the chosen site with no DNS setup by the customer.
6763
+ */
6764
+ const HOSTNAME_RE = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i;
6765
+ const SITE_NAME_RE = /^[a-z][a-z0-9_-]{0,49}$/;
6766
+ function confirm(question) {
6767
+ const rl = createInterface({
6768
+ input: process.stdin,
6769
+ output: process.stdout
6770
+ });
6771
+ return new Promise((resolve) => {
6772
+ rl.question(question, (answer) => {
6773
+ rl.close();
6774
+ resolve(/^y(es)?$/i.test(answer.trim()));
6775
+ });
6776
+ });
6777
+ }
6778
+ function formatPrice(price, currency) {
6779
+ if (price === null) return pc.dim("price unavailable");
6780
+ return pc.bold(`$${price.toFixed(2)} ${currency}/yr`);
6781
+ }
6782
+ async function domainsSearchCommand(query, options = {}) {
6783
+ const { projectId } = await loadProjectConfig();
6784
+ const res = await searchDomainsViaApi(projectId, query, options.limit);
6785
+ console.log();
6786
+ if (res.data.length === 0) {
6787
+ console.log(pc.dim(` No suggestions for "${query}".`));
6788
+ console.log();
6789
+ return;
6790
+ }
6791
+ for (const s of res.data) {
6792
+ const tag = s.available ? pc.green("available") : pc.red("taken");
6793
+ const premium = s.premium ? pc.yellow(" premium") : "";
6794
+ console.log(` ${pc.bold(s.domain)} ${tag}${premium} ${formatPrice(s.price_usd, s.currency)}`);
6795
+ }
6796
+ console.log();
6797
+ console.log(pc.dim(" Buy one with: amba domains buy <domain> --site <site>"));
6798
+ console.log();
6799
+ }
6800
+ async function domainsCheckCommand(domains) {
6801
+ if (domains.length === 0) throw new Error("Pass at least one domain to check.");
6802
+ if (domains.length > 20) throw new Error("At most 20 domains per check.");
6803
+ const normalised = domains.map((d) => d.toLowerCase());
6804
+ for (const d of normalised) if (!HOSTNAME_RE.test(d)) throw new Error(`Invalid domain '${d}'.`);
6805
+ const { projectId } = await loadProjectConfig();
6806
+ const res = await checkDomainsViaApi(projectId, normalised);
6807
+ console.log();
6808
+ for (const a of res.data) {
6809
+ const tag = a.available ? pc.green("available") : pc.red("taken");
6810
+ const unsupported = a.supported ? "" : pc.yellow(" (extension not supported)");
6811
+ console.log(` ${pc.bold(a.domain)} ${tag}${unsupported} ${formatPrice(a.price_usd, a.currency)}`);
6812
+ }
6813
+ console.log();
6814
+ }
6815
+ async function domainsBuyCommand(domain, options) {
6816
+ const hostname = domain.toLowerCase();
6817
+ if (!HOSTNAME_RE.test(hostname)) throw new Error(`Invalid domain '${domain}'.`);
6818
+ if (!options.site || !SITE_NAME_RE.test(options.site)) throw new Error("--site <name> is required (the site to connect the domain to).");
6819
+ const { projectId } = await loadProjectConfig();
6820
+ console.log();
6821
+ console.log(pc.bold(` amba domains buy ${pc.cyan(hostname)}`));
6822
+ console.log(pc.dim(` → connect to site ${pc.cyan(options.site)}`));
6823
+ console.log();
6824
+ const check = await checkDomainsViaApi(projectId, [hostname]);
6825
+ const availability = check.data.find((d) => d.domain === hostname) ?? check.data[0];
6826
+ if (!availability) throw new Error("Could not determine availability for that domain.");
6827
+ if (!availability.available) throw new Error(`${hostname} is not available to register. Try \`amba domains search\`.`);
6828
+ if (!availability.supported) throw new Error(`${hostname} uses an extension we can't register yet.`);
6829
+ if (availability.price_usd === null) throw new Error(`${hostname} can't be priced right now — try again shortly.`);
6830
+ const price = availability.price_usd;
6831
+ console.log(` Price: ${formatPrice(price, availability.currency)}` + (availability.premium ? pc.yellow(" (premium)") : ""));
6832
+ console.log(pc.dim(` Includes: registration${options.noPrivacy ? "" : " + WHOIS privacy"}${options.noAutoRenew ? "" : " + auto-renew"}, auto-connected to ${options.site} (no DNS setup).`));
6833
+ console.log();
6834
+ if (!options.yes) {
6835
+ if (!await confirm(pc.yellow(` Buy ${hostname} for $${price.toFixed(2)} ${availability.currency}? `) + "(y/N) ")) {
6836
+ console.log(pc.dim(" Cancelled — nothing was purchased."));
6837
+ console.log();
6838
+ return;
6839
+ }
6840
+ }
6841
+ const r = (await purchaseDomainViaApi(projectId, {
6842
+ domain: hostname,
6843
+ site: options.site,
6844
+ accept_price_usd: price,
6845
+ confirm: true,
6846
+ privacy: !options.noPrivacy,
6847
+ auto_renew: !options.noAutoRenew,
6848
+ ...options.years ? { years: options.years } : {}
6849
+ })).data;
6850
+ if (!r.executed) {
6851
+ if (r.gated) {
6852
+ console.log(pc.yellow(" ! Domain purchasing is not enabled on this deployment."));
6853
+ console.log(pc.dim(" The domain is available at the price shown; no charge was made."));
6854
+ } else {
6855
+ console.log(pc.yellow(" ! Purchase not completed."));
6856
+ if (r.message) console.log(pc.dim(` ${r.message}`));
6857
+ }
6858
+ console.log();
6859
+ return;
6860
+ }
6861
+ console.log(pc.green(" ✓") + ` Registered ${pc.bold(hostname)}`);
6862
+ if (r.provisioned) {
6863
+ console.log(pc.green(" ✓") + ` Connected to ${options.site}${r.dns_autoconfigured ? " — no DNS setup needed" : ""}.`);
6864
+ if (r.public_url) console.log(pc.green(" ✓") + ` Live at ${pc.underline(r.public_url)}`);
6865
+ console.log(pc.dim(" The TLS certificate is issuing now (usually a minute or two)."));
6866
+ } else if (r.message) console.log(pc.dim(` ${r.message}`));
6867
+ console.log();
6868
+ }
6869
+ async function domainsListCommand() {
6870
+ const { projectId } = await loadProjectConfig();
6871
+ const res = await listPurchasedDomainsViaApi(projectId);
6872
+ console.log();
6873
+ if (res.data.length === 0) {
6874
+ console.log(pc.dim(" No domains purchased through Amba yet."));
6875
+ console.log(pc.dim(" Buy one with: amba domains buy <domain> --site <site>"));
6876
+ console.log();
6877
+ return;
6878
+ }
6879
+ for (const d of res.data) {
6880
+ const domain = String(d.domain ?? "");
6881
+ const status = String(d.status ?? "");
6882
+ const coloured = status === "active" ? pc.green(status) : status === "failed" ? pc.red(status) : pc.yellow(status);
6883
+ console.log(` ${pc.bold(domain)} ${coloured}`);
6884
+ }
6885
+ console.log();
6886
+ }
6887
+ //#endregion
6648
6888
  //#region src/index.ts
6649
6889
  const program = new Command();
6650
6890
  program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("0.1.0");
@@ -6827,17 +7067,17 @@ consumers.command("unbind <queue-name>").description("Remove the binding for a q
6827
7067
  await runAction(() => functionsConsumersUnbindCommand(queueName));
6828
7068
  });
6829
7069
  const secrets = program.command("secrets").description("Function secrets (GCP canonical, Workers-Secret synced)");
6830
- secrets.command("set <name> [value]").description("Write a secret to GCP Secret Manager and queue Workers Secret sync. Use --from-stdin to keep the value out of shell history.").requiredOption("--function <name>", "Function name the secret binds to. Secrets are scoped per dispatched script — there is no project-wide secret in v1; every secret belongs to exactly one function. Use the same name as the entry passed to 'amba functions 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 --function fn --from-stdin`.").action(async (name, value, opts) => {
7070
+ 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) => {
6831
7071
  await runAction(() => secretsSetCommand(name, value, {
6832
7072
  function: opts.function,
6833
7073
  env: opts.env ?? "dev",
6834
7074
  fromStdin: opts.fromStdin
6835
7075
  }));
6836
7076
  });
6837
- secrets.command("list").description("List secret sync status for the current project").action(async () => {
6838
- await runAction(secretsListCommand);
7077
+ 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) => {
7078
+ await runAction(() => secretsListCommand({ function: opts.function }));
6839
7079
  });
6840
- secrets.command("unset <name>").description("Remove a secret from GCP Secret Manager (Workers Secret cleared on next deploy)").requiredOption("--function <name>", "Function name the secret binds to").action(async (name, opts) => {
7080
+ 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) => {
6841
7081
  await runAction(() => secretsUnsetCommand(name, opts));
6842
7082
  });
6843
7083
  const billing = program.command("billing").description("Per-project subscription, headroom, and spend controls");
@@ -6930,6 +7170,25 @@ sitesDomain.command("remove <hostname>").description("Detach a custom hostname (
6930
7170
  zoneId: opts.zoneId
6931
7171
  }));
6932
7172
  });
7173
+ const domains = program.command("domains").description("Buy a domain through Amba and connect it to a site");
7174
+ domains.command("search <query>").description("Search for available domains + prices (free)").option("--limit <n>", "Max suggestions (1-50, default 20)", (v) => parseInt(v, 10)).action(async (query, opts) => {
7175
+ await runAction(() => domainsSearchCommand(query, { limit: opts.limit }));
7176
+ });
7177
+ domains.command("check <domains...>").description("Authoritative availability + price for specific domains (free)").action(async (domainArgs) => {
7178
+ await runAction(() => domainsCheckCommand(domainArgs));
7179
+ });
7180
+ domains.command("buy <domain>").description("Buy a domain + connect it to a site (surfaces cost, asks to confirm)").requiredOption("--site <name>", "Site name to connect the domain to").option("--yes", "Skip the interactive cost confirmation prompt").option("--no-privacy", "Disable WHOIS privacy (privacy is on by default)").option("--no-auto-renew", "Disable auto-renew (on by default)").option("--years <n>", "Registration length in years (default 1)", (v) => parseInt(v, 10)).action(async (domain, opts) => {
7181
+ await runAction(() => domainsBuyCommand(domain, {
7182
+ site: opts.site,
7183
+ yes: opts.yes,
7184
+ noPrivacy: opts.privacy === false,
7185
+ noAutoRenew: opts.autoRenew === false,
7186
+ years: opts.years
7187
+ }));
7188
+ });
7189
+ domains.command("list").description("List domains this project has purchased through Amba").action(async () => {
7190
+ await runAction(domainsListCommand);
7191
+ });
6933
7192
  const aiProviders = program.command("ai").description("AI gateway — manage provider keys + prompts").command("providers").description("Per-project provider key registration (Anthropic, OpenAI)");
6934
7193
  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) => {
6935
7194
  await runAction(() => aiProvidersAddCommand(provider, opts));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layers/amba",
3
- "version": "4.0.2",
3
+ "version": "4.0.3",
4
4
  "description": "amba — agent-native backend-as-a-service. Functions, collections, storage, AI, email, queues, sites — one CLI to spin up your project and ship to production. `npx @layers/amba init` to start.",
5
5
  "type": "module",
6
6
  "bin": {