@a8techads/cli 0.3.0 → 0.4.0

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.
Files changed (2) hide show
  1. package/dist/a8techads.js +277 -49
  2. package/package.json +1 -1
package/dist/a8techads.js CHANGED
@@ -3200,6 +3200,52 @@ function createProfileCommand() {
3200
3200
  return profile;
3201
3201
  }
3202
3202
 
3203
+ // src/utils/http.ts
3204
+ async function apiRequest(opts) {
3205
+ await refreshTokenIfNeeded();
3206
+ const creds = loadCredentials();
3207
+ const profile = getCurrentProfile(creds);
3208
+ if (!profile) {
3209
+ throw new Error('No active profile. Run "a8techads auth login" to authenticate.');
3210
+ }
3211
+ const ctx = loadContext();
3212
+ const context = getCurrentContext(ctx);
3213
+ validateTenantMatch(profile.access_token, context?.tenant_id ?? null);
3214
+ const headers = {
3215
+ Authorization: `Bearer ${profile.access_token}`,
3216
+ "Content-Type": "application/json",
3217
+ ...opts.headers
3218
+ };
3219
+ if (context?.current_capability) {
3220
+ headers["X-Effective-Capability"] = context.current_capability;
3221
+ }
3222
+ if (context?.impersonation) {
3223
+ headers["X-Impersonate-Tenant"] = context.impersonation.target_tenant_id;
3224
+ if (context.impersonation.effective_capability) {
3225
+ headers["X-Impersonate-Capability"] = context.impersonation.effective_capability;
3226
+ }
3227
+ }
3228
+ const url = `${profile.api_url}${opts.path}`;
3229
+ return fetch(url, {
3230
+ method: opts.method ?? "GET",
3231
+ headers,
3232
+ body: opts.body ? JSON.stringify(opts.body) : undefined
3233
+ });
3234
+ }
3235
+ function validateTenantMatch(accessToken, contextTenantId) {
3236
+ const claims = decodeJwt(accessToken);
3237
+ if (!claims)
3238
+ return;
3239
+ const authClaims = getAuthClaims(claims);
3240
+ const tokenTenantId = authClaims?.tenant_id ?? null;
3241
+ if (!contextTenantId || !tokenTenantId)
3242
+ return;
3243
+ if (contextTenantId !== tokenTenantId) {
3244
+ throw new Error(`Token tenant mismatch. Context tenant: ${contextTenantId}, token tenant: ${tokenTenantId}.
3245
+ ` + `Run 'a8techads context set-tenant ${contextTenantId}' to re-authenticate.`);
3246
+ }
3247
+ }
3248
+
3203
3249
  // src/commands/context.ts
3204
3250
  function createContextCommand() {
3205
3251
  const context = new Command("context").description("Workspace context — tenant, capability, and app selection").addHelpText("after", `
@@ -3242,6 +3288,16 @@ Examples:
3242
3288
  }
3243
3289
  console.log(`App: ${profileCtx?.app ?? "none"}`);
3244
3290
  console.log(`Capability: ${profileCtx?.current_capability ?? "null (platform)"}`);
3291
+ if (profileCtx?.impersonation) {
3292
+ const imp = profileCtx.impersonation;
3293
+ console.log(`
3294
+ --- Managed Access Session ---`);
3295
+ console.log(`Target: ${imp.target_tenant_name} (${imp.target_tenant_id})`);
3296
+ console.log(`Capability: ${imp.effective_capability}`);
3297
+ console.log(`Role: ${imp.effective_role}`);
3298
+ console.log(`Grant: ${imp.managed_grant_id ?? "none"}`);
3299
+ console.log(`Started: ${imp.started_at}`);
3300
+ }
3245
3301
  });
3246
3302
  context.command("set-tenant").description(`Switch to a different tenant. Triggers re-authentication if the
3247
3303
  tenant differs from the current token tenant.
@@ -3376,6 +3432,74 @@ Shortcuts:
3376
3432
  console.log("Capability: PUBLISHER");
3377
3433
  console.log("App: ssp");
3378
3434
  });
3435
+ context.command("assume-role").description(`Start a managed access session as a platform operator.
3436
+
3437
+ Requires: platform_owner or platform_admin role.`).option("--tenant <id>", "Target tenant ID (required)").option("--capability <cap>", "Effective capability: ADVERTISER or PUBLISHER").addHelpText("after", `
3438
+ Examples:
3439
+ $ a8techads context assume-role --tenant 00000000-... --capability ADVERTISER
3440
+ $ a8techads context assume-role --tenant <id>`).action(async (opts) => {
3441
+ if (!opts.tenant) {
3442
+ console.error("Error: --tenant is required.");
3443
+ console.error('Run "a8techads context assume-role --help" for usage.');
3444
+ process.exit(1);
3445
+ }
3446
+ try {
3447
+ const body = { target_tenant_id: opts.tenant };
3448
+ if (opts.capability)
3449
+ body.target_capability = opts.capability;
3450
+ const resp = await apiRequest({
3451
+ method: "POST",
3452
+ path: "/api/v1/console/impersonation/start",
3453
+ body
3454
+ });
3455
+ if (!resp.ok) {
3456
+ const err = await resp.json().catch(() => ({ error: resp.statusText }));
3457
+ console.error(`Error: ${err.error || resp.statusText}`);
3458
+ process.exit(1);
3459
+ }
3460
+ const data = await resp.json();
3461
+ const targetTenant = data.target_tenant || {};
3462
+ const ctx = loadContext();
3463
+ const impersonation = {
3464
+ target_tenant_id: opts.tenant,
3465
+ target_tenant_name: targetTenant.company_name || "Unknown",
3466
+ effective_capability: data.effective_capability || opts.capability || "",
3467
+ effective_role: data.effective_role || "",
3468
+ managed_grant_id: data.managed_grant_id || null,
3469
+ started_at: new Date().toISOString()
3470
+ };
3471
+ setCurrentContext(ctx, { impersonation });
3472
+ saveContext(ctx);
3473
+ console.log("Managed access session started.");
3474
+ console.log(`Target: ${impersonation.target_tenant_name} (${opts.tenant})`);
3475
+ console.log(`Capability: ${impersonation.effective_capability}`);
3476
+ console.log(`Grant: ${impersonation.managed_grant_id || "auto-created"}`);
3477
+ } catch (err) {
3478
+ console.error(`Failed: ${err.message}`);
3479
+ process.exit(1);
3480
+ }
3481
+ });
3482
+ context.command("end-session").description(`End the current managed access session.
3483
+
3484
+ Requires: an active managed access session.`).addHelpText("after", `
3485
+ Examples:
3486
+ $ a8techads context end-session`).action(async () => {
3487
+ const ctx = loadContext();
3488
+ const profileCtx = getCurrentContext(ctx);
3489
+ if (!profileCtx?.impersonation) {
3490
+ console.log("No active managed access session.");
3491
+ return;
3492
+ }
3493
+ try {
3494
+ await apiRequest({
3495
+ method: "POST",
3496
+ path: "/api/v1/console/impersonation/end"
3497
+ });
3498
+ } catch {}
3499
+ setCurrentContext(ctx, { impersonation: null });
3500
+ saveContext(ctx);
3501
+ console.log("Managed access session ended.");
3502
+ });
3379
3503
  return context;
3380
3504
  }
3381
3505
  function deriveAppFromTenant(tenant) {
@@ -3392,46 +3516,6 @@ function deriveAuthUrl(tokenEndpoint) {
3392
3516
  return url.origin;
3393
3517
  }
3394
3518
 
3395
- // src/utils/http.ts
3396
- async function apiRequest(opts) {
3397
- await refreshTokenIfNeeded();
3398
- const creds = loadCredentials();
3399
- const profile = getCurrentProfile(creds);
3400
- if (!profile) {
3401
- throw new Error('No active profile. Run "a8techads auth login" to authenticate.');
3402
- }
3403
- const ctx = loadContext();
3404
- const context = getCurrentContext(ctx);
3405
- validateTenantMatch(profile.access_token, context?.tenant_id ?? null);
3406
- const headers = {
3407
- Authorization: `Bearer ${profile.access_token}`,
3408
- "Content-Type": "application/json",
3409
- ...opts.headers
3410
- };
3411
- if (context?.current_capability) {
3412
- headers["X-Effective-Capability"] = context.current_capability;
3413
- }
3414
- const url = `${profile.api_url}${opts.path}`;
3415
- return fetch(url, {
3416
- method: opts.method ?? "GET",
3417
- headers,
3418
- body: opts.body ? JSON.stringify(opts.body) : undefined
3419
- });
3420
- }
3421
- function validateTenantMatch(accessToken, contextTenantId) {
3422
- const claims = decodeJwt(accessToken);
3423
- if (!claims)
3424
- return;
3425
- const authClaims = getAuthClaims(claims);
3426
- const tokenTenantId = authClaims?.tenant_id ?? null;
3427
- if (!contextTenantId || !tokenTenantId)
3428
- return;
3429
- if (contextTenantId !== tokenTenantId) {
3430
- throw new Error(`Token tenant mismatch. Context tenant: ${contextTenantId}, token tenant: ${tokenTenantId}.
3431
- ` + `Run 'a8techads context set-tenant ${contextTenantId}' to re-authenticate.`);
3432
- }
3433
- }
3434
-
3435
3519
  // src/utils/api-prefix.ts
3436
3520
  function getApiPrefix() {
3437
3521
  const ctx = loadContext();
@@ -3572,7 +3656,7 @@ Examples:
3572
3656
  });
3573
3657
  cmd.command("create").description(`Create a new audience.
3574
3658
 
3575
- Phase 1 only supports type UPLOADED_LIST.`).option("--name <name>", "Audience name (required)").option("--type <type>", "Audience type (default: UPLOADED_LIST)", "UPLOADED_LIST").option("--description <desc>", "Description").option("--ttl <days>", "Membership TTL in days (default: 90)", "90").option("--from-json <file>", "Create from JSON file").addHelpText("after", `
3659
+ Phase 1 only supports type UPLOADED_LIST.`).option("--name <name>", "Audience name (required)").option("--type <type>", "Audience type: UPLOADED_LIST or RETARGETING", "UPLOADED_LIST").option("--description <desc>", "Description").option("--ttl <days>", "Membership TTL in days (default: 90)", "90").option("--goal-id <id>", "Conversion goal ID (required for RETARGETING type)").option("--from-json <file>", "Create from JSON file").addHelpText("after", `
3576
3660
  Examples:
3577
3661
  $ a8techads audiences create --name "High-Value Customers"
3578
3662
  $ a8techads audiences create --name "Retarget Pool" --ttl 30
@@ -3586,6 +3670,10 @@ Examples:
3586
3670
  console.error('Error: --name is required. Run "a8techads audiences create --help".');
3587
3671
  process.exit(1);
3588
3672
  }
3673
+ if (opts.type === "RETARGETING" && !opts.goalId) {
3674
+ console.error("Error: --goal-id is required for RETARGETING type.");
3675
+ process.exit(1);
3676
+ }
3589
3677
  body = {
3590
3678
  name: opts.name,
3591
3679
  type: opts.type,
@@ -3593,6 +3681,9 @@ Examples:
3593
3681
  };
3594
3682
  if (opts.description)
3595
3683
  body.description = opts.description;
3684
+ if (opts.goalId) {
3685
+ body.rules = { source: "conversion_goal", goal_id: opts.goalId, action: "positive", recency_days: 30 };
3686
+ }
3596
3687
  }
3597
3688
  const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/audiences`, body });
3598
3689
  const json = await resp.json();
@@ -3657,28 +3748,53 @@ Examples:
3657
3748
  }
3658
3749
  console.log(`Audience ${id} archived.`);
3659
3750
  });
3660
- cmd.command("upload").description(`Upload a user list to populate audience membership.
3751
+ cmd.command("upload").description(`Upload user identifiers to populate audience membership.
3661
3752
 
3662
- Accepts CSV or JSON files with user identifiers.`).argument("<id>", "Audience ID").option("--file <path>", "File path (CSV or JSON)").option("--identifier-type <type>", "Identifier type: EMAIL_HASH or DEVICE_ID", "EMAIL_HASH").option("--estimated-size <n>", "Estimated number of users in file").addHelpText("after", `
3753
+ Reads a file with one identifier per line (email hash, device ID, etc.)
3754
+ and uploads to the audience membership store.`).argument("<id>", "Audience ID").option("--file <path>", "File path (one identifier per line, or JSON array)").option("--identifier-type <type>", "Identifier type: EMAIL_HASH or DEVICE_ID", "EMAIL_HASH").addHelpText("after", `
3663
3755
  Examples:
3664
- $ a8techads audiences upload <id> --file users.csv --identifier-type EMAIL_HASH
3665
- $ a8techads audiences upload <id> --file devices.json --identifier-type DEVICE_ID`).action(async (id, opts) => {
3756
+ $ a8techads audiences upload <id> --file users.txt --identifier-type EMAIL_HASH
3757
+ $ a8techads audiences upload <id> --file devices.txt --identifier-type DEVICE_ID
3758
+
3759
+ File format (one per line):
3760
+ a1b2c3d4e5f6...
3761
+ f6e5d4c3b2a1...`).action(async (id, opts) => {
3666
3762
  if (!opts.file) {
3667
3763
  console.error("Error: --file is required.");
3668
3764
  process.exit(1);
3669
3765
  }
3766
+ const { readFileSync: readFileSync3 } = await import("fs");
3767
+ let identifiers;
3768
+ try {
3769
+ const content = readFileSync3(opts.file, "utf-8").trim();
3770
+ try {
3771
+ identifiers = JSON.parse(content);
3772
+ if (!Array.isArray(identifiers))
3773
+ throw new Error("not array");
3774
+ } catch {
3775
+ identifiers = content.split(`
3776
+ `).map((l) => l.trim()).filter((l) => l.length > 0);
3777
+ }
3778
+ } catch (err) {
3779
+ console.error(`Error reading file: ${err.message}`);
3780
+ process.exit(1);
3781
+ }
3782
+ if (identifiers.length === 0) {
3783
+ console.error("Error: file contains no identifiers.");
3784
+ process.exit(1);
3785
+ }
3786
+ console.log(`Uploading ${identifiers.length} identifiers...`);
3670
3787
  const body = {
3788
+ identifiers,
3671
3789
  identifierType: opts.identifierType
3672
3790
  };
3673
- if (opts.estimatedSize)
3674
- body.estimatedSize = Number(opts.estimatedSize);
3675
3791
  const resp = await apiRequest({ method: "POST", path: `${dspPrefix()}/audiences/${id}/upload`, body });
3676
3792
  const json = await resp.json();
3677
3793
  if (!resp.ok) {
3678
3794
  console.error(`Error: ${json.error ?? resp.statusText}`);
3679
3795
  process.exit(1);
3680
3796
  }
3681
- console.log(`Upload processed. Audience status: ${json.data?.status ?? json.status}`);
3797
+ console.log(`Upload complete. Status: ${json.data?.status ?? json.status}, Members: ${json.data?.uploadedCount ?? json.uploadedCount ?? "?"}`);
3682
3798
  });
3683
3799
  return cmd;
3684
3800
  }
@@ -4488,6 +4604,117 @@ Examples:
4488
4604
  return cmd;
4489
4605
  }
4490
4606
 
4607
+ // src/commands/admin.ts
4608
+ function createAdminCommand() {
4609
+ const cmd = new Command("admin").description(`Platform administration (Console)
4610
+
4611
+ Requires: platform_owner or platform_admin role.`).addHelpText("after", `
4612
+ Examples:
4613
+ $ a8techads admin tenants list
4614
+ $ a8techads admin tenants get <id>
4615
+ $ a8techads admin audit-logs
4616
+ $ a8techads admin system health`);
4617
+ const tenants = cmd.command("tenants").description("Tenant management");
4618
+ addFormatOption(tenants.command("list").description("List all tenants.")).action(async (opts) => {
4619
+ const resp = await apiRequest({ path: "/api/v1/console/tenants" });
4620
+ const json = await resp.json();
4621
+ if (!resp.ok) {
4622
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4623
+ process.exit(1);
4624
+ }
4625
+ const rows = (json.data ?? json).map((t) => ({
4626
+ id: t.id,
4627
+ name: t.companyName ?? t.company_name,
4628
+ type: t.tenantType ?? t.tenant_type,
4629
+ status: t.status
4630
+ }));
4631
+ const columns = [
4632
+ { key: "id", header: "ID", width: 36 },
4633
+ { key: "name", header: "NAME", width: 25 },
4634
+ { key: "type", header: "TYPE", width: 12 },
4635
+ { key: "status", header: "STATUS", width: 10 }
4636
+ ];
4637
+ printData(rows, columns, opts.format);
4638
+ });
4639
+ addFormatOption(tenants.command("get").description("Get tenant details.").argument("<id>", "Tenant ID")).action(async (id, opts) => {
4640
+ const resp = await apiRequest({ path: `/api/v1/console/tenants/${id}` });
4641
+ const json = await resp.json();
4642
+ if (!resp.ok) {
4643
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4644
+ process.exit(1);
4645
+ }
4646
+ printDetail(json.data ?? json, opts.format);
4647
+ });
4648
+ const members = tenants.command("members").description("Tenant member management");
4649
+ addFormatOption(members.command("list").description("List tenant members.").option("--tenant <id>", "Tenant ID (required)")).action(async (opts) => {
4650
+ if (!opts.tenant) {
4651
+ console.error("Error: --tenant is required.");
4652
+ process.exit(1);
4653
+ }
4654
+ const resp = await apiRequest({ path: `/api/v1/console/tenants/${opts.tenant}/members` });
4655
+ const json = await resp.json();
4656
+ if (!resp.ok) {
4657
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4658
+ process.exit(1);
4659
+ }
4660
+ const rows = (json.data ?? json).map((m) => ({
4661
+ id: m.userId ?? m.user_id ?? m.id,
4662
+ email: m.email,
4663
+ role: m.role,
4664
+ status: m.status
4665
+ }));
4666
+ printData(rows, [
4667
+ { key: "id", header: "USER ID", width: 36 },
4668
+ { key: "email", header: "EMAIL", width: 30 },
4669
+ { key: "role", header: "ROLE", width: 20 }
4670
+ ], opts.format);
4671
+ });
4672
+ addFormatOption(cmd.command("audit-logs").description("View audit logs.").option("--tenant <id>", "Filter by tenant").option("--limit <n>", "Max results", "20")).action(async (opts) => {
4673
+ const params = new URLSearchParams({ limit: opts.limit });
4674
+ if (opts.tenant)
4675
+ params.set("tenant_id", opts.tenant);
4676
+ const resp = await apiRequest({ path: `/api/v1/console/audit-logs?${params}` });
4677
+ const json = await resp.json();
4678
+ if (!resp.ok) {
4679
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4680
+ process.exit(1);
4681
+ }
4682
+ const rows = (json.data ?? json).map((l) => ({
4683
+ action: l.action,
4684
+ target: l.targetType ?? l.target_type,
4685
+ targetId: l.targetId ?? l.target_id,
4686
+ user: l.adminUserId ?? l.admin_user_id,
4687
+ time: l.createdAt ?? l.created_at
4688
+ }));
4689
+ printData(rows, [
4690
+ { key: "action", header: "ACTION", width: 25 },
4691
+ { key: "target", header: "TARGET", width: 12 },
4692
+ { key: "targetId", header: "TARGET ID", width: 36 },
4693
+ { key: "time", header: "TIME", width: 22 }
4694
+ ], opts.format);
4695
+ });
4696
+ const system = cmd.command("system").description("System operations");
4697
+ addFormatOption(system.command("health").description("System health check.")).action(async (opts) => {
4698
+ const resp = await apiRequest({ path: "/api/v1/console/system/health" });
4699
+ const json = await resp.json();
4700
+ if (!resp.ok) {
4701
+ console.error(`Error: ${json.error ?? resp.statusText}`);
4702
+ process.exit(1);
4703
+ }
4704
+ printDetail(json.data ?? json, opts.format);
4705
+ });
4706
+ system.command("cache-clear").description("Clear system cache.").action(async () => {
4707
+ const resp = await apiRequest({ method: "POST", path: "/api/v1/console/system/cache/clear" });
4708
+ if (!resp.ok) {
4709
+ const j = await resp.json();
4710
+ console.error(`Error: ${j.error ?? resp.statusText}`);
4711
+ process.exit(1);
4712
+ }
4713
+ console.log("Cache cleared.");
4714
+ });
4715
+ return cmd;
4716
+ }
4717
+
4491
4718
  // src/commands/settings.ts
4492
4719
  function settingsPrefix() {
4493
4720
  const ctx = loadContext();
@@ -4619,7 +4846,7 @@ Examples:
4619
4846
 
4620
4847
  // src/index.ts
4621
4848
  function createProgram() {
4622
- const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.2.0").addHelpText("after", `
4849
+ const program2 = new Command().name("a8techads").description("A8TechAds CLI — programmatic ad platform management").version("0.4.0").addHelpText("after", `
4623
4850
  Command Groups:
4624
4851
  auth Authentication (login, logout, token, status)
4625
4852
  profile Multi-profile management
@@ -4654,6 +4881,7 @@ Getting Started:
4654
4881
  program2.addCommand(createUsersCommand());
4655
4882
  program2.addCommand(createSettingsCommand());
4656
4883
  program2.addCommand(createInvoicesCommand());
4884
+ program2.addCommand(createAdminCommand());
4657
4885
  return program2;
4658
4886
  }
4659
4887
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a8techads/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "A8TechAds CLI — programmatic ad platform management",
5
5
  "type": "module",
6
6
  "bin": {