@mongoui/skills 0.2.0 → 0.2.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/README.md +50 -0
- package/dist/products/index.js +7 -1
- package/package.json +7 -3
- package/skills/bms-api/SKILL.md +26 -0
- package/skills/bms-api/references/api-workflows.md +27 -0
- package/skills/bms-api/references/auth-and-calling.md +35 -0
- package/skills/bms-api/references/endpoint-index.md +137 -0
- package/skills/bms-api/references/openapi.json +1 -0
- package/skills/bms-api/references/source-manifest.json +5 -0
- package/skills/bms-api/scripts/bms_signed_request.py +103 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Sign BMS JSON requests offline; use --send explicitly for a network request."""
|
|
3
|
+
import argparse
|
|
4
|
+
import hashlib
|
|
5
|
+
import hmac
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import urllib.request
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
TOKEN_EXCEPTIONS = {'/openapi/v1/bms/auth/token', '/openapi/v1/bms/auth/customer/register'}
|
|
17
|
+
|
|
18
|
+
def canonical_query(raw):
|
|
19
|
+
# Match pkg/crypto/signature.go: decode form query before sorting; no re-encoding.
|
|
20
|
+
return '&'.join(key+'='+value for key, value in sorted(urllib.parse.parse_qsl(raw, keep_blank_values=True)))
|
|
21
|
+
|
|
22
|
+
def sign_request(method, path, raw_query, body, app_id, secret, timestamp, nonce):
|
|
23
|
+
canonical = '\n'.join([method.upper(), path, canonical_query(raw_query), hashlib.sha256(body).hexdigest(), timestamp.strip(), nonce.strip(), app_id.strip()])
|
|
24
|
+
signature = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
|
|
25
|
+
return canonical, {'X-OP-App-Key': app_id.strip(), 'X-OP-Timestamp': timestamp.strip(), 'X-OP-Nonce': nonce.strip(), 'X-OP-Sign': signature}
|
|
26
|
+
|
|
27
|
+
def redact(value):
|
|
28
|
+
if isinstance(value, dict):
|
|
29
|
+
return {key: '<redacted>' if any(word in key.lower() for word in ['secret', 'token', 'password', 'aeskey', 'sign']) else redact(item) for key, item in value.items()}
|
|
30
|
+
if isinstance(value, list):
|
|
31
|
+
return [redact(item) for item in value]
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
35
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
def main():
|
|
39
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
40
|
+
parser.add_argument('--method', required=True)
|
|
41
|
+
parser.add_argument('--path', required=True, help='Complete public path without query')
|
|
42
|
+
parser.add_argument('--query', default='', help='Actual URL query string; percent-encoded when needed')
|
|
43
|
+
body_group = parser.add_mutually_exclusive_group()
|
|
44
|
+
body_group.add_argument('--body-json')
|
|
45
|
+
body_group.add_argument('--body-file', type=Path, help='Exact UTF-8 JSON body bytes')
|
|
46
|
+
parser.add_argument('--base-url', default=os.getenv('OPEN_API_BASE_URL'))
|
|
47
|
+
parser.add_argument('--timestamp', default=str(int(time.time())))
|
|
48
|
+
parser.add_argument('--nonce', default=uuid.uuid4().hex)
|
|
49
|
+
parser.add_argument('--send', action='store_true')
|
|
50
|
+
parser.add_argument('--timeout', type=float, default=20)
|
|
51
|
+
args = parser.parse_args()
|
|
52
|
+
app_id = os.getenv('OPEN_APP_ID', '').strip()
|
|
53
|
+
secret = os.getenv('OPEN_APP_SECRET', '')
|
|
54
|
+
token = os.getenv('BMS_TOKEN', '').strip()
|
|
55
|
+
if not app_id or not secret:
|
|
56
|
+
parser.error('Set OPEN_APP_ID and OPEN_APP_SECRET in the server environment.')
|
|
57
|
+
if not args.path.startswith('/openapi/v1/bms/') or '?' in args.path or '#' in args.path:
|
|
58
|
+
parser.error('--path must be the full public BMS path, without query or fragment.')
|
|
59
|
+
try:
|
|
60
|
+
if args.body_file:
|
|
61
|
+
body = args.body_file.read_bytes()
|
|
62
|
+
json.loads(body)
|
|
63
|
+
elif args.body_json is not None:
|
|
64
|
+
body = json.dumps(json.loads(args.body_json), ensure_ascii=False, separators=(',', ':')).encode()
|
|
65
|
+
else:
|
|
66
|
+
body = b''
|
|
67
|
+
except (OSError, ValueError):
|
|
68
|
+
parser.error('Body must be valid JSON; multipart and file downloads need a separate client.')
|
|
69
|
+
canonical, headers = sign_request(args.method, args.path, args.query, body, app_id, secret, args.timestamp, args.nonce)
|
|
70
|
+
headers['Accept'] = 'application/json'
|
|
71
|
+
if body:
|
|
72
|
+
headers['Content-Type'] = 'application/json'
|
|
73
|
+
if args.path not in TOKEN_EXCEPTIONS:
|
|
74
|
+
if args.send and not token:
|
|
75
|
+
parser.error('Business requests require BMS_TOKEN from this environment.')
|
|
76
|
+
if token:
|
|
77
|
+
headers['token'] = token
|
|
78
|
+
if not args.send:
|
|
79
|
+
print(json.dumps({'mode':'offline', 'method':args.method.upper(), 'path':args.path, 'canonical_string':canonical, 'headers':redact(headers), 'body_bytes':len(body), 'token_required':args.path not in TOKEN_EXCEPTIONS}, ensure_ascii=False, indent=2))
|
|
80
|
+
return 0
|
|
81
|
+
parsed = urllib.parse.urlsplit(args.base_url or '')
|
|
82
|
+
if parsed.scheme not in {'http','https'} or not parsed.netloc or parsed.username or parsed.query or parsed.fragment or parsed.path.rstrip('/'):
|
|
83
|
+
parser.error('Set OPEN_API_BASE_URL to the selected Open Platform gateway origin, without credentials, path, query or fragment.')
|
|
84
|
+
url = args.base_url.rstrip('/')+args.path+('?' + args.query if args.query else '')
|
|
85
|
+
request = urllib.request.Request(url, data=body if body else None, headers=headers, method=args.method.upper())
|
|
86
|
+
try:
|
|
87
|
+
response = urllib.request.build_opener(NoRedirect()).open(request, timeout=args.timeout)
|
|
88
|
+
with response:
|
|
89
|
+
status, data = response.status, response.read()
|
|
90
|
+
except urllib.error.HTTPError as error:
|
|
91
|
+
status, data = error.code, error.read()
|
|
92
|
+
except (urllib.error.URLError, TimeoutError):
|
|
93
|
+
print('Request outcome unknown. Query the business record before replaying a write.', file=sys.stderr)
|
|
94
|
+
return 1
|
|
95
|
+
print('HTTP', status)
|
|
96
|
+
try:
|
|
97
|
+
print(json.dumps(redact(json.loads(data)), ensure_ascii=False, indent=2))
|
|
98
|
+
except (ValueError, UnicodeError):
|
|
99
|
+
print('Non-JSON response; use a binary-capable client for exports.')
|
|
100
|
+
return 0 if 200 <= status < 300 else 1
|
|
101
|
+
|
|
102
|
+
if __name__ == '__main__':
|
|
103
|
+
raise SystemExit(main())
|