@passioncode-ai/fabric-agent-adapter 0.81.0 → 0.83.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/CHANGELOG.md +200 -3
- package/README.md +11 -6
- package/SECURITY.md +8 -5
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +7 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +9 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +18 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +25 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +19 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +31 -15
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +274 -17
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +24 -151
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check_cli.py +164 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +3 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +17 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +24 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +14 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/README.md +76 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/config.example.json +28 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/data_agent.py +366 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +32 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +4 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support-agent.mjs +115 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +63 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/a2a.md +98 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +23 -10
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +9 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +28 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/ops-digest.md +75 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +7 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +30 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +10 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +70 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +19 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_a2a.py +158 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +7 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +22 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a-server.mjs +274 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +14 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +54 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +45 -31
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +16 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +29 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +18 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +6 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +27 -10
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +3 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +21 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +20 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +88 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +24 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ops-digest.mjs +455 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +17 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +18 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +133 -30
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +44 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-socket-proof.mjs +180 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +5 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet-identity.mjs +468 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a.py +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a_server.py +404 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +16 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +97 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +35 -48
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +15 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +35 -159
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +18 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +23 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +43 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +21 -18
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +39 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +55 -96
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +98 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +92 -10
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +43 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ops_digest.py +647 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +16 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +20 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +180 -162
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +274 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_socket_proof.py +390 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +3 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet_identity.py +492 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +465 -55
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample-remote-service.mjs +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +16 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/tailnet-groups.schema.json +69 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +5 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +16 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +14 -9
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +9 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +4 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +18 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +2 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/admit.py +162 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +19 -1
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +2 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +30 -17
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +53 -35
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +10 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +2 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +34 -21
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +96 -15
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +147 -45
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py
CHANGED
|
@@ -93,26 +93,29 @@ class ProviderError(Exception):
|
|
|
93
93
|
"""An entry was refused; the message is one readable sentence and never quotes a value."""
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return Path(override).expanduser()
|
|
96
|
+
def fabric_root() -> Path:
|
|
97
|
+
"""The platform's Fabric root, as Fabric's registryDirs computes it: the folder `services/` and `providers/` sit in
|
|
98
|
+
when neither override is set."""
|
|
100
99
|
home = Path.home()
|
|
101
100
|
if sys.platform == "darwin":
|
|
102
|
-
return home / "Library/Application Support/ai.passioncode.fabric
|
|
101
|
+
return home / "Library/Application Support/ai.passioncode.fabric"
|
|
103
102
|
if sys.platform == "win32":
|
|
104
103
|
# provider.md "Where the directory is" (DEC-0032): LOCALAPPDATA, never the roaming APPDATA.
|
|
105
104
|
local = os.environ.get("LOCALAPPDATA")
|
|
106
|
-
return (Path(local) if local else home / "AppData" / "Local") / "passioncode-fabric"
|
|
107
|
-
return Path(os.environ.get("XDG_DATA_HOME") or str(home / ".local/share")) / "passioncode-fabric
|
|
105
|
+
return (Path(local) if local else home / "AppData" / "Local") / "passioncode-fabric"
|
|
106
|
+
return Path(os.environ.get("XDG_DATA_HOME") or str(home / ".local/share")) / "passioncode-fabric"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def services_dir() -> Path:
|
|
110
|
+
override = os.environ.get("FABRIC_SERVICES_DIR")
|
|
111
|
+
return Path(override).expanduser() if override else fabric_root() / "services"
|
|
108
112
|
|
|
109
113
|
|
|
110
114
|
def providers_dir() -> Path:
|
|
111
|
-
"""FABRIC_PROVIDERS_DIR, else `providers/` in the
|
|
115
|
+
"""FABRIC_PROVIDERS_DIR, else `providers/` in the platform's Fabric root, as Fabric's registryDirs reads it: a set
|
|
116
|
+
FABRIC_SERVICES_DIR moves the services folder only (FAA-189 (2))."""
|
|
112
117
|
override = os.environ.get("FABRIC_PROVIDERS_DIR")
|
|
113
|
-
if override
|
|
114
|
-
return Path(override).expanduser()
|
|
115
|
-
return services_dir().parent / "providers"
|
|
118
|
+
return Path(override).expanduser() if override else fabric_root() / "providers"
|
|
116
119
|
|
|
117
120
|
|
|
118
121
|
def looks_like_credential(value: str) -> bool:
|
|
@@ -155,7 +158,7 @@ def validate_provider_entry(entry: Any) -> List[str]:
|
|
|
155
158
|
# FAA-120, as the service kit: a repository is named, never reached with a credential in its URI. Said
|
|
156
159
|
# before the character check, so a password beside a character the URI class refuses (a backslash, as
|
|
157
160
|
# git reads one) is still named a credential (FAA-127).
|
|
158
|
-
problems.append("source.repository carries no password or credential: it names the repository")
|
|
161
|
+
problems.append("source.repository carries no password or credential: it names the repository (write the repository's name in lower case if it is one)")
|
|
159
162
|
elif "repository" in source and not _is_uri(source["repository"]):
|
|
160
163
|
problems.append("source.repository is an absolute URI")
|
|
161
164
|
if not isinstance(entry["installedAt"], str) or not _TIMESTAMP.fullmatch(entry["installedAt"]):
|
|
@@ -207,12 +210,17 @@ def _run_problems(run: Any) -> List[str]:
|
|
|
207
210
|
return problems
|
|
208
211
|
|
|
209
212
|
|
|
213
|
+
def _no_constant(name: str) -> Any:
|
|
214
|
+
raise ValueError("%s is no JSON" % name)
|
|
215
|
+
|
|
216
|
+
|
|
210
217
|
def manifest_problems(entry: Dict[str, Any]) -> List[str]:
|
|
211
218
|
"""FAC-SEM-014 (DEC-0017), URI half: the manifest resolves and its provider.id equals providerId, compared by normal
|
|
212
219
|
form (DEC-0076)."""
|
|
213
220
|
try:
|
|
214
|
-
manifest = json.loads(Path(os.path.expanduser(str(entry["manifest"]))).read_text(encoding="utf-8")
|
|
215
|
-
|
|
221
|
+
manifest = json.loads(Path(os.path.expanduser(str(entry["manifest"]))).read_text(encoding="utf-8"),
|
|
222
|
+
parse_constant=_no_constant)
|
|
223
|
+
except (OSError, ValueError, RecursionError): # NaN or Infinity is no JSON, as pack reads it (FAA-194 (38))
|
|
216
224
|
return ["the manifest %s does not resolve (FAC-SEM-014)" % entry["manifest"]]
|
|
217
225
|
provider = manifest.get("provider") if isinstance(manifest, dict) else None
|
|
218
226
|
if not isinstance(provider, dict) or not isinstance(manifest.get("capabilities"), list):
|
|
@@ -300,6 +308,14 @@ def entry_stem(path: str) -> str:
|
|
|
300
308
|
return re.sub(r"\.json\Z", "", re.sub(r"\A[^\n\r\u2028\u2029]*[\\/]", "", path, count=1))
|
|
301
309
|
|
|
302
310
|
|
|
311
|
+
def argument_stem(path: str, platform: Optional[str] = None) -> str:
|
|
312
|
+
"""The stem of the file a `validate` argument names: CT-193's, after a Windows drive-relative prefix (`C:name.json`
|
|
313
|
+
is name.json in that drive's folder) is set aside, which the registry never meets (FAA-189 (8))."""
|
|
314
|
+
if (platform or sys.platform) == "win32":
|
|
315
|
+
path = re.sub(r"\A[A-Za-z]:(?![\\/])", "", path)
|
|
316
|
+
return entry_stem(path)
|
|
317
|
+
|
|
318
|
+
|
|
303
319
|
def main(argv: Optional[List[str]] = None) -> int:
|
|
304
320
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
305
321
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
@@ -335,7 +351,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
335
351
|
entry = json.loads(args.path.read_text(encoding="utf-8"))
|
|
336
352
|
problems = validate_provider_entry(entry)
|
|
337
353
|
# CT-193 (FAC-SEM-014): the stem is what follows the last / or \ and loses .json, on every platform.
|
|
338
|
-
stem =
|
|
354
|
+
stem = argument_stem(str(args.path))
|
|
339
355
|
if isinstance(entry, dict) and entry.get("id") != stem:
|
|
340
356
|
problems.append("the file name %s is not <id>.json for %s (FAC-SEM-014)" % (args.path.name, entry.get("id")))
|
|
341
357
|
if not problems:
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py
CHANGED
|
@@ -10,8 +10,11 @@ U+001C to U+001F, and not U+FEFF), `.` stopping at every line terminator, ASCII
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import bisect
|
|
14
|
+
import ipaddress
|
|
13
15
|
import re
|
|
14
|
-
from typing import Dict, List, Tuple
|
|
16
|
+
from typing import Dict, List, Optional, Sequence, Tuple
|
|
17
|
+
import unicodedata
|
|
15
18
|
import urllib.parse
|
|
16
19
|
|
|
17
20
|
CREDENTIAL_WORDS = (
|
|
@@ -28,8 +31,36 @@ CREDENTIAL_PAIRS: Dict[str, tuple] = {
|
|
|
28
31
|
}
|
|
29
32
|
#: ECMAScript's \s, as a character class body.
|
|
30
33
|
JS_SPACE = "\t\n\x0b\x0c\r \xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
|
|
31
|
-
|
|
34
|
+
NOT_LINE_END = "[^\n\r\u2028\u2029]" # ECMAScript's `.` without the s flag
|
|
32
35
|
JWT_SHAPE = re.compile(r"eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*")
|
|
36
|
+
_URL_BASE64_RUN = re.compile(r"[A-Za-z0-9_-]*")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def jwt_spans(text: str, first: bool = False) -> List[Tuple[int, int]]:
|
|
40
|
+
"""The spans JWT_SHAPE matches in a text, leftmost and not overlapping, read in one pass (the contract's `jwtSpans`,
|
|
41
|
+
FAA-313): the regular expression backtracks from every `eyJ` of a long run, quadratic in it, while every `eyJ` of
|
|
42
|
+
one run shares that run's end, so only its first is tried, and a match ends where its third segment's run ends.
|
|
43
|
+
`first` stops at the first match."""
|
|
44
|
+
spans: List[Tuple[int, int]] = []
|
|
45
|
+
at = text.find("eyJ")
|
|
46
|
+
while at >= 0:
|
|
47
|
+
one = _URL_BASE64_RUN.match(text, at + 3).end()
|
|
48
|
+
resume = one
|
|
49
|
+
if one - at - 3 >= 2 and text[one:one + 1] == ".":
|
|
50
|
+
two = _URL_BASE64_RUN.match(text, one + 1).end()
|
|
51
|
+
if two - one - 1 >= 2 and text[two:two + 1] == ".":
|
|
52
|
+
three = _URL_BASE64_RUN.match(text, two + 1).end()
|
|
53
|
+
spans.append((at, three))
|
|
54
|
+
if first:
|
|
55
|
+
break
|
|
56
|
+
resume = three
|
|
57
|
+
at = text.find("eyJ", resume)
|
|
58
|
+
return spans
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def has_jwt(text: str) -> bool:
|
|
62
|
+
"""Whether JWT_SHAPE matches anywhere in a text, read in one pass (the contract's `hasJwt`, FAA-313)."""
|
|
63
|
+
return bool(jwt_spans(text, True))
|
|
33
64
|
#: Where a provider key's prefix may start (DEC-0151): the start of a value, a character that is no ASCII letter or
|
|
34
65
|
#: digit, or an escape (a percent-escape, single or double, a backslash escape, an octal, `\\x` with two hex digits,
|
|
35
66
|
#: `\\u` with four, an ESC colour sequence, raw or escaped). A consuming group, as the contract writes it.
|
|
@@ -43,14 +74,17 @@ _PROVIDER_KEYS = (r"(?:sk-(?=[A-Za-z0-9_-]{0,255}?[0-9])(?:(?=[A-Za-z0-9_-]{0,25
|
|
|
43
74
|
#: The contract's SECRET_VALUE at the pin (DEC-0151): `Bearer ` at a value's start in any case, or a provider key after a
|
|
44
75
|
#: boundary. No case-insensitive flag; digits are ASCII. A match includes its boundary character: callers test, never
|
|
45
76
|
#: read the matched text or its index.
|
|
46
|
-
SECRET_VALUE = re.compile("(^[Bb][Ee][Aa][Rr][Ee][Rr][" + JS_SPACE + "]" +
|
|
77
|
+
SECRET_VALUE = re.compile("(^[Bb][Ee][Aa][Rr][Ee][Rr][" + JS_SPACE + "]" + NOT_LINE_END + "+|" + _KEY_BOUNDARY + _PROVIDER_KEYS + ")")
|
|
47
78
|
_LONG_RUN = re.compile(r"[A-Za-z0-9+/=_-]{40,}")
|
|
48
|
-
|
|
79
|
+
BEARER_INSIDE = re.compile(r"(?<![A-Za-z0-9_])bearer[%s]+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{12,}=*" % JS_SPACE,
|
|
49
80
|
re.IGNORECASE | re.ASCII)
|
|
50
81
|
_PATH_LIKE = re.compile(r"^(?:[/~.]|[A-Za-z]:[\\/]|[a-z][a-z0-9+.-]*://)", re.IGNORECASE | re.ASCII)
|
|
51
82
|
_WORD_SPLIT = re.compile("[%s._-]+" % JS_SPACE)
|
|
52
83
|
_ARGUMENT_SPLIT = re.compile("[%s=]" % JS_SPACE)
|
|
53
|
-
|
|
84
|
+
SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
|
|
85
|
+
#: The names the check and older readers wrote before these became public (FAA-190 (2), (12)): kept so a module from
|
|
86
|
+
#: an earlier version beside this one still loads.
|
|
87
|
+
_BEARER_INSIDE, _SCHEME, _NOT_LINE_END = BEARER_INSIDE, SCHEME, NOT_LINE_END
|
|
54
88
|
#: WHATWG's special schemes that carry an authority with user information (file has none).
|
|
55
89
|
_SPECIAL = ("ftp", "http", "https", "ws", "wss")
|
|
56
90
|
|
|
@@ -77,13 +111,17 @@ def credential_name(name: str) -> bool:
|
|
|
77
111
|
return any(word in words and any(p in words for p in partners) for word, partners in CREDENTIAL_PAIRS.items())
|
|
78
112
|
|
|
79
113
|
|
|
80
|
-
def
|
|
114
|
+
def long_secret(value: str) -> bool:
|
|
115
|
+
"""A run of 40 or more base64 characters mixing upper case, lower case and digits (the Node twin's longSecret)."""
|
|
81
116
|
return any(re.search(r"[a-z]", run) and re.search(r"[A-Z]", run) and re.search(r"[0-9]", run) for run in _LONG_RUN.findall(value))
|
|
82
117
|
|
|
83
118
|
|
|
119
|
+
_long_secret = long_secret # its name before FAA-190 (2)
|
|
120
|
+
|
|
121
|
+
|
|
84
122
|
def secret_value(value: str) -> bool:
|
|
85
123
|
"""A JWT anywhere, a provider key or `Bearer …`, or a long base64 run mixing upper case, lower case and digits."""
|
|
86
|
-
return bool(
|
|
124
|
+
return bool(has_jwt(value) or SECRET_VALUE.search(value) or long_secret(value))
|
|
87
125
|
|
|
88
126
|
|
|
89
127
|
def _path_like(piece: str) -> bool:
|
|
@@ -95,13 +133,13 @@ def _path_like(piece: str) -> bool:
|
|
|
95
133
|
def secret_argument(value: str) -> bool:
|
|
96
134
|
"""The same for a launch argument or a server's command and URL: a long run is looked for between path separators,
|
|
97
135
|
so a folder path holding a session id is not taken for a key."""
|
|
98
|
-
if
|
|
136
|
+
if has_jwt(value) or SECRET_VALUE.search(value) or BEARER_INSIDE.search(value):
|
|
99
137
|
return True
|
|
100
138
|
for piece in _ARGUMENT_SPLIT.split(value):
|
|
101
139
|
if _path_like(piece):
|
|
102
|
-
if any(
|
|
140
|
+
if any(long_secret(part) for part in re.split(r"[\\/]", piece)):
|
|
103
141
|
return True
|
|
104
|
-
elif
|
|
142
|
+
elif long_secret(piece):
|
|
105
143
|
return True
|
|
106
144
|
return False
|
|
107
145
|
|
|
@@ -141,6 +179,155 @@ _C0_OR_SPACE = "".join(map(chr, range(0x21)))
|
|
|
141
179
|
_LONE_SURROGATE = re.compile("[\ud800-\udfff]")
|
|
142
180
|
|
|
143
181
|
|
|
182
|
+
# The WHATWG host parser, enough to know when `new URL` refuses a host (FAA-98, moved here from fabric_connections for
|
|
183
|
+
# secret_url in FAA-309, so the module still ships alone in adapting-projects-to-fabric).
|
|
184
|
+
_FORBIDDEN_HOST = set("\x00\t\n\r #/:<>?@[\\]^|")
|
|
185
|
+
_FORBIDDEN_DOMAIN = _FORBIDDEN_HOST | set(chr(c) for c in range(0x20)) | {"%", "\x7f"}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# FAA-98: UTS46 narrowed, not ported. A non-ASCII host is refused only where every Node from 20 to 26 refuses it, so
|
|
189
|
+
# the doubt stays on the closed side (its URL's credentials are read). The two tables are measured by
|
|
190
|
+
# test/fixtures/idna/make_idna_tables.mjs (idna-tables.json, held equal by test_the_idna_tables_are_the_measured_ones):
|
|
191
|
+
# code points refused in every context, and code points removed between letters. The contextual rules read Unicode 3.2
|
|
192
|
+
# classes (`unicodedata.ucd_3_2_0`) and skip what that Unicode does not know, since Node's own UTS46 data lags its
|
|
193
|
+
# Unicode: an Arabic letter of Unicode 14 is no right-to-left letter to it.
|
|
194
|
+
_IDNA_DISALLOWED = (
|
|
195
|
+
(0x80, 0xa0), (0xa8, 0xa8), (0xaf, 0xaf), (0xb4, 0xb4), (0xb8, 0xb8), (0x2d8, 0x2dd), (0x37a, 0x37a),
|
|
196
|
+
(0x384, 0x385), (0x600, 0x605), (0x61c, 0x61c), (0x6dd, 0x6dd), (0x70f, 0x70f), (0x890, 0x891), (0x8e2, 0x8e2),
|
|
197
|
+
(0x1680, 0x1680), (0x1fbd, 0x1fbd), (0x1fbf, 0x1fc1), (0x1fcd, 0x1fcf), (0x1fdd, 0x1fdf), (0x1fed, 0x1fee),
|
|
198
|
+
(0x1ffd, 0x1ffe), (0x2000, 0x200a), (0x200e, 0x200f), (0x2017, 0x2017), (0x2024, 0x2026), (0x2028, 0x202f),
|
|
199
|
+
(0x203e, 0x203e), (0x2047, 0x2049), (0x205f, 0x205f), (0x2066, 0x2069), (0x2100, 0x2101), (0x2105, 0x2106),
|
|
200
|
+
(0x2488, 0x249b), (0x2a74, 0x2a74), (0x2ff0, 0x3000), (0x309b, 0x309c), (0x31ef, 0x31ef), (0x33c2, 0x33c2),
|
|
201
|
+
(0x33c7, 0x33c7), (0x33d8, 0x33d8), (0xe000, 0xf8ff), (0xfc5e, 0xfc63), (0xfdfa, 0xfdfb), (0xfe12, 0xfe13),
|
|
202
|
+
(0xfe16, 0xfe16), (0xfe19, 0xfe19), (0xfe30, 0xfe30), (0xfe47, 0xfe4c), (0xfe52, 0xfe52), (0xfe55, 0xfe56),
|
|
203
|
+
(0xfe5f, 0xfe5f), (0xfe64, 0xfe65), (0xfe68, 0xfe68), (0xfe6a, 0xfe6b), (0xfe70, 0xfe70), (0xfe72, 0xfe72),
|
|
204
|
+
(0xfe74, 0xfe74), (0xfe76, 0xfe76), (0xfe78, 0xfe78), (0xfe7a, 0xfe7a), (0xfe7c, 0xfe7c), (0xfe7e, 0xfe7e),
|
|
205
|
+
(0xff03, 0xff03), (0xff05, 0xff05), (0xff0f, 0xff0f), (0xff1a, 0xff1a), (0xff1c, 0xff1c), (0xff1e, 0xff20),
|
|
206
|
+
(0xff3b, 0xff3e), (0xff5c, 0xff5c), (0xffe3, 0xffe3), (0xfff9, 0xfffd), (0x110bd, 0x110bd), (0x110cd, 0x110cd),
|
|
207
|
+
(0x13430, 0x1343f), (0x1f100, 0x1f100), (0xe0001, 0xe0001), (0xe0020, 0xe007f), (0xf0000, 0xffffd),
|
|
208
|
+
(0x100000, 0x10fffd),
|
|
209
|
+
)
|
|
210
|
+
_IDNA_IGNORED = (
|
|
211
|
+
(0xad, 0xad), (0x34f, 0x34f), (0x180b, 0x180d), (0x180f, 0x180f), (0x200b, 0x200b), (0x2060, 0x2060),
|
|
212
|
+
(0x2064, 0x2064), (0xfe00, 0xfe0f), (0xfeff, 0xfeff), (0x1bca0, 0x1bca3), (0xe0100, 0xe01ef),
|
|
213
|
+
)
|
|
214
|
+
_IDNA_DOTS = re.compile("[.\u3002\uff0e\uff61]")
|
|
215
|
+
_U32 = unicodedata.ucd_3_2_0
|
|
216
|
+
_RTL_OK = {"R", "AL", "AN", "EN", "ES", "CS", "ET", "ON", "BN", "NSM"}
|
|
217
|
+
_JOINERS = "\u200c\u200d"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _in_ranges(ranges: Sequence[tuple], cp: int) -> bool:
|
|
221
|
+
i = bisect.bisect_right(ranges, (cp, 0x10ffff)) - 1
|
|
222
|
+
return i >= 0 and ranges[i][0] <= cp <= ranges[i][1]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _known(c: str) -> bool:
|
|
226
|
+
return _U32.category(c) != "Cn" and unicodedata.category(c) != "Cn"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _joiner_refused(label: str, i: int) -> bool:
|
|
230
|
+
"""CheckJoiners as Node 20 to 26 all read it: they find a joining context, or a virama, anywhere in the label
|
|
231
|
+
(`\u0628a\u200c\u0627` and `\u0628\u200c\u0627\u200d` parse), so only a joiner with nothing but ASCII beside it
|
|
232
|
+
in its label has none for certain."""
|
|
233
|
+
return all(c.isascii() for j, c in enumerate(label) if j != i)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _bidi_refused(label: str) -> bool:
|
|
237
|
+
"""RFC 5893 as Node 20 to 26 all read it: only a label holding R, AL or AN is judged, and its first code point is
|
|
238
|
+
not (Node 26 alone refuses a label opening with a digit or a hyphen)."""
|
|
239
|
+
if not label or not all(_known(c) for c in label) or any(c in _JOINERS for c in label):
|
|
240
|
+
return False # Node 20 to 26 pass a label holding a joiner over (`\u05d0a\u094d\u200d` parses)
|
|
241
|
+
k = [_U32.bidirectional(c) for c in label]
|
|
242
|
+
if not any(x in ("R", "AL", "AN") for x in k):
|
|
243
|
+
return False
|
|
244
|
+
body = list(k)
|
|
245
|
+
while body and body[-1] == "NSM":
|
|
246
|
+
body.pop()
|
|
247
|
+
if not body or body[-1] not in ("R", "AL", "EN", "AN"):
|
|
248
|
+
return True
|
|
249
|
+
return k[0] in ("R", "AL") and (any(x not in _RTL_OK for x in k) or ("EN" in k and "AN" in k))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _idna_host(host: str) -> Optional[str]:
|
|
253
|
+
"""None where WHATWG's domain-to-ASCII refuses a non-ASCII host for certain; else the host as UTS46 maps it, read
|
|
254
|
+
as NFKC with case folding (exact enough for the ASCII rules that follow, which read digits and dots)."""
|
|
255
|
+
if any(_in_ranges(_IDNA_DISALLOWED, ord(c)) for c in host):
|
|
256
|
+
return None
|
|
257
|
+
kept = "".join(c for c in host if not _in_ranges(_IDNA_IGNORED, ord(c)))
|
|
258
|
+
if kept == "":
|
|
259
|
+
return None # nothing left: an empty host
|
|
260
|
+
if not all(_known(c) for c in kept):
|
|
261
|
+
return host # a code point this Python's Unicode does not know: the contextual rules are not read
|
|
262
|
+
mapped = unicodedata.normalize("NFC", unicodedata.normalize("NFKC", unicodedata.normalize("NFKC", kept).casefold()))
|
|
263
|
+
for label in _IDNA_DOTS.split(mapped):
|
|
264
|
+
first = label[:1]
|
|
265
|
+
if first and first not in "\u17b4\u17b5" and _U32.category(first)[0] == "M" and unicodedata.category(first)[0] == "M":
|
|
266
|
+
return None # a label opening with a mark (U+17B4 and U+17B5 are ignored by the newer Nodes)
|
|
267
|
+
if any(c in _JOINERS and _joiner_refused(label, i) for i, c in enumerate(label)):
|
|
268
|
+
return None
|
|
269
|
+
if _bidi_refused(label):
|
|
270
|
+
return None
|
|
271
|
+
return _IDNA_DOTS.sub(".", mapped)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _ipv4_number(part: str) -> Optional[int]:
|
|
275
|
+
if part == "":
|
|
276
|
+
return None # an empty part fails WHATWG's IPv4 parser (`1..2`); `0x` alone is 0
|
|
277
|
+
base = 10
|
|
278
|
+
if part[:2].lower() == "0x":
|
|
279
|
+
part, base = part[2:], 16
|
|
280
|
+
elif len(part) > 1 and part[0] == "0":
|
|
281
|
+
part, base = part[1:], 8
|
|
282
|
+
if part == "":
|
|
283
|
+
return 0
|
|
284
|
+
digits = {10: "0123456789", 16: "0123456789abcdefABCDEF", 8: "01234567"}[base]
|
|
285
|
+
return int(part, base) if all(c in digits for c in part) else None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _domain_ok(host: str) -> bool:
|
|
289
|
+
"""The WHATWG host parser on a special scheme's host, enough to know when `new URL` refuses it."""
|
|
290
|
+
if host.startswith("["):
|
|
291
|
+
return host.endswith("]") and _ipv6_ok(host[1:-1])
|
|
292
|
+
if re.search(r"%(?![0-9A-Fa-f]{2})", host) is None:
|
|
293
|
+
try:
|
|
294
|
+
host = urllib.parse.unquote_to_bytes(host).decode("utf-8")
|
|
295
|
+
except UnicodeDecodeError:
|
|
296
|
+
return False
|
|
297
|
+
if any(c in _FORBIDDEN_DOMAIN for c in host):
|
|
298
|
+
return False
|
|
299
|
+
# A non-ASCII host goes through UTS46 in WHATWG; this module reads it narrowly (_idna_host, FAA-98). When unsure,
|
|
300
|
+
# the host is taken as valid, so its URL's credentials are read: the doubt then hides a list rather than publish one
|
|
301
|
+
# (round-2 reading). A host UTS46 maps to ASCII is read by the ASCII rules below (`127.0.0.1` is an address).
|
|
302
|
+
if any(ord(c) > 0x7f for c in host):
|
|
303
|
+
mapped = _idna_host(host)
|
|
304
|
+
if mapped is None:
|
|
305
|
+
return False
|
|
306
|
+
if not mapped.isascii() or any(c in _FORBIDDEN_DOMAIN for c in mapped):
|
|
307
|
+
return True
|
|
308
|
+
host = mapped
|
|
309
|
+
labels = host.split(".")
|
|
310
|
+
if labels and labels[-1] == "" and len(labels) > 1:
|
|
311
|
+
labels = labels[:-1]
|
|
312
|
+
last = labels[-1] if labels else ""
|
|
313
|
+
if not (last and (last.isdigit() and last.isascii() or re.fullmatch(r"0[xX][0-9A-Fa-f]*", last))):
|
|
314
|
+
return host != ""
|
|
315
|
+
numbers = [_ipv4_number(label) for label in labels]
|
|
316
|
+
if len(numbers) > 4 or any(n is None for n in numbers):
|
|
317
|
+
return False
|
|
318
|
+
return all(n <= 255 for n in numbers[:-1]) and numbers[-1] < 256 ** (5 - len(numbers))
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _ipv6_ok(text: str) -> bool:
|
|
322
|
+
if "%" in text:
|
|
323
|
+
return False # a zone id: `ipaddress` takes one, WHATWG does not
|
|
324
|
+
try:
|
|
325
|
+
ipaddress.IPv6Address(text)
|
|
326
|
+
return True
|
|
327
|
+
except ValueError:
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
|
|
144
331
|
def _percent_encode(text: str, also: str) -> str:
|
|
145
332
|
"""WHATWG's percent-encoding of a query or a fragment: every C0 control, space, `"`, `<`, `>`, every code point
|
|
146
333
|
above U+007E and the characters in `also`, as UTF-8 %XX (a lone surrogate as U+FFFD, as a USVString reads it).
|
|
@@ -162,8 +349,8 @@ def url_parts(value: str) -> Tuple[str, str, str]:
|
|
|
162
349
|
after every slash or backslash that follows `scheme:` (the special authority ignore slashes state) and ends at the
|
|
163
350
|
next slash, backslash, `?` or `#`, so `<https>:u:p@h` and `<https>:\\u:p@h` carry the password p; any other scheme
|
|
164
351
|
has an authority only after `//`, and `file` has no user information. The user information ends at the authority's
|
|
165
|
-
last `@`, and the password follows its first `:`.
|
|
166
|
-
|
|
352
|
+
last `@`, and the password follows its first `:`. The host is not checked here: secret_url refuses first a URL
|
|
353
|
+
whose host or port `new URL` refuses (url_refused, FAA-309)."""
|
|
167
354
|
scheme, _, rest = value.partition(":")
|
|
168
355
|
rest, _, fragment = rest.partition("#")
|
|
169
356
|
rest, _, query = rest.partition("?")
|
|
@@ -182,17 +369,69 @@ def url_parts(value: str) -> Tuple[str, str, str]:
|
|
|
182
369
|
_url_parts = url_parts # the name before FAA-250, kept for code that reached it
|
|
183
370
|
|
|
184
371
|
|
|
372
|
+
def url_refused(value: str) -> bool:
|
|
373
|
+
"""Whether `new URL` refuses an absolute URL (trimmed as secret_url trims it) for its host or port (FAA-309), read
|
|
374
|
+
with the same authority url_parts reads: a special scheme's host through the WHATWG host parser and the IDNA reading
|
|
375
|
+
of FAA-98 (`_domain_ok`), never empty; `file`'s host, after `//`, the same, empty allowed; another scheme's host,
|
|
376
|
+
after `//`, without a forbidden host code point, and empty only with no user information and no `:` after it (an
|
|
377
|
+
empty port is refused too); a port of ASCII digits up to 65535. Where the reading is unsure, the host is taken as
|
|
378
|
+
valid (the detector's safe side)."""
|
|
379
|
+
value = _LONE_SURROGATE.sub("\ufffd", value)
|
|
380
|
+
scheme, _, rest = value.partition(":")
|
|
381
|
+
rest = rest.partition("#")[0].partition("?")[0]
|
|
382
|
+
lowered = scheme.lower()
|
|
383
|
+
if lowered in _SPECIAL:
|
|
384
|
+
authority = rest.replace("\\", "/").lstrip("/").partition("/")[0]
|
|
385
|
+
elif lowered == "file":
|
|
386
|
+
rest = rest.replace("\\", "/")
|
|
387
|
+
if not rest.startswith("//"):
|
|
388
|
+
return False
|
|
389
|
+
host = rest[2:].partition("/")[0]
|
|
390
|
+
return not (host == "" or re.fullmatch(r"[A-Za-z][:|]", host) or _domain_ok(host))
|
|
391
|
+
elif rest.startswith("//"):
|
|
392
|
+
authority = rest[2:].partition("/")[0]
|
|
393
|
+
else:
|
|
394
|
+
return False
|
|
395
|
+
hostport = authority.rpartition("@")[2]
|
|
396
|
+
if hostport.startswith("["):
|
|
397
|
+
close = hostport.find("]")
|
|
398
|
+
if close < 0:
|
|
399
|
+
return True
|
|
400
|
+
host, after = hostport[:close + 1], hostport[close + 1:]
|
|
401
|
+
if after and not after.startswith(":"):
|
|
402
|
+
return True
|
|
403
|
+
port = after[1:]
|
|
404
|
+
else:
|
|
405
|
+
host, _, port = hostport.partition(":")
|
|
406
|
+
if port and not (port.isascii() and port.isdigit() and int(port) <= 65535):
|
|
407
|
+
return True
|
|
408
|
+
if lowered in _SPECIAL:
|
|
409
|
+
return host == "" or not _domain_ok(host)
|
|
410
|
+
if host.startswith("["):
|
|
411
|
+
return not _ipv6_ok(host[1:-1])
|
|
412
|
+
return any(c in _FORBIDDEN_HOST for c in host) or (host == "" and ("@" in authority or ":" in hostport))
|
|
413
|
+
|
|
414
|
+
|
|
185
415
|
def secret_url(value: str) -> bool:
|
|
186
416
|
"""Whether a URL an MCP entry or a launch names carries a credential (LC-21): anything secret_argument finds, a
|
|
187
417
|
password in its user information, or a query or fragment parameter whose name can only carry a credential and has
|
|
188
|
-
a value, or whose value is shaped like one. A string that is not an absolute URL
|
|
418
|
+
a value, or whose value is shaped like one. A string that is not an absolute URL, or one whose host or port
|
|
419
|
+
`new URL` refuses (url_refused, FAA-309), is checked as an argument only, as the Node twin's secretUrl reads it."""
|
|
420
|
+
return _url_credential(value, hosts=True)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _url_credential(value: str, hosts: bool) -> bool:
|
|
424
|
+
"""secret_url; with `hosts` False the host is not checked, as repository_credential reads a repository's parameters
|
|
425
|
+
whatever its host (the Node twin's repositoryParts)."""
|
|
189
426
|
if secret_argument(value):
|
|
190
427
|
return True
|
|
191
428
|
# What WHATWG URL parsing does first: trim C0 controls and space from the ends (never other Unicode spaces, which it
|
|
192
429
|
# refuses), then remove every tab, CR and LF.
|
|
193
430
|
value = re.sub(r"[\t\n\r]", "", value.strip(_C0_OR_SPACE))
|
|
194
|
-
if not
|
|
431
|
+
if not SCHEME.match(value):
|
|
195
432
|
return False # not an absolute URL (WHATWG URL parsing refuses it)
|
|
433
|
+
if hosts and url_refused(value):
|
|
434
|
+
return False # `new URL` refuses its host or port, so the Node twin reads it as an argument only (FAA-309)
|
|
196
435
|
password, query, fragment = url_parts(value)
|
|
197
436
|
if password:
|
|
198
437
|
return True
|
|
@@ -210,7 +449,25 @@ _REPOSITORY_AUTHORITY = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://")
|
|
|
210
449
|
#: boundary, so composed after a separator look-behind it would miss a key after `-`, `.` or `_`.
|
|
211
450
|
_REPOSITORY_KEYS = r"sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,}"
|
|
212
451
|
_KEY_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))(?:%s)" % _REPOSITORY_KEYS)
|
|
213
|
-
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def jwt_at_a_word(text: str) -> bool:
|
|
455
|
+
"""Whether JWT_SHAPE matches at a word's start: the text's start, or after a `.`, `_` or `-` (FAA-321). Read in one
|
|
456
|
+
pass, as jwt_spans: the look-behind expression backtracked from every such `eyJ` of a long run, quadratic in it.
|
|
457
|
+
Every `eyJ` of one run shares that run's end and a shorter first segment, so only a run's first `eyJ` at a word's
|
|
458
|
+
start is tried; an `eyJ` that is not at one is passed over, since a later one of its run may be."""
|
|
459
|
+
at = text.find("eyJ")
|
|
460
|
+
while at >= 0:
|
|
461
|
+
if at and text[at - 1] not in "._-":
|
|
462
|
+
at = text.find("eyJ", at + 1)
|
|
463
|
+
continue
|
|
464
|
+
one = _URL_BASE64_RUN.match(text, at + 3).end()
|
|
465
|
+
if one - at - 3 >= 2 and text[one:one + 1] == ".":
|
|
466
|
+
two = _URL_BASE64_RUN.match(text, one + 1).end()
|
|
467
|
+
if two - one - 1 >= 2 and text[two:two + 1] == ".":
|
|
468
|
+
return True
|
|
469
|
+
at = text.find("eyJ", one)
|
|
470
|
+
return False
|
|
214
471
|
|
|
215
472
|
|
|
216
473
|
def repository_name(piece: str) -> bool:
|
|
@@ -218,7 +475,7 @@ def repository_name(piece: str) -> bool:
|
|
|
218
475
|
key's or JWT's shape (`sk-`, `ghp_`, `xoxb-`, `AKIA`, `eyJ`) at its start or after a `.`, `_` or `-` (0.62.0
|
|
219
476
|
readings 9 and 10), so a letters-only key is still a key while `task-pipeline-dashboard`, which holds an `sk-` shape
|
|
220
477
|
inside a word, is a name."""
|
|
221
|
-
return bool(_REPOSITORY_NAME.match(piece)) and not _KEY_AT_A_WORD.search(piece) and not
|
|
478
|
+
return bool(_REPOSITORY_NAME.match(piece)) and not _KEY_AT_A_WORD.search(piece) and not jwt_at_a_word(piece)
|
|
222
479
|
|
|
223
480
|
|
|
224
481
|
def ascii_decoded(text: str) -> str:
|
|
@@ -260,4 +517,4 @@ def repository_credential(value: str) -> bool:
|
|
|
260
517
|
odd = [piece for written in (path, ascii_decoded(path)) for piece in re.split(r"[/\\:]", written)
|
|
261
518
|
if piece and not repository_name(piece)]
|
|
262
519
|
# the scheme and authority decoded too: git sends `https://%67hp_...@h` with its user decoded (unit reading 12)
|
|
263
|
-
return
|
|
520
|
+
return _url_credential(head + tail, hosts=False) or secret_argument(ascii_decoded(head)) or any(secret_argument(piece) for piece in odd)
|