@codyswann/lisa 4.55.8 → 4.55.9

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 (103) hide show
  1. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  2. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  3. package/dist/core/upstream-evidence-manifest.js +9 -8
  4. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  5. package/package.json +4 -4
  6. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  8. package/plugins/lisa/.codex-plugin/skills/lisa-jira-evidence/SKILL.md +3 -0
  9. package/plugins/lisa/.codex-plugin/skills/lisa-jira-evidence/scripts/post-evidence.sh +32 -2
  10. package/plugins/lisa/.codex-plugin/skills/lisa-jira-read-ticket/SKILL.md +1 -1
  11. package/plugins/lisa/.codex-plugin/skills/lisa-jira-read-ticket/scripts/download-attachment.sh +57 -5
  12. package/plugins/lisa/skills/lisa-jira-evidence/SKILL.md +3 -0
  13. package/plugins/lisa/skills/lisa-jira-evidence/scripts/post-evidence.sh +32 -2
  14. package/plugins/lisa/skills/lisa-jira-read-ticket/SKILL.md +1 -1
  15. package/plugins/lisa/skills/lisa-jira-read-ticket/scripts/download-attachment.sh +57 -5
  16. package/plugins/lisa-agy/plugin.json +1 -1
  17. package/plugins/lisa-agy/skills/lisa-jira-evidence/SKILL.md +3 -0
  18. package/plugins/lisa-agy/skills/lisa-jira-evidence/scripts/post-evidence.sh +32 -2
  19. package/plugins/lisa-agy/skills/lisa-jira-read-ticket/SKILL.md +1 -1
  20. package/plugins/lisa-agy/skills/lisa-jira-read-ticket/scripts/download-attachment.sh +57 -5
  21. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  24. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-copilot/skills/lisa-jira-evidence/SKILL.md +3 -0
  28. package/plugins/lisa-copilot/skills/lisa-jira-evidence/scripts/post-evidence.sh +32 -2
  29. package/plugins/lisa-copilot/skills/lisa-jira-read-ticket/SKILL.md +1 -1
  30. package/plugins/lisa-copilot/skills/lisa-jira-read-ticket/scripts/download-attachment.sh +57 -5
  31. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-cursor/skills/lisa-jira-evidence/SKILL.md +3 -0
  33. package/plugins/lisa-cursor/skills/lisa-jira-evidence/scripts/post-evidence.sh +32 -2
  34. package/plugins/lisa-cursor/skills/lisa-jira-read-ticket/SKILL.md +1 -1
  35. package/plugins/lisa-cursor/skills/lisa-jira-read-ticket/scripts/download-attachment.sh +57 -5
  36. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-expo/.codex-plugin/skills/jira-evidence/SKILL.md +3 -0
  39. package/plugins/lisa-expo/.codex-plugin/skills/jira-evidence/scripts/post-evidence.sh +32 -2
  40. package/plugins/lisa-expo/skills/jira-evidence/SKILL.md +3 -0
  41. package/plugins/lisa-expo/skills/jira-evidence/scripts/post-evidence.sh +32 -2
  42. package/plugins/lisa-expo-agy/plugin.json +1 -1
  43. package/plugins/lisa-expo-agy/skills/jira-evidence/SKILL.md +3 -0
  44. package/plugins/lisa-expo-agy/skills/jira-evidence/scripts/post-evidence.sh +32 -2
  45. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-expo-copilot/skills/jira-evidence/SKILL.md +3 -0
  47. package/plugins/lisa-expo-copilot/skills/jira-evidence/scripts/post-evidence.sh +32 -2
  48. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-expo-cursor/skills/jira-evidence/SKILL.md +3 -0
  50. package/plugins/lisa-expo-cursor/skills/jira-evidence/scripts/post-evidence.sh +32 -2
  51. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  54. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  59. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  64. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  69. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-rails/.codex-plugin/skills/jira-evidence/SKILL.md +3 -0
  74. package/plugins/lisa-rails/.codex-plugin/skills/jira-evidence/scripts/post-evidence.sh +32 -2
  75. package/plugins/lisa-rails/skills/jira-evidence/SKILL.md +3 -0
  76. package/plugins/lisa-rails/skills/jira-evidence/scripts/post-evidence.sh +32 -2
  77. package/plugins/lisa-rails-agy/plugin.json +1 -1
  78. package/plugins/lisa-rails-agy/skills/jira-evidence/SKILL.md +3 -0
  79. package/plugins/lisa-rails-agy/skills/jira-evidence/scripts/post-evidence.sh +32 -2
  80. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-rails-copilot/skills/jira-evidence/SKILL.md +3 -0
  82. package/plugins/lisa-rails-copilot/skills/jira-evidence/scripts/post-evidence.sh +32 -2
  83. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-rails-cursor/skills/jira-evidence/SKILL.md +3 -0
  85. package/plugins/lisa-rails-cursor/skills/jira-evidence/scripts/post-evidence.sh +32 -2
  86. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  88. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  89. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  93. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  94. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  96. package/plugins/src/base/skills/lisa-jira-evidence/SKILL.md +3 -0
  97. package/plugins/src/base/skills/lisa-jira-evidence/scripts/post-evidence.sh +32 -2
  98. package/plugins/src/base/skills/lisa-jira-read-ticket/SKILL.md +1 -1
  99. package/plugins/src/base/skills/lisa-jira-read-ticket/scripts/download-attachment.sh +57 -5
  100. package/plugins/src/expo/skills/jira-evidence/SKILL.md +3 -0
  101. package/plugins/src/expo/skills/jira-evidence/scripts/post-evidence.sh +32 -2
  102. package/plugins/src/rails/skills/jira-evidence/SKILL.md +3 -0
  103. package/plugins/src/rails/skills/jira-evidence/scripts/post-evidence.sh +32 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.55.8",
3
+ "version": "4.55.9",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.55.8",
3
+ "version": "4.55.9",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.55.8",
3
+ "version": "4.55.9",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "4.55.8",
3
+ "version": "4.55.9",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -34,6 +34,9 @@ Upload captured evidence and generated templates to GitHub PR description and JI
34
34
 
35
35
  ## Prerequisites
36
36
 
37
+ - `python3` for destination validation. Configure `server` as a bare HTTPS origin:
38
+ no userinfo, path prefix, query, or fragment. Invalid destinations are rejected
39
+ before Basic authorization is constructed.
37
40
  - `JIRA_API_TOKEN` environment variable set
38
41
  - a jira-cli config — `.lisa/jira-cli/.config.yml` if the `setup-jira-cli`
39
42
  SessionStart hook wrote one, otherwise `~/.config/.jira/.config.yml`. `server`
@@ -73,8 +73,38 @@ else
73
73
  echo " writes the project config — or run 'jira init' locally." >&2
74
74
  exit 1
75
75
  fi
76
- JIRA_SERVER=$(grep '^server:' "$JIRA_CONFIG" | awk '{print $2}')
76
+ JIRA_SERVER=$(sed -n 's/^server:[[:space:]]*//p' "$JIRA_CONFIG")
77
77
  JIRA_USER=$(grep '^login:' "$JIRA_CONFIG" | awk '{print $2}')
78
+ # Validate before constructing credentials; use only the approved HTTPS origin.
79
+ JIRA_SERVER=$(python3 - "$JIRA_SERVER" <<'PY_ORIGIN'
80
+ import ipaddress
81
+ import re
82
+ import sys
83
+ from urllib.parse import urlsplit
84
+
85
+ raw = sys.argv[1]
86
+ try:
87
+ if not raw or any(c <= " " or c >= "\x7f" for c in raw):
88
+ raise ValueError()
89
+ url = urlsplit(raw)
90
+ if (url.scheme != "https" or "@" in url.netloc or "?" in raw
91
+ or "#" in raw or url.path not in ("", "/") or not url.hostname):
92
+ raise ValueError()
93
+ host = url.hostname
94
+ if url.netloc.startswith("["):
95
+ host = f"[{ipaddress.IPv6Address(host).compressed}]"
96
+ elif not re.fullmatch(r"[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*", host):
97
+ raise ValueError()
98
+ port = url.port
99
+ if port == 0:
100
+ raise ValueError()
101
+ suffix = f":{port}" if port is not None and port != 443 else ""
102
+ print(f"https://{host}{suffix}")
103
+ except ValueError:
104
+ sys.exit("ERROR: JIRA_SERVER must be a bare HTTPS origin without userinfo, path, query or fragment.")
105
+ PY_ORIGIN
106
+ )
107
+
78
108
  GH_REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner')
79
109
  RELEASE_TAG="pr-assets"
80
110
 
@@ -104,7 +134,7 @@ fi
104
134
  echo "Found ${#SCREENSHOTS[@]} screenshots and ${#TEXT_EVIDENCE[@]} text evidence files to upload"
105
135
 
106
136
  # Compute JIRA auth early (used in steps 3 and 4)
107
- JIRA_AUTH=$(echo -n "$JIRA_USER:$JIRA_API_TOKEN" | base64)
137
+ JIRA_AUTH=$(printf '%s' "$JIRA_USER:$JIRA_API_TOKEN" | base64 | tr -d '\n')
108
138
 
109
139
  # ── Step 1: Upload to GitHub pr-assets release ──────────────────────────────
110
140
  echo ""
@@ -46,7 +46,7 @@ The Atlassian MCP exposes attachment metadata but no binary-fetch tool ([JRACLOU
46
46
  bash .claude/skills/jira-read-ticket/scripts/download-attachment.sh <id-or-content-url> <output-path>
47
47
  ```
48
48
 
49
- Requires `JIRA_SERVER`, `JIRA_LOGIN`, and `JIRA_API_TOKEN` in the environment (same contract as `jira-evidence`). If those are not set the helper exits with code 2 and a clear remediation message — record the URL only and continue.
49
+ Requires `python3` and `JIRA_API_TOKEN`. `JIRA_SERVER` and `JIRA_LOGIN` prefer environment values, then the project's `.lisa/jira-cli/.config.yml`, then the announced home config fallback. Missing credentials produce exit code 2 — record the URL only and continue. The server must be a bare HTTPS origin without userinfo, path prefix, query, or fragment. A full attachment URL must use that same origin; invalid destinations are rejected before Basic authorization is constructed. Signed external downloads receive no Basic credentials and use HTTPS only.
50
50
 
51
51
  After download, branch on `mimeType`:
52
52
  - `image/*` — pass the local path to image-aware downstream tools
@@ -14,6 +14,8 @@
14
14
  # See https://jira.atlassian.com/browse/JRACLOUD-97830 for the upstream
15
15
  # gap; remove this helper once Atlassian ships a download tool in the MCP.
16
16
  #
17
+ # Requires python3 for destination validation.
18
+ #
17
19
  # Required env vars:
18
20
  # JIRA_SERVER - https://<your-tenant>.atlassian.net
19
21
  # JIRA_LOGIN - login email
@@ -86,7 +88,7 @@ if [[ -z "${JIRA_SERVER:-}" || -z "${JIRA_LOGIN:-}" ]]; then
86
88
  fi
87
89
 
88
90
  if [[ -z "${JIRA_SERVER:-}" && -n "$JIRA_CONFIG" ]]; then
89
- JIRA_SERVER=$(grep '^server:' "$JIRA_CONFIG" | awk '{print $2}')
91
+ JIRA_SERVER=$(sed -n 's/^server:[[:space:]]*//p' "$JIRA_CONFIG")
90
92
  fi
91
93
  if [[ -z "${JIRA_LOGIN:-}" && -n "$JIRA_CONFIG" ]]; then
92
94
  JIRA_LOGIN=$(grep '^login:' "$JIRA_CONFIG" | awk '{print $2}')
@@ -101,18 +103,68 @@ for VAR in JIRA_SERVER JIRA_LOGIN JIRA_API_TOKEN; do
101
103
  fi
102
104
  done
103
105
 
106
+ # Validate before constructing credentials; use only the approved HTTPS origin.
107
+ JIRA_SERVER=$(python3 - "$JIRA_SERVER" <<'PY_ORIGIN'
108
+ import ipaddress
109
+ import re
110
+ import sys
111
+ from urllib.parse import urlsplit
112
+
113
+ raw = sys.argv[1]
114
+ try:
115
+ if not raw or any(c <= " " or c >= "\x7f" for c in raw):
116
+ raise ValueError()
117
+ url = urlsplit(raw)
118
+ if (url.scheme != "https" or "@" in url.netloc or "?" in raw
119
+ or "#" in raw or url.path not in ("", "/") or not url.hostname):
120
+ raise ValueError()
121
+ host = url.hostname
122
+ if url.netloc.startswith("["):
123
+ host = f"[{ipaddress.IPv6Address(host).compressed}]"
124
+ elif not re.fullmatch(r"[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*", host):
125
+ raise ValueError()
126
+ port = url.port
127
+ if port == 0:
128
+ raise ValueError()
129
+ suffix = f":{port}" if port is not None and port != 443 else ""
130
+ print(f"https://{host}{suffix}")
131
+ except ValueError:
132
+ sys.exit("ERROR: JIRA_SERVER must be a bare HTTPS origin without userinfo, path, query or fragment.")
133
+ PY_ORIGIN
134
+ )
135
+
104
136
  OUTPUT_DIR=$(dirname "$OUTPUT_PATH")
105
137
  if [[ ! -d "$OUTPUT_DIR" ]]; then
106
138
  echo "ERROR: Output directory does not exist: $OUTPUT_DIR" >&2
107
139
  exit 3
108
140
  fi
109
141
 
110
- if [[ "$ID_OR_URL" == http*://* ]]; then
111
- ATTACHMENT_URL="$ID_OR_URL"
142
+ if [[ "$ID_OR_URL" =~ ^[0-9]+$ ]]; then
143
+ ATTACHMENT_URL="$JIRA_SERVER/rest/api/3/attachment/content/$ID_OR_URL"
112
144
  else
113
- ATTACHMENT_URL="${JIRA_SERVER%/}/rest/api/3/attachment/content/$ID_OR_URL"
145
+ ATTACHMENT_URL="$ID_OR_URL"
114
146
  fi
115
147
 
148
+ # A supplied URL receives Basic auth only when it names the configured origin.
149
+ ATTACHMENT_URL=$(python3 - "$JIRA_SERVER" "$ATTACHMENT_URL" <<'PY_ATTACHMENT'
150
+ import sys
151
+ from urllib.parse import urlsplit, urlunsplit
152
+
153
+ try:
154
+ origin, raw = sys.argv[1:]
155
+ if not raw or any(c <= " " or c >= "\x7f" for c in raw):
156
+ raise ValueError()
157
+ base, target = urlsplit(origin), urlsplit(raw)
158
+ if (target.scheme != "https" or "@" in target.netloc or "#" in raw
159
+ or target.hostname != base.hostname
160
+ or (target.port if target.port is not None else 443) != (base.port or 443)):
161
+ raise ValueError()
162
+ print(urlunsplit(("https", base.netloc, target.path, target.query, "")))
163
+ except ValueError:
164
+ sys.exit("ERROR: Attachment must be an ID or an HTTPS URL on the configured Jira origin.")
165
+ PY_ATTACHMENT
166
+ )
167
+
116
168
  JIRA_AUTH=$(printf '%s' "$JIRA_LOGIN:$JIRA_API_TOKEN" | base64 | tr -d '\n')
117
169
 
118
170
  # Atlassian responds 302 to a signed URL on media.atlassian.com that has its
@@ -134,7 +186,7 @@ case "$HTTP_CODE" in
134
186
  echo "ERROR: Got HTTP $HTTP_CODE but no Location header in response." >&2
135
187
  exit 1
136
188
  fi
137
- curl -sSf -o "$OUTPUT_PATH" "$SIGNED_URL" || { echo "ERROR: Download from signed URL failed." >&2; exit 1; }
189
+ curl --proto '=https' -sSf -o "$OUTPUT_PATH" --url "$SIGNED_URL" || { echo "ERROR: Download from signed URL failed." >&2; exit 1; }
138
190
  ;;
139
191
  200)
140
192
  curl -sSf -o "$OUTPUT_PATH" \
@@ -17,6 +17,9 @@ Upload captured screenshots and generated templates to GitHub PR description and
17
17
 
18
18
  ## Prerequisites
19
19
 
20
+ - `python3` for destination validation. Configure `server` as a bare HTTPS origin:
21
+ no userinfo, path prefix, query, or fragment. Invalid destinations are rejected
22
+ before Basic authorization is constructed.
20
23
  - `JIRA_API_TOKEN` environment variable set
21
24
  - a jira-cli config — `.lisa/jira-cli/.config.yml` if the `setup-jira-cli`
22
25
  SessionStart hook wrote one, otherwise `~/.config/.jira/.config.yml`. `server`
@@ -73,8 +73,38 @@ else
73
73
  echo " writes the project config — or run 'jira init' locally." >&2
74
74
  exit 1
75
75
  fi
76
- JIRA_SERVER=$(grep '^server:' "$JIRA_CONFIG" | awk '{print $2}')
76
+ JIRA_SERVER=$(sed -n 's/^server:[[:space:]]*//p' "$JIRA_CONFIG")
77
77
  JIRA_USER=$(grep '^login:' "$JIRA_CONFIG" | awk '{print $2}')
78
+ # Validate before constructing credentials; use only the approved HTTPS origin.
79
+ JIRA_SERVER=$(python3 - "$JIRA_SERVER" <<'PY_ORIGIN'
80
+ import ipaddress
81
+ import re
82
+ import sys
83
+ from urllib.parse import urlsplit
84
+
85
+ raw = sys.argv[1]
86
+ try:
87
+ if not raw or any(c <= " " or c >= "\x7f" for c in raw):
88
+ raise ValueError()
89
+ url = urlsplit(raw)
90
+ if (url.scheme != "https" or "@" in url.netloc or "?" in raw
91
+ or "#" in raw or url.path not in ("", "/") or not url.hostname):
92
+ raise ValueError()
93
+ host = url.hostname
94
+ if url.netloc.startswith("["):
95
+ host = f"[{ipaddress.IPv6Address(host).compressed}]"
96
+ elif not re.fullmatch(r"[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*", host):
97
+ raise ValueError()
98
+ port = url.port
99
+ if port == 0:
100
+ raise ValueError()
101
+ suffix = f":{port}" if port is not None and port != 443 else ""
102
+ print(f"https://{host}{suffix}")
103
+ except ValueError:
104
+ sys.exit("ERROR: JIRA_SERVER must be a bare HTTPS origin without userinfo, path, query or fragment.")
105
+ PY_ORIGIN
106
+ )
107
+
78
108
  GH_REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner')
79
109
  RELEASE_TAG="pr-assets"
80
110
 
@@ -104,7 +134,7 @@ fi
104
134
  echo "Found ${#SCREENSHOTS[@]} screenshots and ${#TEXT_EVIDENCE[@]} text evidence files to upload"
105
135
 
106
136
  # Compute JIRA auth early (used in steps 3 and 4)
107
- JIRA_AUTH=$(echo -n "$JIRA_USER:$JIRA_API_TOKEN" | base64)
137
+ JIRA_AUTH=$(printf '%s' "$JIRA_USER:$JIRA_API_TOKEN" | base64 | tr -d '\n')
108
138
 
109
139
  # ── Step 1: Upload to GitHub pr-assets release ──────────────────────────────
110
140
  echo ""
@@ -17,6 +17,9 @@ Upload captured evidence and generated templates to GitHub PR description and JI
17
17
 
18
18
  ## Prerequisites
19
19
 
20
+ - `python3` for destination validation. Configure `server` as a bare HTTPS origin:
21
+ no userinfo, path prefix, query, or fragment. Invalid destinations are rejected
22
+ before Basic authorization is constructed.
20
23
  - `JIRA_API_TOKEN` environment variable set
21
24
  - a jira-cli config — `.lisa/jira-cli/.config.yml` if the `setup-jira-cli`
22
25
  SessionStart hook wrote one, otherwise `~/.config/.jira/.config.yml`. `server`
@@ -73,8 +73,38 @@ else
73
73
  echo " writes the project config — or run 'jira init' locally." >&2
74
74
  exit 1
75
75
  fi
76
- JIRA_SERVER=$(grep '^server:' "$JIRA_CONFIG" | awk '{print $2}')
76
+ JIRA_SERVER=$(sed -n 's/^server:[[:space:]]*//p' "$JIRA_CONFIG")
77
77
  JIRA_USER=$(grep '^login:' "$JIRA_CONFIG" | awk '{print $2}')
78
+ # Validate before constructing credentials; use only the approved HTTPS origin.
79
+ JIRA_SERVER=$(python3 - "$JIRA_SERVER" <<'PY_ORIGIN'
80
+ import ipaddress
81
+ import re
82
+ import sys
83
+ from urllib.parse import urlsplit
84
+
85
+ raw = sys.argv[1]
86
+ try:
87
+ if not raw or any(c <= " " or c >= "\x7f" for c in raw):
88
+ raise ValueError()
89
+ url = urlsplit(raw)
90
+ if (url.scheme != "https" or "@" in url.netloc or "?" in raw
91
+ or "#" in raw or url.path not in ("", "/") or not url.hostname):
92
+ raise ValueError()
93
+ host = url.hostname
94
+ if url.netloc.startswith("["):
95
+ host = f"[{ipaddress.IPv6Address(host).compressed}]"
96
+ elif not re.fullmatch(r"[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*", host):
97
+ raise ValueError()
98
+ port = url.port
99
+ if port == 0:
100
+ raise ValueError()
101
+ suffix = f":{port}" if port is not None and port != 443 else ""
102
+ print(f"https://{host}{suffix}")
103
+ except ValueError:
104
+ sys.exit("ERROR: JIRA_SERVER must be a bare HTTPS origin without userinfo, path, query or fragment.")
105
+ PY_ORIGIN
106
+ )
107
+
78
108
  GH_REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner')
79
109
  RELEASE_TAG="pr-assets"
80
110
 
@@ -104,7 +134,7 @@ fi
104
134
  echo "Found ${#SCREENSHOTS[@]} screenshots and ${#TEXT_EVIDENCE[@]} text evidence files to upload"
105
135
 
106
136
  # Compute JIRA auth early (used in steps 3 and 4)
107
- JIRA_AUTH=$(echo -n "$JIRA_USER:$JIRA_API_TOKEN" | base64)
137
+ JIRA_AUTH=$(printf '%s' "$JIRA_USER:$JIRA_API_TOKEN" | base64 | tr -d '\n')
108
138
 
109
139
  # ── Step 1: Upload to GitHub pr-assets release ──────────────────────────────
110
140
  echo ""