opencode-skills-antigravity 0.0.7 → 0.0.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 (37) hide show
  1. package/README.md +5 -3
  2. package/bundled-skills/007/scripts/full_audit.py +6 -4
  3. package/bundled-skills/007/scripts/score_calculator.py +67 -7
  4. package/bundled-skills/algorithmic-art/templates/viewer.html +2 -2
  5. package/bundled-skills/apify-actorization/SKILL.md +1 -2
  6. package/bundled-skills/apify-actorization/references/cli-actorization.md +4 -4
  7. package/bundled-skills/docs/COMMUNITY_GUIDELINES.md +1 -1
  8. package/bundled-skills/docs/contributors/community-guidelines.md +3 -32
  9. package/bundled-skills/docs/integrations/jetski-gemini-loader/loader.ts +21 -3
  10. package/bundled-skills/docs/maintainers/security-findings-triage-2026-03-18-addendum.md +22 -0
  11. package/bundled-skills/docs/users/getting-started.md +1 -1
  12. package/bundled-skills/docs/users/walkthrough.md +21 -17
  13. package/bundled-skills/dotnet-backend-patterns/resources/implementation-playbook.md +2 -2
  14. package/bundled-skills/instagram/scripts/auth.py +15 -6
  15. package/bundled-skills/landing-page-generator/SKILL.md +203 -0
  16. package/bundled-skills/landing-page-generator/references/conversion-patterns.md +176 -0
  17. package/bundled-skills/landing-page-generator/references/frameworks.md +177 -0
  18. package/bundled-skills/landing-page-generator/references/landing-page-patterns.md +98 -0
  19. package/bundled-skills/landing-page-generator/references/seo-checklist.md +109 -0
  20. package/bundled-skills/landing-page-generator/scripts/landing_page_scaffolder.py +568 -0
  21. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package-lock.json +33 -1073
  22. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package.json +7 -4
  23. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/src/db/migrations.ts +15 -3
  24. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/src/routes/todos.ts +85 -88
  25. package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package-lock.json +260 -456
  26. package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package.json +4 -2
  27. package/bundled-skills/notebooklm/scripts/auth_manager.py +17 -3
  28. package/bundled-skills/notebooklm/scripts/browser_session.py +11 -2
  29. package/bundled-skills/radix-ui-design-system/examples/README.md +1 -1
  30. package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/nodejs/src/webhook-handler.ts +5 -3
  31. package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/python/app.py +21 -13
  32. package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/python/webhook_handler.py +11 -4
  33. package/package.json +1 -1
  34. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/src/db/db.ts +0 -35
  35. /package/bundled-skills/dotnet-backend-patterns/assets/{repository-template.cs → repository-template.cs.template} +0 -0
  36. /package/bundled-skills/dotnet-backend-patterns/assets/{service-template.cs → service-template.cs.template} +0 -0
  37. /package/bundled-skills/radix-ui-design-system/templates/{component-template.tsx → component-template.tsx.template} +0 -0
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # opencode-skills-antigravity
1
+ # OpenCode Skills Antigravity
2
2
 
3
3
  An [OpenCode CLI](https://opencode.ai/) plugin that bundles and manages the [Antigravity Awesome Skills](https://github.com/sickn33/antigravity-awesome-skills) repository for instant use.
4
4
 
@@ -14,14 +14,16 @@ OpenCode automatically detects all skills and makes them available to the AI age
14
14
  You can then invoke any skill explicitly in your prompt:
15
15
 
16
16
  ```bash
17
- opencode run @brainstorming help me plan a feature
17
+ opencode run /brainstorming help me plan a feature
18
18
  ```
19
19
 
20
+ Skills are also available as `/` commands directly in the OpenCode chat (e.g., `/brainstorming`).
21
+
20
22
  Or simply describe what you want and OpenCode will pick the right skill automatically.
21
23
 
22
24
  ## 🚀 Installation
23
25
 
24
- ### 1. Add the plugin to your global OpenCode config
26
+ ### Add the plugin to your global OpenCode config
25
27
 
26
28
  Edit (or create) `~/.config/opencode/opencode.json`:
27
29
 
@@ -1081,6 +1081,7 @@ def run_audit(
1081
1081
  inj_report: dict = {"findings": [], "score": 100, "total_findings": 0}
1082
1082
  quick_report: dict = {"findings": [], "score": 100, "total_findings": 0}
1083
1083
  all_findings: list[dict] = []
1084
+ report_findings: list[dict] = []
1084
1085
 
1085
1086
  if need_scanners:
1086
1087
  logger.info("Running scanners for phases %s...", [p for p in phases_list if p >= 3])
@@ -1121,6 +1122,7 @@ def run_audit(
1121
1122
  + quick_report.get("findings", [])
1122
1123
  )
1123
1124
  all_findings = score_calculator._deduplicate_findings(raw)
1125
+ report_findings = score_calculator.redact_findings_for_report(all_findings)
1124
1126
 
1125
1127
  # ------------------------------------------------------------------
1126
1128
  # Collect source files if needed for phase 6
@@ -1142,7 +1144,7 @@ def run_audit(
1142
1144
  if 2 in phases_list:
1143
1145
  # Phase 2 benefits from phase 1 data and findings
1144
1146
  surface = phases_data.get("phase1") or _phase1_surface_mapping(target, verbose=verbose)
1145
- phases_data["phase2"] = _phase2_threat_modeling_hints(surface, all_findings)
1147
+ phases_data["phase2"] = _phase2_threat_modeling_hints(surface, report_findings)
1146
1148
 
1147
1149
  if 3 in phases_list:
1148
1150
  phases_data["phase3"] = _phase3_security_checklist(
@@ -1164,10 +1166,10 @@ def run_audit(
1164
1166
  )
1165
1167
 
1166
1168
  if 4 in phases_list:
1167
- phases_data["phase4"] = _phase4_red_team_scenarios(all_findings, auth_score)
1169
+ phases_data["phase4"] = _phase4_red_team_scenarios(report_findings, auth_score)
1168
1170
 
1169
1171
  if 5 in phases_list:
1170
- phases_data["phase5"] = _phase5_blue_team_recommendations(all_findings, auth_score)
1172
+ phases_data["phase5"] = _phase5_blue_team_recommendations(report_findings, auth_score)
1171
1173
 
1172
1174
  if 6 in phases_list:
1173
1175
  phases_data["phase6"] = _phase6_verdict(
@@ -1227,7 +1229,7 @@ def run_audit(
1227
1229
  "phases_run": phases_list,
1228
1230
  "phases": phases_data,
1229
1231
  "total_findings": len(all_findings),
1230
- "findings": all_findings,
1232
+ "findings": report_findings,
1231
1233
  "report_path": str(report_path),
1232
1234
  }
1233
1235
 
@@ -64,6 +64,17 @@ import quick_scan # noqa: E402
64
64
  # ---------------------------------------------------------------------------
65
65
  logger = setup_logging("007-score-calculator")
66
66
 
67
+ _SENSITIVE_FINDING_KEYS = {
68
+ "snippet",
69
+ "secret",
70
+ "token",
71
+ "password",
72
+ "access_token",
73
+ "app_secret",
74
+ "authorization_code",
75
+ "client_secret",
76
+ }
77
+
67
78
 
68
79
  # ---------------------------------------------------------------------------
69
80
  # Positive-signal patterns (auth, encryption, resilience, monitoring)
@@ -360,6 +371,51 @@ def _bar(score: float, width: int = 20) -> str:
360
371
  return "[" + "#" * filled + "." * (width - filled) + "]"
361
372
 
362
373
 
374
+ def _redact_report_value(value):
375
+ """Recursively redact sensitive values from report payloads."""
376
+ if isinstance(value, dict):
377
+ return {key: _redact_report_value(value[key]) for key in value}
378
+ if isinstance(value, list):
379
+ return [_redact_report_value(item) for item in value]
380
+ return value
381
+
382
+
383
+ def redact_findings_for_report(findings: list[dict]) -> list[dict]:
384
+ """Return findings safe to serialize in user-facing reports."""
385
+ redacted: list[dict] = []
386
+
387
+ for finding in findings:
388
+ safe_finding: dict = {}
389
+ finding_type = str(finding.get("type", "")).lower()
390
+
391
+ for key, value in finding.items():
392
+ key_lower = key.lower()
393
+ if key_lower in _SENSITIVE_FINDING_KEYS:
394
+ safe_finding[key] = "[redacted]"
395
+ continue
396
+ if finding_type == "secret" and key_lower in {"entropy", "match", "raw", "value"}:
397
+ safe_finding[key] = "[redacted]"
398
+ continue
399
+ safe_finding[key] = _redact_report_value(value)
400
+
401
+ redacted.append(safe_finding)
402
+
403
+ return redacted
404
+
405
+
406
+ def build_safe_scanner_summaries(scanner_summaries: dict[str, dict]) -> dict[str, dict]:
407
+ """Return scanner summaries with primitive numeric values only."""
408
+ safe_summaries: dict[str, dict] = {}
409
+
410
+ for scanner_name, summary in scanner_summaries.items():
411
+ safe_summaries[scanner_name] = {
412
+ "findings": int(summary.get("findings", 0)),
413
+ "score": float(summary.get("score", 0)),
414
+ }
415
+
416
+ return safe_summaries
417
+
418
+
363
419
  def format_text_report(
364
420
  target: str,
365
421
  domain_scores: dict[str, float],
@@ -430,6 +486,7 @@ def build_json_report(
430
486
  elapsed: float,
431
487
  ) -> dict:
432
488
  """Build a structured JSON report."""
489
+ safe_findings = redact_findings_for_report(all_findings)
433
490
  return {
434
491
  "report": "score_calculator",
435
492
  "target": target,
@@ -444,7 +501,7 @@ def build_json_report(
444
501
  "emoji": verdict["emoji"],
445
502
  },
446
503
  "scanner_summaries": scanner_summaries,
447
- "findings": all_findings,
504
+ "findings": safe_findings,
448
505
  }
449
506
 
450
507
 
@@ -564,6 +621,9 @@ def run_score(
564
621
  all_findings_raw = secrets_findings + dep_findings + inj_findings + quick_findings
565
622
  all_findings = _deduplicate_findings(all_findings_raw)
566
623
  total_findings = len(all_findings)
624
+ safe_findings = redact_findings_for_report(all_findings)
625
+ safe_total_findings = len(safe_findings)
626
+ safe_scanner_summaries = build_safe_scanner_summaries(scanner_summaries)
567
627
 
568
628
  logger.info(
569
629
  "Aggregated %d raw findings -> %d unique (deduplicated)",
@@ -613,8 +673,8 @@ def run_score(
613
673
  result=f"final_score={final_score}, verdict={verdict['label']}",
614
674
  details={
615
675
  "domain_scores": domain_scores,
616
- "total_findings": total_findings,
617
- "scanner_summaries": scanner_summaries,
676
+ "total_findings": safe_total_findings,
677
+ "scanner_summaries": safe_scanner_summaries,
618
678
  "duration_seconds": round(elapsed, 3),
619
679
  },
620
680
  )
@@ -627,9 +687,9 @@ def run_score(
627
687
  domain_scores=domain_scores,
628
688
  final_score=final_score,
629
689
  verdict=verdict,
630
- scanner_summaries=scanner_summaries,
690
+ scanner_summaries=safe_scanner_summaries,
631
691
  all_findings=all_findings,
632
- total_findings=total_findings,
692
+ total_findings=safe_total_findings,
633
693
  elapsed=elapsed,
634
694
  )
635
695
 
@@ -641,8 +701,8 @@ def run_score(
641
701
  domain_scores=domain_scores,
642
702
  final_score=final_score,
643
703
  verdict=verdict,
644
- scanner_summaries=scanner_summaries,
645
- total_findings=total_findings,
704
+ scanner_summaries=safe_scanner_summaries,
705
+ total_findings=safe_total_findings,
646
706
  elapsed=elapsed,
647
707
  ))
648
708
 
@@ -20,7 +20,7 @@
20
20
  <meta charset="UTF-8">
21
21
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
22
22
  <title>Generative Art Viewer</title>
23
- <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>
23
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js" integrity="sha512-bcfltY+lNLlNxz38yBBm/HLaUB1gTV6I0e+fahbF9pS6roIdzUytozWdnFV8ZnM6cSAG5EbmO0ag0a/fLZSG4Q==" crossorigin="anonymous"></script>
24
24
  <link rel="preconnect" href="https://fonts.googleapis.com">
25
25
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
26
26
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&family=Lora:wght@400;500&display=swap" rel="stylesheet">
@@ -596,4 +596,4 @@
596
596
  });
597
597
  </script>
598
598
  </body>
599
- </html>
599
+ </html>
@@ -45,10 +45,9 @@ Verify CLI is logged in:
45
45
  apify info # Should return your username
46
46
  ```
47
47
 
48
- If not logged in, check if `APIFY_TOKEN` environment variable is defined. If not, ask the user to generate one at https://console.apify.com/settings/integrations, then:
48
+ If not logged in, check if `APIFY_TOKEN` environment variable is defined. If not, ask the user to generate one at https://console.apify.com/settings/integrations, add it to their shell or secret manager without putting the literal token in command history, then run:
49
49
 
50
50
  ```bash
51
- export APIFY_TOKEN="your_token_here"
52
51
  apify login
53
52
  ```
54
53
 
@@ -33,12 +33,12 @@ Reference the [cli-start template Dockerfile](https://github.com/apify/actor-tem
33
33
  ```dockerfile
34
34
  FROM apify/actor-node:20
35
35
 
36
- # Install ubi for easy GitHub release installation
37
- RUN curl --silent --location \
38
- https://raw.githubusercontent.com/houseabsolute/ubi/master/bootstrap/bootstrap-ubi.sh | sh
36
+ # Install ubi from a package source or a verified release artifact
37
+ # Example: use your base image package manager or vendor a pinned binary in the build context
38
+ # RUN apt-get update && apt-get install -y ubi
39
39
 
40
40
  # Install your CLI tool from GitHub releases (example)
41
- # RUN ubi --project your-org/your-tool --in /usr/local/bin
41
+ # RUN install -m 0755 ./vendor/your-tool /usr/local/bin/your-tool
42
42
 
43
43
  # Or install apify-cli and jq manually
44
44
  RUN npm install -g apify-cli
@@ -1,3 +1,3 @@
1
1
  # Community Guidelines
2
2
 
3
- This document moved to [`contributors/community-guidelines.md`](contributors/community-guidelines.md).
3
+ This document moved to [`../CODE_OF_CONDUCT.md`](../CODE_OF_CONDUCT.md).
@@ -1,33 +1,4 @@
1
- # Code of Conduct
1
+ # Community Guidelines
2
2
 
3
- ## Our Pledge
4
-
5
- In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone.
6
-
7
- ## Our Standards
8
-
9
- Examples of behavior that contributes to creating a positive environment include:
10
-
11
- - Using welcoming and inclusive language
12
- - Being respectful of differing viewpoints and experiences
13
- - Gracefully accepting constructive criticism
14
- - Focusing on what is best for the community
15
- - Showing empathy towards other community members
16
-
17
- Examples of unacceptable behavior by participants include:
18
-
19
- - The use of sexualized language or imagery and unwelcome sexual attention or advances
20
- - Trolling, insulting/derogatory comments, and personal or political attacks
21
- - Public or private harassment
22
- - Publishing others' private information without explicit permission
23
- - Other conduct which could reasonably be considered inappropriate in a professional setting
24
-
25
- ## Enforcement
26
-
27
- Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28
-
29
- ## Attribution
30
-
31
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1.
32
-
33
- [homepage]: https://www.contributor-covenant.org
3
+ The canonical project policy now lives in the repository root at
4
+ [`CODE_OF_CONDUCT.md`](../../CODE_OF_CONDUCT.md).
@@ -83,16 +83,34 @@ export async function loadSkillBodies(
83
83
  ): Promise<string[]> {
84
84
  const bodies: string[] = [];
85
85
  const rootPath = path.resolve(skillsRoot);
86
+ const rootRealPath = await fs.promises.realpath(rootPath);
86
87
 
87
88
  for (const meta of metas) {
88
- const fullPath = path.resolve(rootPath, meta.path, "SKILL.md");
89
- const relativePath = path.relative(rootPath, fullPath);
89
+ const skillDirPath = path.resolve(rootPath, meta.path);
90
+ const relativePath = path.relative(rootPath, skillDirPath);
90
91
 
91
92
  if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
92
93
  throw new Error(`Skill path escapes skills root: ${meta.id}`);
93
94
  }
94
95
 
95
- const text = await fs.promises.readFile(fullPath, "utf8");
96
+ const skillDirStat = await fs.promises.lstat(skillDirPath);
97
+ if (!skillDirStat.isDirectory() || skillDirStat.isSymbolicLink()) {
98
+ throw new Error(`Skill directory must be a regular directory inside the skills root: ${meta.id}`);
99
+ }
100
+
101
+ const fullPath = path.join(skillDirPath, "SKILL.md");
102
+ const skillFileStat = await fs.promises.lstat(fullPath);
103
+ if (!skillFileStat.isFile() || skillFileStat.isSymbolicLink()) {
104
+ throw new Error(`SKILL.md must be a regular file inside the skills root: ${meta.id}`);
105
+ }
106
+
107
+ const realPath = await fs.promises.realpath(fullPath);
108
+ const realRelativePath = path.relative(rootRealPath, realPath);
109
+ if (realRelativePath.startsWith("..") || path.isAbsolute(realRelativePath)) {
110
+ throw new Error(`SKILL.md resolves outside the skills root: ${meta.id}`);
111
+ }
112
+
113
+ const text = await fs.promises.readFile(realPath, "utf8");
96
114
  bodies.push(text);
97
115
  }
98
116
 
@@ -0,0 +1,22 @@
1
+ # Security Findings Triage Addendum (2026-03-18)
2
+
3
+ This addendum supersedes the previous Jetski loader assessment in
4
+ `security-findings-triage-2026-03-15.md`.
5
+
6
+ ## Correction
7
+
8
+ - Finding: `Example loader trusts manifest paths, enabling file read`
9
+ - Path: `docs/integrations/jetski-gemini-loader/loader.ts`
10
+ - Previous triage status on 2026-03-15: `obsolete/not reproducible on current HEAD`
11
+ - Corrected assessment: the loader was still reproducible via a symlinked
12
+ `SKILL.md` that resolved outside `skillsRoot`. A local proof read the linked
13
+ file contents successfully.
14
+
15
+ ## Current Status
16
+
17
+ - The loader now rejects symlinked skill directories and symlinked `SKILL.md`
18
+ files.
19
+ - The loader now resolves the real path for `SKILL.md` and rejects any target
20
+ outside the configured `skillsRoot`.
21
+ - Regression coverage lives in
22
+ `tools/scripts/tests/jetski_gemini_loader.test.js`.
@@ -1,4 +1,4 @@
1
- # Getting Started with Antigravity Awesome Skills (V8.1.0)
1
+ # Getting Started with Antigravity Awesome Skills (V8.2.0)
2
2
 
3
3
  **New here? This guide will help you supercharge your AI Agent in 5 minutes.**
4
4
 
@@ -1,42 +1,46 @@
1
- # Walkthrough: Release v8.1.0 Maintenance Sweep
1
+ # Walkthrough: Release v8.2.0 Maintenance Sweep
2
2
 
3
3
  ## Overview
4
4
 
5
- This walkthrough captures the maintainer-side documentation and release preparation work for **v8.1.0** after the 2026-03-17 PR merge batch.
5
+ This walkthrough captures the maintainer-side documentation and release publication work for **v8.2.0** after the 2026-03-18 maintenance sweep.
6
6
 
7
7
  ## Changes Verified
8
8
 
9
9
  ### 1. Community PR batch integrated
10
10
 
11
- - **PR #324**: Added `progressive-web-app`
12
- - **PR #325**: Added `vibers-code-review`
13
- - **PR #326**: Repaired invalid YAML frontmatter in existing skills
14
- - **PR #329**: Added `trpc-fullstack`
15
- - **PR #330**: Aligned FAQ risk labels and documented the active `skill-review` check
16
- - **PR #331**: Removed a broken reference from `skills/data-scientist/SKILL.md`
11
+ - **PR #333**: Repaired missing required frontmatter fields in `skill-anatomy` and `adapter-patterns`
12
+ - **PR #336**: Added `astro`, `hono`, `pydantic-ai`, and `sveltekit`
13
+ - **PR #338**: Repaired malformed markdown in `browser-extension-builder`
14
+ - **PR #343**: Added missing metadata labels to `devcontainer-setup`
15
+ - **PR #340**: Added `openclaw-github-repo-commander`
16
+ - **PR #334**: Added `goldrush-api`
17
+ - **PR #345**: Added `Wolfe-Jam/faf-skills` to README source attributions
17
18
 
18
19
  ### 2. Release-facing docs refreshed
19
20
 
20
21
  - **README.md**:
21
- - Current release updated to **v8.1.0**
22
+ - Current release updated to **v8.2.0**
22
23
  - Release summary text aligned with the merged PR batch
23
24
  - Contributor acknowledgements kept in sync with the latest merge set
24
25
  - **docs/users/getting-started.md**:
25
- - Version header updated to **v8.1.0**
26
- - **docs/users/faq.md**:
27
- - Active `skill-review` workflow guidance retained as the current contributor check
26
+ - Version header updated to **v8.2.0**
28
27
  - **CHANGELOG.md**:
29
- - Added the release notes section for **8.1.0**
28
+ - Added the release notes section for **8.2.0**
30
29
 
31
- ### 3. Release protocol to run
30
+ ### 3. Maintenance fixes verified
31
+
32
+ - **Issue #344**: Corrected `.claude-plugin/marketplace.json` to use `source: "./"` and added a regression test for the Claude Code marketplace entry
33
+ - **.github/MAINTENANCE.md**: Documented the maintainer flow for fork-gated workflows and stale PR metadata
34
+
35
+ ### 4. Release protocol executed
32
36
 
33
37
  - `npm run release:preflight`
34
38
  - `npm run security:docs`
35
39
  - `npm run validate:strict` (diagnostic, optional blocker)
36
- - `npm run release:prepare -- 8.1.0`
37
- - `npm run release:publish -- 8.1.0`
40
+ - `npm run release:prepare -- 8.2.0`
41
+ - `npm run release:publish -- 8.2.0`
38
42
 
39
43
  ## Expected Outcome
40
44
 
41
45
  - Documentation, changelog, and generated metadata all agree on the release state.
42
- - The repository is ready for the `v8.1.0` tag and GitHub release.
46
+ - The repository published the `v8.2.0` tag and GitHub release successfully.
@@ -793,7 +793,7 @@ public class ProductsApiTests : IClassFixture<WebApplicationFactory<Program>>
793
793
 
794
794
  ## Resources
795
795
 
796
- - **assets/service-template.cs**: Complete service implementation template
797
- - **assets/repository-template.cs**: Repository pattern implementation
796
+ - **assets/service-template.cs.template**: Complete service implementation template
797
+ - **assets/repository-template.cs.template**: Repository pattern implementation
798
798
  - **references/ef-core-best-practices.md**: EF Core optimization guide
799
799
  - **references/dapper-patterns.md**: Advanced Dapper usage patterns
@@ -19,6 +19,7 @@ from __future__ import annotations
19
19
 
20
20
  import argparse
21
21
  import asyncio
22
+ import html
22
23
  import json
23
24
  import os
24
25
  import sys
@@ -47,6 +48,15 @@ db = Database()
47
48
  db.init()
48
49
 
49
50
 
51
+ def _mask_secret(value: str, keep: int = 4) -> str:
52
+ """Mask secret-like values before showing them in terminal output."""
53
+ if not value:
54
+ return "(hidden)"
55
+ if len(value) <= keep:
56
+ return "*" * len(value)
57
+ return f"{value[:keep]}...masked"
58
+
59
+
50
60
  # ── OAuth Callback Server ────────────────────────────────────────────────────
51
61
 
52
62
  class OAuthCallbackHandler(BaseHTTPRequestHandler):
@@ -71,7 +81,8 @@ class OAuthCallbackHandler(BaseHTTPRequestHandler):
71
81
  self.send_response(400)
72
82
  self.send_header("Content-Type", "text/html; charset=utf-8")
73
83
  self.end_headers()
74
- self.wfile.write(f"<html><body><h2>Erro: {error}</h2></body></html>".encode())
84
+ safe_error = html.escape(error, quote=True)
85
+ self.wfile.write(f"<html><body><h2>Erro: {safe_error}</h2></body></html>".encode())
75
86
  else:
76
87
  self.send_response(404)
77
88
  self.end_headers()
@@ -84,7 +95,7 @@ def wait_for_oauth_code() -> Optional[str]:
84
95
  """Inicia servidor local e espera pelo código de autorização."""
85
96
  server = HTTPServer(("localhost", OAUTH_REDIRECT_PORT), OAuthCallbackHandler)
86
97
  server.timeout = 120 # 2 minutos
87
- print(f"Aguardando autorização em http://localhost:{OAUTH_REDIRECT_PORT}/callback ...")
98
+ print("Aguardando autorização no callback OAuth local...")
88
99
  print("(Timeout: 2 minutos)\n")
89
100
 
90
101
  while OAuthCallbackHandler.authorization_code is None:
@@ -285,10 +296,8 @@ async def setup() -> None:
285
296
  f"response_type=code"
286
297
  )
287
298
 
288
- print(f"\nAbrindo browser para autorização...")
289
- # Mask client_id in auth URL to avoid logging credentials
290
- masked_url = auth_url.replace(app_id, app_id[:4] + "...masked") if app_id else auth_url
291
- print(f"URL: {masked_url}\n")
299
+ print("\nAbrindo browser para autorização...")
300
+ print("A URL de autorização e o App ID não serão exibidos para evitar vazamento de credenciais.\n")
292
301
  webbrowser.open(auth_url)
293
302
 
294
303
  # Esperar callback