@odla-ai/cli 0.10.0 → 0.10.1

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/index.cjs CHANGED
@@ -252,14 +252,16 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
252
252
  return cached.token;
253
253
  }
254
254
  const browser = approvalBrowser(options);
255
+ const email = handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0);
255
256
  const { token, expiresAt } = await (0, import_db.requestToken)({
256
257
  endpoint: cfg.platformUrl,
258
+ email,
257
259
  label: `${cfg.app.id} provisioner`,
258
260
  fetch: doFetch,
259
- onCode: async ({ userCode, expiresIn }) => {
260
- const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
261
+ onCode: async ({ userCode, expiresIn, verificationUriComplete }) => {
262
+ const approvalUrl = verificationUriComplete ?? handshakeUrl(cfg.platformUrl, userCode);
261
263
  out.log("");
262
- out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
264
+ out.log(`Review, then approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
263
265
  if (browser.open) {
264
266
  try {
265
267
  await (options.openApprovalUrl ?? openUrl)(approvalUrl);
@@ -273,10 +275,17 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
273
275
  out.log("");
274
276
  }
275
277
  });
276
- writePrivateJson(cfg.local.tokenFile, { platform: audience, token, expiresAt });
278
+ writePrivateJson(cfg.local.tokenFile, { platform: audience, email, token, expiresAt });
277
279
  out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
278
280
  return token;
279
281
  }
282
+ function handshakeEmail(value, cached) {
283
+ const email = (value ?? import_node_process2.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
284
+ if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
285
+ throw new Error("a fresh odla handshake requires --email <account> or ODLA_USER_EMAIL");
286
+ }
287
+ return email;
288
+ }
280
289
  function approvalBrowser(options) {
281
290
  if (options.open === true) return { open: true, mode: "forced" };
282
291
  if (options.open === false) return { open: false, reason: "disabled by --no-open" };
@@ -369,14 +378,16 @@ async function scopedToken(platform, scope, options, doFetch, out) {
369
378
  out.log(`auth: using cached ${scope} grant (${tokenFile})`);
370
379
  return cached.token;
371
380
  }
381
+ const email = handshakeEmail(options.email, cache?.platform === audience ? cache.email : void 0);
372
382
  const { token, expiresAt } = await (0, import_db2.requestToken)({
373
383
  endpoint: audience,
384
+ email,
374
385
  label: `odla CLI admin AI (${scope})`,
375
386
  scopes: [scope],
376
387
  fetch: doFetch,
377
- onCode: async ({ userCode, expiresIn }) => {
378
- const approvalUrl = handshakeUrl(audience, userCode);
379
- out.log(`Approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
388
+ onCode: async ({ userCode, expiresIn, verificationUriComplete }) => {
389
+ const approvalUrl = verificationUriComplete ?? handshakeUrl(audience, userCode);
390
+ out.log(`Review, then approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
380
391
  const shouldOpen = options.open === true || options.open !== false && !import_node_process4.default.env.CI && Boolean(import_node_process4.default.stdin.isTTY && import_node_process4.default.stdout.isTTY);
381
392
  if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
382
393
  }
@@ -384,7 +395,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
384
395
  const tokens = cache?.platform === audience ? { ...cache.tokens ?? {} } : {};
385
396
  tokens[scope] = { token, expiresAt };
386
397
  if ((0, import_node_fs2.existsSync)(`${import_node_process4.default.cwd()}/.git`)) ensureGitignore(import_node_process4.default.cwd(), [tokenFile]);
387
- writePrivateJson(tokenFile, { platform: audience, tokens });
398
+ writePrivateJson(tokenFile, { platform: audience, email, tokens });
388
399
  out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
389
400
  return token;
390
401
  }
@@ -554,6 +565,7 @@ async function adminAi(options) {
554
565
  platform,
555
566
  scope,
556
567
  token: options.token,
568
+ email: options.email,
557
569
  open: options.open,
558
570
  fetch: doFetch,
559
571
  stdout: out,
@@ -813,6 +825,7 @@ async function adminCommand(parsed) {
813
825
  "platform",
814
826
  "json",
815
827
  "open",
828
+ "email",
816
829
  "provider",
817
830
  "model",
818
831
  "enabled",
@@ -840,6 +853,7 @@ async function adminCommand(parsed) {
840
853
  maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
841
854
  maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
842
855
  platform: stringOpt(parsed.options.platform),
856
+ email: stringOpt(parsed.options.email),
843
857
  json: parsed.options.json === true,
844
858
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
845
859
  credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
@@ -859,6 +873,7 @@ async function adminCommand(parsed) {
859
873
  // src/capabilities.ts
860
874
  var CAPABILITIES = {
861
875
  cli: [
876
+ "start an email-bound device request without accepting a password or user session token",
862
877
  "register the app and enable configured services per environment",
863
878
  "issue, persist, and explicitly rotate configured service credentials",
864
879
  "write local Worker values and push deployed Worker secrets through Wrangler stdin",
@@ -877,7 +892,7 @@ var CAPABILITIES = {
877
892
  "wire @odla-ai/calendar into trusted Worker code and keep the app admin key out of browsers"
878
893
  ],
879
894
  human: [
880
- "approve the odla device code and one-off third-party logins",
895
+ "provide the existing odla account email, then sign in and explicitly review/approve the exact device code",
881
896
  "review mirrored calendar/attendee disclosure and complete the server-issued Google consent flow",
882
897
  "consent to production changes with --yes",
883
898
  "request destructive credential rotation explicitly",
@@ -887,6 +902,7 @@ var CAPABILITIES = {
887
902
  ],
888
903
  studio: [
889
904
  "view telemetry and environment state",
905
+ "let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them",
890
906
  "review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens",
891
907
  "perform manual credential recovery when the CLI's local shown-once copy is unavailable",
892
908
  "configure system AI purposes and view app/environment/run-attributed usage",
@@ -1420,6 +1436,7 @@ async function lifecycleContext(options) {
1420
1436
  {
1421
1437
  configPath: cfg.configPath,
1422
1438
  token: options.token,
1439
+ email: options.email,
1423
1440
  open: options.open,
1424
1441
  interactive: options.interactive,
1425
1442
  openApprovalUrl: options.openConsentUrl
@@ -1836,13 +1853,13 @@ Usage:
1836
1853
  odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
1837
1854
  odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
1838
1855
  odla-ai doctor [--config odla.config.mjs]
1839
- odla-ai calendar status [--env dev] [--json]
1840
- odla-ai calendar calendars [--env dev] [--json]
1841
- odla-ai calendar connect [--env dev] [--no-open] [--yes]
1842
- odla-ai calendar resync [--env dev] [--yes]
1843
- odla-ai calendar disconnect [--env dev] --yes
1856
+ odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
1857
+ odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
1858
+ odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
1859
+ odla-ai calendar resync [--env dev] [--email <odla-account>] [--yes]
1860
+ odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
1844
1861
  odla-ai capabilities [--json]
1845
- odla-ai admin ai show [--platform https://odla.ai] [--json]
1862
+ odla-ai admin ai show [--platform https://odla.ai] [--email <odla-account>] [--json]
1846
1863
  odla-ai admin ai models [--provider <id>] [--json]
1847
1864
  odla-ai admin ai set <purpose> [--provider <id>] [--model <id>] [--enabled|--no-enabled]
1848
1865
  odla-ai admin ai set security --discovery-provider <id> --discovery-model <id>
@@ -1851,7 +1868,7 @@ Usage:
1851
1868
  odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
1852
1869
  odla-ai admin ai usage [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
1853
1870
  odla-ai admin ai audit [--limit <1-200>] [--json]
1854
- odla-ai security github connect [--repo owner/name] [--env dev] [--no-open]
1871
+ odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
1855
1872
  odla-ai security github disconnect --source <id> [--env dev] [--yes]
1856
1873
  odla-ai security plan [--env dev] [--json]
1857
1874
  odla-ai security sources [--env dev] [--json]
@@ -1860,12 +1877,12 @@ Usage:
1860
1877
  odla-ai security report <job-id> [--json]
1861
1878
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
1862
1879
  odla-ai security run [target] --self --ack-redacted-source
1863
- odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
1864
- odla-ai smoke [--config odla.config.mjs] [--env dev] [--no-open]
1880
+ odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
1881
+ odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
1865
1882
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
1866
1883
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
1867
- odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
1868
- odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
1884
+ odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
1885
+ odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
1869
1886
  odla-ai version
1870
1887
 
1871
1888
  Commands:
@@ -1893,6 +1910,10 @@ Safety:
1893
1910
  preflights Wrangler before any shown-once issuance or destructive rotation.
1894
1911
  Provision opens the approval page automatically in interactive terminals;
1895
1912
  use --open to force browser launch or --no-open to suppress it.
1913
+ A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
1914
+ The email is a non-secret identity hint: never provide a password or session
1915
+ token. The matching account must already exist, be signed in, explicitly
1916
+ review the exact code, and finish any current request before claiming another.
1896
1917
  Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
1897
1918
  OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
1898
1919
  GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
@@ -2523,7 +2544,7 @@ async function hostedSecurityContext(parsed, dependencies) {
2523
2544
  const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
2524
2545
  const token = await getDeveloperToken(
2525
2546
  cfg,
2526
- { configPath, open, openApprovalUrl: dependencies.openUrl },
2547
+ { configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
2527
2548
  doFetch,
2528
2549
  stdout
2529
2550
  );
@@ -3152,6 +3173,7 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
3152
3173
  "source",
3153
3174
  "ref",
3154
3175
  "follow",
3176
+ "email",
3155
3177
  "open",
3156
3178
  "json",
3157
3179
  "ack-redacted-source",
@@ -3240,6 +3262,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
3240
3262
  "platform",
3241
3263
  "self",
3242
3264
  "ack-redacted-source",
3265
+ "email",
3243
3266
  "open",
3244
3267
  "fail-on",
3245
3268
  "fail-on-candidates",
@@ -3269,6 +3292,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
3269
3292
  return getScopedPlatformToken({
3270
3293
  platform: request.platform,
3271
3294
  scope: request.scope,
3295
+ email: stringOpt(parsed.options.email),
3272
3296
  open,
3273
3297
  fetch: doFetch,
3274
3298
  stdout: out,
@@ -3281,7 +3305,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
3281
3305
  }
3282
3306
  return getDeveloperToken(
3283
3307
  cfg,
3284
- { configPath, open, openApprovalUrl: dependencies.openUrl },
3308
+ { configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
3285
3309
  doFetch,
3286
3310
  out
3287
3311
  );
@@ -3315,7 +3339,7 @@ async function securityCommand(parsed, dependencies) {
3315
3339
  return;
3316
3340
  }
3317
3341
  if (sub === "plan") {
3318
- assertArgs(parsed, ["config", "env", "platform", "open", "json"], 2);
3342
+ assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 2);
3319
3343
  const context = await hostedSecurityContext(parsed, dependencies);
3320
3344
  const plan = await getHostedSecurityPlan(context);
3321
3345
  if (parsed.options.json === true) context.stdout.log(JSON.stringify(plan, null, 2));
@@ -3331,7 +3355,7 @@ async function securityCommand(parsed, dependencies) {
3331
3355
  return;
3332
3356
  }
3333
3357
  if (sub === "report") {
3334
- assertArgs(parsed, ["config", "env", "platform", "open", "json"], 3);
3358
+ assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
3335
3359
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
3336
3360
  const context = await hostedSecurityContext(parsed, dependencies);
3337
3361
  const report = await getHostedSecurityReport({ ...context, jobId });
@@ -3349,7 +3373,7 @@ async function securityCommand(parsed, dependencies) {
3349
3373
  async function githubSecurityCommand(parsed, dependencies) {
3350
3374
  const action = parsed.positionals[2];
3351
3375
  if (action === "disconnect") {
3352
- assertArgs(parsed, ["config", "env", "platform", "source", "open", "yes"], 3);
3376
+ assertArgs(parsed, ["config", "env", "platform", "source", "email", "open", "yes"], 3);
3353
3377
  const context2 = await hostedSecurityContext(parsed, dependencies);
3354
3378
  const sourceId = requiredString(parsed.options.source, "--source");
3355
3379
  const confirmed = parsed.options.yes === true || await interactiveConfirmation(
@@ -3366,7 +3390,7 @@ async function githubSecurityCommand(parsed, dependencies) {
3366
3390
  if (action !== "connect") {
3367
3391
  throw new Error('unknown security github command. Try "odla-ai security github connect".');
3368
3392
  }
3369
- assertArgs(parsed, ["config", "env", "platform", "repo", "open"], 3);
3393
+ assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);
3370
3394
  const context = await hostedSecurityContext(parsed, dependencies);
3371
3395
  const repository = stringOpt(parsed.options.repo) ?? await inferGitHubRepository(process.cwd(), dependencies.readGitOrigin);
3372
3396
  const connection = await connectGitHubSecuritySource({
@@ -3381,7 +3405,7 @@ async function githubSecurityCommand(parsed, dependencies) {
3381
3405
  context.stdout.log("github: odla.ai stores the installation; no PAT or GitHub token is written locally");
3382
3406
  }
3383
3407
  async function listSecuritySources(parsed, dependencies) {
3384
- assertArgs(parsed, ["config", "env", "platform", "open", "json"], 2);
3408
+ assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 2);
3385
3409
  const context = await hostedSecurityContext(parsed, dependencies);
3386
3410
  const [plan, sources] = await Promise.all([
3387
3411
  getHostedSecurityPlan(context),
@@ -3403,7 +3427,7 @@ source repository default ref status`);
3403
3427
  }
3404
3428
  }
3405
3429
  async function securityStatus(parsed, dependencies) {
3406
- assertArgs(parsed, ["config", "env", "platform", "open", "json", "follow"], 3);
3430
+ assertArgs(parsed, ["config", "env", "platform", "email", "open", "json", "follow"], 3);
3407
3431
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
3408
3432
  const context = await hostedSecurityContext(parsed, dependencies);
3409
3433
  const job = parsed.options.follow === true ? await followHostedSecurityJob({
@@ -3706,6 +3730,7 @@ async function smoke(options) {
3706
3730
  {
3707
3731
  configPath: cfg.configPath,
3708
3732
  token: options.token,
3733
+ email: options.email,
3709
3734
  open: options.open,
3710
3735
  interactive: options.interactive,
3711
3736
  openApprovalUrl: options.openApprovalUrl
@@ -3853,11 +3878,12 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
3853
3878
  return;
3854
3879
  }
3855
3880
  if (command === "smoke") {
3856
- assertArgs(parsed, ["config", "env", "token", "open"], 1);
3881
+ assertArgs(parsed, ["config", "env", "token", "email", "open"], 1);
3857
3882
  await smoke({
3858
3883
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
3859
3884
  env: stringOpt(parsed.options.env),
3860
3885
  token: stringOpt(parsed.options.token),
3886
+ email: stringOpt(parsed.options.email),
3861
3887
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
3862
3888
  openApprovalUrl: dependencies.openUrl,
3863
3889
  fetch: dependencies.fetch,
@@ -3895,7 +3921,7 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
3895
3921
  if (command === "secrets") {
3896
3922
  const sub = parsed.positionals[1];
3897
3923
  if (sub === "set" || sub === "set-clerk-key") {
3898
- assertArgs(parsed, ["config", "env", "from-env", "stdin", "token", "yes"], sub === "set" ? 3 : 2);
3924
+ assertArgs(parsed, ["config", "env", "from-env", "stdin", "token", "email", "yes"], sub === "set" ? 3 : 2);
3899
3925
  const options = {
3900
3926
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
3901
3927
  name: parsed.positionals[2],
@@ -3903,6 +3929,7 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
3903
3929
  fromEnv: stringOpt(parsed.options["from-env"]),
3904
3930
  stdin: parsed.options.stdin === true,
3905
3931
  token: stringOpt(parsed.options.token),
3932
+ email: stringOpt(parsed.options.email),
3906
3933
  yes: parsed.options.yes === true,
3907
3934
  fetch: dependencies.fetch,
3908
3935
  stdout: dependencies.stdout
@@ -3936,6 +3963,7 @@ async function provisionCommand(parsed, dependencies) {
3936
3963
  "write-credentials",
3937
3964
  "write-dev-vars",
3938
3965
  "token",
3966
+ "email",
3939
3967
  "open",
3940
3968
  "yes"
3941
3969
  ], 1);
@@ -3949,6 +3977,7 @@ async function provisionCommand(parsed, dependencies) {
3949
3977
  writeCredentials: parsed.options["write-credentials"] !== false,
3950
3978
  writeDevVars: typeof writeDevVars2 === "string" ? writeDevVars2 : writeDevVars2 === true,
3951
3979
  token: stringOpt(parsed.options.token),
3980
+ email: stringOpt(parsed.options.email),
3952
3981
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
3953
3982
  yes: parsed.options.yes === true,
3954
3983
  fetch: dependencies.fetch,
@@ -3962,13 +3991,14 @@ async function calendarCommand(parsed, dependencies) {
3962
3991
  if (sub !== "status" && sub !== "calendars" && sub !== "connect" && sub !== "resync" && sub !== "disconnect") {
3963
3992
  throw new Error(`unknown calendar subcommand "${sub ?? ""}". Try "odla-ai calendar status --env dev".`);
3964
3993
  }
3965
- assertArgs(parsed, ["config", "env", "json", "token", "open", "yes"], 2);
3994
+ assertArgs(parsed, ["config", "env", "json", "token", "email", "open", "yes"], 2);
3966
3995
  if (sub !== "status" && sub !== "calendars" && parsed.options.json !== void 0) throw new Error(`--json is supported only by calendar status/calendars`);
3967
3996
  if ((sub === "status" || sub === "calendars") && parsed.options.yes !== void 0) throw new Error(`--yes is not used by "calendar ${sub}"`);
3968
3997
  const options = {
3969
3998
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
3970
3999
  env: stringOpt(parsed.options.env),
3971
4000
  token: stringOpt(parsed.options.token),
4001
+ email: stringOpt(parsed.options.email),
3972
4002
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
3973
4003
  yes: parsed.options.yes === true,
3974
4004
  json: parsed.options.json === true,