ticketlens 0.21.0 → 0.21.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticketlens",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "description": "Jira CLI for developers — fetch ticket context, triage your queue, and stop tab-switching. Zero dependencies, all local.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,13 +25,37 @@ export function readCliToken(configDir = DEFAULT_CONFIG_DIR) {
25
25
  }
26
26
  }
27
27
 
28
- export function saveCliToken(token, configDir = DEFAULT_CONFIG_DIR) {
28
+ // Server-asserted tier (personal or team-seat) synced alongside the token on
29
+ // every login/sync — separate from the signed, offline license.json file.
30
+ // Lets team-invited members get Pro-equivalent CLI access without a personal
31
+ // TL- key, mirroring what the manager already pays for.
32
+ export function readCliTokenTier(configDir = DEFAULT_CONFIG_DIR) {
33
+ const p = cliTokenPath(configDir);
34
+ if (!existsSync(p)) return null;
35
+ try {
36
+ const data = JSON.parse(readFileSync(p, 'utf8'));
37
+ return typeof data.tier === 'string' ? data.tier : null;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ export function saveCliToken(token, configDir = DEFAULT_CONFIG_DIR, tier = undefined) {
29
44
  mkdirSync(configDir, { recursive: true });
30
45
  const p = cliTokenPath(configDir);
31
- writeFileSync(p, JSON.stringify({ token }, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
46
+ const payload = tier === undefined ? { token } : { token, tier };
47
+ writeFileSync(p, JSON.stringify(payload, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
32
48
  chmodSync(p, 0o600);
33
49
  }
34
50
 
51
+ // Updates just the cached tier without touching the token — used by `tl sync`
52
+ // to keep entitlement fresh between logins.
53
+ export function saveCliTokenTier(tier, configDir = DEFAULT_CONFIG_DIR) {
54
+ const token = readCliToken(configDir);
55
+ if (!token) return;
56
+ saveCliToken(token, configDir, tier);
57
+ }
58
+
35
59
  export function deleteCliToken(configDir = DEFAULT_CONFIG_DIR) {
36
60
  const p = cliTokenPath(configDir);
37
61
  if (existsSync(p)) {
@@ -5,6 +5,7 @@ import crypto from 'node:crypto';
5
5
  import { DEFAULT_CONFIG_DIR } from './config.mjs';
6
6
  import { createStyler } from './ansi.mjs';
7
7
  import { siteBase } from './api-utils.mjs';
8
+ import { readCliTokenTier } from './cli-auth.mjs';
8
9
 
9
10
  // Mixed into the HMAC key so that knowing the license key alone is not sufficient
10
11
  // to forge a valid signature — an attacker also needs this constant from the source.
@@ -100,6 +101,15 @@ export function isLicensed(tier, configDir = DEFAULT_CONFIG_DIR) {
100
101
  if (required === 0) return true;
101
102
  if (process.env.TICKETLENS_SKIP_LICENSE === 'true') return true;
102
103
 
104
+ if (checkOwnLicense(required, configDir)) return true;
105
+
106
+ // Team-seat entitlement, server-asserted and synced via login/`tl sync` —
107
+ // covers invited team members who never activated a personal TL- key.
108
+ const syncedTier = LICENSE_TIERS[readCliTokenTier(configDir)] ?? 0;
109
+ return syncedTier >= required;
110
+ }
111
+
112
+ function checkOwnLicense(required, configDir) {
103
113
  const license = readLicense(configDir);
104
114
  if (!license) return false;
105
115
 
@@ -83,7 +83,12 @@ export async function runLogin({
83
83
  return;
84
84
  }
85
85
 
86
- saveCliTokenFn(token);
86
+ let tier;
87
+ try {
88
+ tier = (await res.json())?.tier;
89
+ } catch { /* tier is best-effort — token is still valid */ }
90
+
91
+ saveCliTokenFn(token, undefined, tier);
87
92
  stream.write(`\x1b[A\r\x1b[2K ${s.green('✔')} Logged in.\n`);
88
93
 
89
94
  // Flow 1: pull team Jira config for Pro/Team members (silently skipped for Free)
@@ -5,7 +5,7 @@
5
5
  * Server wins on all non-credential fields (shape, prefixes, URLs, etc.).
6
6
  */
7
7
 
8
- import { readCliToken } from './cli-auth.mjs';
8
+ import { readCliToken, saveCliTokenTier } from './cli-auth.mjs';
9
9
  import {
10
10
  loadProfiles,
11
11
  loadCredentials,
@@ -88,6 +88,8 @@ export async function syncProfiles({
88
88
  let json;
89
89
  try { json = await res.json(); } catch { return { error: 'invalid-json' }; }
90
90
 
91
+ if (typeof json?.tier === 'string') saveCliTokenTier(json.tier, configDir);
92
+
91
93
  const remoteProfiles = Array.isArray(json?.profiles) ? json.profiles : [];
92
94
  const localConfig = loadProfiles(configDir) || { profiles: {} };
93
95
  const localCreds = loadCredentials(configDir);