@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.
- package/dist/api-client.d.ts +89 -12
- package/dist/commands/domains.d.ts +27 -0
- package/dist/commands/functions-logs.d.ts +2 -2
- package/dist/commands/functions.d.ts +5 -4
- package/dist/commands/secrets.d.ts +29 -9
- package/dist/index.js +318 -59
- package/package.json +1 -1
package/dist/api-client.d.ts
CHANGED
|
@@ -348,11 +348,14 @@ export declare function scheduleFunction(projectId: string, input: {
|
|
|
348
348
|
schedule_id: string;
|
|
349
349
|
}>>;
|
|
350
350
|
export interface FunctionLogEvent {
|
|
351
|
-
/**
|
|
351
|
+
/** Sanitized log row — see `r2-log-reader.ts:SanitizedLogEvent`. Field-shape stable. */
|
|
352
352
|
Event?: {
|
|
353
353
|
Timestamp?: string;
|
|
354
354
|
};
|
|
355
|
-
|
|
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
|
|
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
|
-
/**
|
|
436
|
-
|
|
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
|
|
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
|
|
451
|
-
|
|
452
|
-
|
|
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
|
|
13
|
-
* 2026-05-08T12:34:57.012Z scan-letter
|
|
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
|
|
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
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
/**
|
|
10
|
-
|
|
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
|
|
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
|
|
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)}
|
|
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 \`
|
|
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
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
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
|
|
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
|
|
5159
|
-
*
|
|
5160
|
-
*
|
|
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
|
-
|
|
5436
|
-
|
|
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
|
|
5590
|
-
* 2026-05-08T12:34:57.012Z scan-letter
|
|
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.
|
|
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
|
|
5750
|
-
*
|
|
5751
|
-
*
|
|
5752
|
-
*
|
|
5753
|
-
*
|
|
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(`
|
|
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
|
-
|
|
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
|
-
|
|
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)} ${
|
|
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
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
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
|
|
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(`
|
|
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
|
|
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)} ${
|
|
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("
|
|
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
|
|
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.
|
|
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": {
|