vpndetection-fastapi 2.1.7__tar.gz → 2.1.9__tar.gz

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.
@@ -2,6 +2,18 @@
2
2
 
3
3
  What each release changed for you, newest first. Each line is a commit's summary, linked to its full description and diff. Releases before 2.0.5 are described by their release commits.
4
4
 
5
+ ## 2.1.9 - 2026-10-20
6
+
7
+ ### Features
8
+
9
+ - Require vpndetection 5.11.0: lookup results carry location and asn ([`ada4345`](https://github.com/vpndetection-io/sdk-python-fastapi/commit/ada4345bde920539b2f279f03d790ae42a74998d))
10
+
11
+ ## 2.1.8 - 2026-10-19
12
+
13
+ ### Fixes
14
+
15
+ - Require vpndetection 5.10.0: num_ports and num_proxies in the pubproxy detail ([`c5fa5cb`](https://github.com/vpndetection-io/sdk-python-fastapi/commit/c5fa5cb80237225ebb3d7aee077094549fbf9451))
16
+
5
17
  ## 2.1.7 - 2026-10-18
6
18
 
7
19
  ### Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vpndetection-fastapi
3
- Version: 2.1.7
3
+ Version: 2.1.9
4
4
  Summary: Official FastAPI and Starlette middleware for the VPNDetection API. Detect VPNs, proxies, Tor, hosting and CDN visitors on every request.
5
5
  Project-URL: Homepage, https://vpndetection.io
6
6
  Project-URL: Source, https://github.com/vpndetection-io/sdk-python-fastapi
@@ -20,7 +20,7 @@ Classifier: Topic :: Security
20
20
  Classifier: Typing :: Typed
21
21
  Requires-Python: >=3.11
22
22
  Requires-Dist: starlette>=0.37
23
- Requires-Dist: vpndetection<6,>=5.9.0
23
+ Requires-Dist: vpndetection<6,>=5.11.0
24
24
  Provides-Extra: dev
25
25
  Requires-Dist: fastapi>=0.115; extra == 'dev'
26
26
  Requires-Dist: mypy>=1.11; extra == 'dev'
@@ -45,7 +45,7 @@ pip install vpndetection-fastapi
45
45
 
46
46
  Requires Python 3.11 or newer.
47
47
 
48
- You need an API key. Create one in the [console](https://app.vpndetection.io); the free tier's allowance is counted per source address, and a server is a single source address, so a key is what makes this usable in production rather than optional.
48
+ You need an API key. Create one in the [console](https://app.vpndetection.io); without one the allowance is counted per source address, and a server is a single source address, so a key is what makes this usable in production rather than optional.
49
49
 
50
50
  ```python
51
51
  from fastapi import FastAPI
@@ -146,7 +146,7 @@ Beyond a few million distinct visitors a day, stop calling the API per request:
146
146
 
147
147
  ## Absent is not false
148
148
 
149
- Only `ip` and `is_vpn` come back on every plan. A field your plan does not include is `None`, which means "not in your plan" rather than "checked, and no".
149
+ Without a key only `ip` and `is_vpn` come back, and a free key adds Tor, relay, `location` and `asn`. A field your plan does not include is `None`, which means "not in your plan" rather than "checked, and no".
150
150
 
151
151
  ```python
152
152
  lookup(request).result.is_hosting # None when your plan does not include it
@@ -15,7 +15,7 @@ pip install vpndetection-fastapi
15
15
 
16
16
  Requires Python 3.11 or newer.
17
17
 
18
- You need an API key. Create one in the [console](https://app.vpndetection.io); the free tier's allowance is counted per source address, and a server is a single source address, so a key is what makes this usable in production rather than optional.
18
+ You need an API key. Create one in the [console](https://app.vpndetection.io); without one the allowance is counted per source address, and a server is a single source address, so a key is what makes this usable in production rather than optional.
19
19
 
20
20
  ```python
21
21
  from fastapi import FastAPI
@@ -116,7 +116,7 @@ Beyond a few million distinct visitors a day, stop calling the API per request:
116
116
 
117
117
  ## Absent is not false
118
118
 
119
- Only `ip` and `is_vpn` come back on every plan. A field your plan does not include is `None`, which means "not in your plan" rather than "checked, and no".
119
+ Without a key only `ip` and `is_vpn` come back, and a free key adds Tor, relay, `location` and `asn`. A field your plan does not include is `None`, which means "not in your plan" rather than "checked, and no".
120
120
 
121
121
  ```python
122
122
  lookup(request).result.is_hosting # None when your plan does not include it
@@ -0,0 +1,5 @@
1
+ # Security policy
2
+
3
+ Please report a security issue in this repository by email to security@vpndetection.io, not in a public issue, pull request or discussion. Include what you found and the steps that reproduce it.
4
+
5
+ Keep any testing against the VPNDetection API to your own account and API key, and give us a reasonable chance to fix the issue before you publish it.
@@ -24,7 +24,7 @@ classifiers = [
24
24
  "Topic :: Security",
25
25
  "Typing :: Typed",
26
26
  ]
27
- dependencies = ["vpndetection>=5.9.0,<6", "starlette>=0.37"]
27
+ dependencies = ["vpndetection>=5.11.0,<6", "starlette>=0.37"]
28
28
 
29
29
  [project.urls]
30
30
  Homepage = "https://vpndetection.io"
@@ -45,7 +45,7 @@ __all__ = [
45
45
  "xff_ip_selector",
46
46
  ]
47
47
 
48
- __version__ = "2.1.7"
48
+ __version__ = "2.1.9"
49
49
 
50
50
  # Set on a request ``skip`` claimed, so block_if can tell it from one the middleware
51
51
  # never saw.
@@ -296,7 +296,7 @@
296
296
  }
297
297
  ],
298
298
  "bogonResponse": {
299
- "why": "A bogon is answered locally in the full max shape. Every flag is present and false, every detail object is present and empty, and is_bogon marks it as computed rather than served.",
299
+ "why": "A bogon is answered locally in the full shape. Every flag is present and false, every detail object is present and empty, location and asn carry every key empty as the API answers a bogon (asn 0, strings empty), and is_bogon marks it as computed rather than served.",
300
300
  "flagsFalse": [
301
301
  "is_vpn",
302
302
  "is_hosting",
@@ -318,7 +318,26 @@
318
318
  "dcproxy",
319
319
  "mobproxy",
320
320
  "pubproxy"
321
- ]
321
+ ],
322
+ "location": {
323
+ "continent": "",
324
+ "continent_name": "",
325
+ "country": "",
326
+ "country_name": "",
327
+ "timezone": "",
328
+ "currency_code": "",
329
+ "currency_name": "",
330
+ "currency_symbol": "",
331
+ "flag_emoji": "",
332
+ "flag_unicode": ""
333
+ },
334
+ "asn": {
335
+ "asn": 0,
336
+ "name": "",
337
+ "domain": "",
338
+ "type": "",
339
+ "prefix": ""
340
+ }
322
341
  },
323
342
  "lookup": [
324
343
  {
@@ -493,11 +512,13 @@
493
512
  "mobproxy": {},
494
513
  "pubproxy": {
495
514
  "entry": true,
496
- "exit": false,
515
+ "exit": true,
497
516
  "protocols": [
498
517
  "http",
499
518
  "https"
500
519
  ],
520
+ "num_ports": 3,
521
+ "num_proxies": 2,
501
522
  "last_seen": "2026-10-17",
502
523
  "last_usable": ""
503
524
  }
@@ -528,11 +549,13 @@
528
549
  },
529
550
  "pubproxy": {
530
551
  "entry": true,
531
- "exit": false,
552
+ "exit": true,
532
553
  "protocols": [
533
554
  "http",
534
555
  "https"
535
556
  ],
557
+ "num_ports": 3,
558
+ "num_proxies": 2,
536
559
  "last_seen": "2026-10-17",
537
560
  "last_usable": ""
538
561
  }
@@ -614,6 +637,85 @@
614
637
  "pubproxy"
615
638
  ]
616
639
  }
640
+ },
641
+ {
642
+ "name": "free-location-asn",
643
+ "why": "A free key's shape since 2026-10-20: Tor and relay beside the VPN flag, and where the address is and which AS announces it. location and asn carry every key whenever present, so each maps whole.",
644
+ "status": 200,
645
+ "body": {
646
+ "ip": "146.70.116.130",
647
+ "is_vpn": true,
648
+ "is_relay": false,
649
+ "is_tor": false,
650
+ "relay": {},
651
+ "tor": {},
652
+ "location": {
653
+ "continent": "EU",
654
+ "continent_name": "Europe",
655
+ "country": "AT",
656
+ "country_name": "Austria",
657
+ "timezone": "Europe/Vienna",
658
+ "currency_code": "EUR",
659
+ "currency_name": "Euro",
660
+ "currency_symbol": "€",
661
+ "flag_emoji": "🇦🇹",
662
+ "flag_unicode": "U+1F1E6 U+1F1F9"
663
+ },
664
+ "asn": {
665
+ "asn": 9009,
666
+ "name": "M247 Europe SRL",
667
+ "domain": "m247global.com",
668
+ "type": "hosting",
669
+ "prefix": "146.70.116.0/24"
670
+ }
671
+ },
672
+ "expect": {
673
+ "ip": "146.70.116.130",
674
+ "isBogon": false,
675
+ "present": {
676
+ "is_vpn": true,
677
+ "is_relay": false,
678
+ "is_tor": false
679
+ },
680
+ "absent": [
681
+ "is_hosting",
682
+ "is_cdn",
683
+ "is_resproxy",
684
+ "is_dcproxy",
685
+ "is_mobproxy",
686
+ "is_pubproxy",
687
+ "vpn",
688
+ "hosting",
689
+ "cdn",
690
+ "resproxy",
691
+ "dcproxy",
692
+ "mobproxy",
693
+ "pubproxy"
694
+ ],
695
+ "emptyPresent": [
696
+ "relay",
697
+ "tor"
698
+ ],
699
+ "location": {
700
+ "continent": "EU",
701
+ "continent_name": "Europe",
702
+ "country": "AT",
703
+ "country_name": "Austria",
704
+ "timezone": "Europe/Vienna",
705
+ "currency_code": "EUR",
706
+ "currency_name": "Euro",
707
+ "currency_symbol": "€",
708
+ "flag_emoji": "🇦🇹",
709
+ "flag_unicode": "U+1F1E6 U+1F1F9"
710
+ },
711
+ "asn": {
712
+ "asn": 9009,
713
+ "name": "M247 Europe SRL",
714
+ "domain": "m247global.com",
715
+ "type": "hosting",
716
+ "prefix": "146.70.116.0/24"
717
+ }
718
+ }
617
719
  }
618
720
  ],
619
721
  "errors": [
@@ -5503,6 +5605,8 @@
5503
5605
  "protocols": [
5504
5606
  "socks5"
5505
5607
  ],
5608
+ "num_ports": 1,
5609
+ "num_proxies": 0,
5506
5610
  "last_seen": "2026-10-17",
5507
5611
  "last_usable": "2026-10-16"
5508
5612
  }
@@ -5562,6 +5666,8 @@
5562
5666
  "protocols": [
5563
5667
  "socks5"
5564
5668
  ],
5669
+ "num_ports": 1,
5670
+ "num_proxies": 0,
5565
5671
  "last_seen": "2026-10-17",
5566
5672
  "last_usable": "2026-10-16"
5567
5673
  }
@@ -6807,6 +6913,497 @@
6807
6913
  }
6808
6914
  }
6809
6915
  ]
6916
+ },
6917
+ "register": {
6918
+ "why": "Vectors for register, RFC 7591 dynamic client registration: the client_id a client gets when it cannot serve a Client ID Metadata Document. The one OAuth request whose body is JSON. Nothing is stored until a person approves the client, so registering again consumes nothing and the operation takes the client's retries. noCredential holds for it as for every OAuth operation. A runner that has not shipped register never reads this key; nothing outside it names the operation.",
6919
+ "endpoint": {
6920
+ "method": "POST",
6921
+ "path": "/oauth/register"
6922
+ },
6923
+ "contentType": "application/json",
6924
+ "bodies": {
6925
+ "why": "Decode the received body as JSON and compare it EXACTLY: same members, no extras, same values, redirect_uris in the order given. An optional argument given empty is left out, never sent empty. Values carrying a quote, a backslash, + & = % / space and non-ASCII catch an encoder that builds JSON by hand.",
6926
+ "cases": [
6927
+ {
6928
+ "name": "redirect-uris-only",
6929
+ "args": {
6930
+ "redirectUris": [
6931
+ "http://127.0.0.1:8765/callback"
6932
+ ]
6933
+ },
6934
+ "body": {
6935
+ "redirect_uris": [
6936
+ "http://127.0.0.1:8765/callback"
6937
+ ]
6938
+ }
6939
+ },
6940
+ {
6941
+ "name": "several-uris-and-a-name",
6942
+ "args": {
6943
+ "redirectUris": [
6944
+ "https://app.example.com/oauth/callback",
6945
+ "http://localhost/cb?a=1&b=two words%2B"
6946
+ ],
6947
+ "clientName": "Exämple \"CLI\" + tools/\\ 中"
6948
+ },
6949
+ "body": {
6950
+ "redirect_uris": [
6951
+ "https://app.example.com/oauth/callback",
6952
+ "http://localhost/cb?a=1&b=two words%2B"
6953
+ ],
6954
+ "client_name": "Exämple \"CLI\" + tools/\\ 中"
6955
+ }
6956
+ },
6957
+ {
6958
+ "name": "an-empty-client-name-is-left-out",
6959
+ "args": {
6960
+ "redirectUris": [
6961
+ "http://[::1]/cb"
6962
+ ],
6963
+ "clientName": ""
6964
+ },
6965
+ "body": {
6966
+ "redirect_uris": [
6967
+ "http://[::1]/cb"
6968
+ ]
6969
+ }
6970
+ }
6971
+ ]
6972
+ },
6973
+ "refusals": {
6974
+ "why": "Refused before anything is sent, the way the SDK refuses a required value given empty in the code flow's methods: no request reaches the server. Only emptiness is checked here; which URIs are acceptable is the server's to say (invalid_redirect_uri).",
6975
+ "cases": [
6976
+ {
6977
+ "name": "no-redirect-uris",
6978
+ "args": {
6979
+ "redirectUris": []
6980
+ }
6981
+ },
6982
+ {
6983
+ "name": "an-empty-redirect-uri",
6984
+ "args": {
6985
+ "redirectUris": [
6986
+ "http://127.0.0.1:8765/callback",
6987
+ ""
6988
+ ]
6989
+ }
6990
+ }
6991
+ ]
6992
+ },
6993
+ "responses": {
6994
+ "why": "Decoding a 2xx, as the other responses sections do: every listed member present with that value, every absent one the language's absence. client_id_issued_at is Unix seconds, so a 32-bit integer holds it only until 2038.",
6995
+ "cases": [
6996
+ {
6997
+ "name": "as-served",
6998
+ "status": 201,
6999
+ "body": {
7000
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7001
+ "client_id_issued_at": 1792304178,
7002
+ "client_name": "Example CLI",
7003
+ "redirect_uris": [
7004
+ "http://127.0.0.1:8765/callback"
7005
+ ],
7006
+ "grant_types": [
7007
+ "authorization_code",
7008
+ "refresh_token"
7009
+ ],
7010
+ "response_types": [
7011
+ "code"
7012
+ ],
7013
+ "token_endpoint_auth_method": "none",
7014
+ "scope": "apikeys.use"
7015
+ },
7016
+ "expect": {
7017
+ "present": {
7018
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7019
+ "client_id_issued_at": 1792304178,
7020
+ "client_name": "Example CLI",
7021
+ "redirect_uris": [
7022
+ "http://127.0.0.1:8765/callback"
7023
+ ],
7024
+ "grant_types": [
7025
+ "authorization_code",
7026
+ "refresh_token"
7027
+ ],
7028
+ "response_types": [
7029
+ "code"
7030
+ ],
7031
+ "token_endpoint_auth_method": "none",
7032
+ "scope": "apikeys.use"
7033
+ },
7034
+ "absent": []
7035
+ }
7036
+ },
7037
+ {
7038
+ "name": "rfc-minimum",
7039
+ "status": 201,
7040
+ "body": {
7041
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7042
+ "client_id_issued_at": 1792304178,
7043
+ "client_name": "Example CLI",
7044
+ "redirect_uris": [
7045
+ "http://127.0.0.1:8765/callback"
7046
+ ],
7047
+ "token_endpoint_auth_method": "none"
7048
+ },
7049
+ "expect": {
7050
+ "present": {
7051
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7052
+ "client_id_issued_at": 1792304178,
7053
+ "client_name": "Example CLI",
7054
+ "redirect_uris": [
7055
+ "http://127.0.0.1:8765/callback"
7056
+ ],
7057
+ "token_endpoint_auth_method": "none"
7058
+ },
7059
+ "absent": [
7060
+ "grant_types",
7061
+ "response_types",
7062
+ "scope"
7063
+ ]
7064
+ }
7065
+ },
7066
+ {
7067
+ "name": "issued-after-2038",
7068
+ "status": 201,
7069
+ "body": {
7070
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7071
+ "client_id_issued_at": 4102444800,
7072
+ "client_name": "Example CLI",
7073
+ "redirect_uris": [
7074
+ "http://127.0.0.1:8765/callback"
7075
+ ],
7076
+ "token_endpoint_auth_method": "none"
7077
+ },
7078
+ "expect": {
7079
+ "present": {
7080
+ "client_id_issued_at": 4102444800
7081
+ },
7082
+ "absent": [
7083
+ "scope"
7084
+ ]
7085
+ }
7086
+ },
7087
+ {
7088
+ "name": "unknown-members-ignored",
7089
+ "status": 200,
7090
+ "body": {
7091
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7092
+ "client_id_issued_at": 1792304178,
7093
+ "client_name": "Example CLI",
7094
+ "redirect_uris": [
7095
+ "http://127.0.0.1:8765/callback"
7096
+ ],
7097
+ "grant_types": [
7098
+ "authorization_code",
7099
+ "refresh_token"
7100
+ ],
7101
+ "response_types": [
7102
+ "code"
7103
+ ],
7104
+ "token_endpoint_auth_method": "none",
7105
+ "scope": "apikeys.use",
7106
+ "client_secret_expires_at": 0,
7107
+ "software_id": "example"
7108
+ },
7109
+ "expect": {
7110
+ "present": {
7111
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7112
+ "scope": "apikeys.use"
7113
+ },
7114
+ "absent": []
7115
+ }
7116
+ }
7117
+ ]
7118
+ },
7119
+ "unreadable": {
7120
+ "why": "A 2xx register cannot read as its answer, with the client's retries at 0: the SDK's ordinary error (type client), kind server_error, carrying the status, after one request. Never a raw parser error, and never an empty registration. A member of the wrong type is unreadable at any depth, inside redirect_uris included, and an optional one too.",
7121
+ "cases": [
7122
+ {
7123
+ "name": "html-page",
7124
+ "status": 200,
7125
+ "rawBody": "<html><body>Sign in</body></html>"
7126
+ },
7127
+ {
7128
+ "name": "empty-body",
7129
+ "status": 201,
7130
+ "rawBody": ""
7131
+ },
7132
+ {
7133
+ "name": "cut-off",
7134
+ "status": 201,
7135
+ "rawBody": "{\"client_id\":\"dcr_x\",\"client_id_is"
7136
+ },
7137
+ {
7138
+ "name": "an-array",
7139
+ "status": 201,
7140
+ "body": []
7141
+ },
7142
+ {
7143
+ "name": "empty-object",
7144
+ "status": 201,
7145
+ "body": {}
7146
+ },
7147
+ {
7148
+ "name": "no-client-id",
7149
+ "status": 201,
7150
+ "body": {
7151
+ "client_id_issued_at": 1792304178,
7152
+ "client_name": "Example CLI",
7153
+ "redirect_uris": [
7154
+ "http://127.0.0.1:8765/callback"
7155
+ ],
7156
+ "token_endpoint_auth_method": "none"
7157
+ }
7158
+ },
7159
+ {
7160
+ "name": "no-client-id-issued-at",
7161
+ "status": 201,
7162
+ "body": {
7163
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7164
+ "client_name": "Example CLI",
7165
+ "redirect_uris": [
7166
+ "http://127.0.0.1:8765/callback"
7167
+ ],
7168
+ "token_endpoint_auth_method": "none"
7169
+ }
7170
+ },
7171
+ {
7172
+ "name": "no-client-name",
7173
+ "status": 201,
7174
+ "body": {
7175
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7176
+ "client_id_issued_at": 1792304178,
7177
+ "redirect_uris": [
7178
+ "http://127.0.0.1:8765/callback"
7179
+ ],
7180
+ "token_endpoint_auth_method": "none"
7181
+ }
7182
+ },
7183
+ {
7184
+ "name": "no-redirect-uris",
7185
+ "status": 201,
7186
+ "body": {
7187
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7188
+ "client_id_issued_at": 1792304178,
7189
+ "client_name": "Example CLI",
7190
+ "token_endpoint_auth_method": "none"
7191
+ }
7192
+ },
7193
+ {
7194
+ "name": "no-token-endpoint-auth-method",
7195
+ "status": 201,
7196
+ "body": {
7197
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7198
+ "client_id_issued_at": 1792304178,
7199
+ "client_name": "Example CLI",
7200
+ "redirect_uris": [
7201
+ "http://127.0.0.1:8765/callback"
7202
+ ]
7203
+ }
7204
+ },
7205
+ {
7206
+ "name": "issued-at-as-a-string",
7207
+ "status": 201,
7208
+ "body": {
7209
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7210
+ "client_id_issued_at": "1792304178",
7211
+ "client_name": "Example CLI",
7212
+ "redirect_uris": [
7213
+ "http://127.0.0.1:8765/callback"
7214
+ ],
7215
+ "token_endpoint_auth_method": "none"
7216
+ }
7217
+ },
7218
+ {
7219
+ "name": "redirect-uris-not-a-list",
7220
+ "status": 201,
7221
+ "body": {
7222
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7223
+ "client_id_issued_at": 1792304178,
7224
+ "client_name": "Example CLI",
7225
+ "redirect_uris": "http://127.0.0.1:8765/callback",
7226
+ "token_endpoint_auth_method": "none"
7227
+ }
7228
+ },
7229
+ {
7230
+ "name": "a-redirect-uri-not-a-string",
7231
+ "status": 201,
7232
+ "body": {
7233
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7234
+ "client_id_issued_at": 1792304178,
7235
+ "client_name": "Example CLI",
7236
+ "redirect_uris": [
7237
+ 1
7238
+ ],
7239
+ "token_endpoint_auth_method": "none"
7240
+ }
7241
+ },
7242
+ {
7243
+ "name": "scope-not-a-string",
7244
+ "status": 201,
7245
+ "body": {
7246
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7247
+ "client_id_issued_at": 1792304178,
7248
+ "client_name": "Example CLI",
7249
+ "redirect_uris": [
7250
+ "http://127.0.0.1:8765/callback"
7251
+ ],
7252
+ "token_endpoint_auth_method": "none",
7253
+ "scope": [
7254
+ "apikeys.use"
7255
+ ]
7256
+ }
7257
+ }
7258
+ ]
7259
+ },
7260
+ "retries": [
7261
+ {
7262
+ "name": "register-retries-a-5xx",
7263
+ "args": {
7264
+ "redirectUris": [
7265
+ "http://127.0.0.1:8765/callback"
7266
+ ]
7267
+ },
7268
+ "responses": [
7269
+ {
7270
+ "status": 503,
7271
+ "rawBody": ""
7272
+ },
7273
+ {
7274
+ "status": 201,
7275
+ "body": {
7276
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7277
+ "client_id_issued_at": 1792304178,
7278
+ "client_name": "Example CLI",
7279
+ "redirect_uris": [
7280
+ "http://127.0.0.1:8765/callback"
7281
+ ],
7282
+ "grant_types": [
7283
+ "authorization_code",
7284
+ "refresh_token"
7285
+ ],
7286
+ "response_types": [
7287
+ "code"
7288
+ ],
7289
+ "token_endpoint_auth_method": "none",
7290
+ "scope": "apikeys.use"
7291
+ }
7292
+ }
7293
+ ],
7294
+ "expect": {
7295
+ "requests": 2,
7296
+ "outcome": "ok"
7297
+ }
7298
+ },
7299
+ {
7300
+ "name": "register-retries-an-unreadable-2xx",
7301
+ "args": {
7302
+ "redirectUris": [
7303
+ "http://127.0.0.1:8765/callback"
7304
+ ]
7305
+ },
7306
+ "responses": [
7307
+ {
7308
+ "status": 200,
7309
+ "rawBody": "<html><body>Sign in</body></html>"
7310
+ },
7311
+ {
7312
+ "status": 201,
7313
+ "body": {
7314
+ "client_id": "dcr_eyJ2IjoxLCJhIjoiaW50ZXJuZXRkYXRhIiwibiI6IkV4YW1wbGUgQ0xJIn0.bm90LWEtcmVhbC1tYWMtZXhhbXBsZS1vbmx5LTAxMjM",
7315
+ "client_id_issued_at": 1792304178,
7316
+ "client_name": "Example CLI",
7317
+ "redirect_uris": [
7318
+ "http://127.0.0.1:8765/callback"
7319
+ ],
7320
+ "grant_types": [
7321
+ "authorization_code",
7322
+ "refresh_token"
7323
+ ],
7324
+ "response_types": [
7325
+ "code"
7326
+ ],
7327
+ "token_endpoint_auth_method": "none",
7328
+ "scope": "apikeys.use"
7329
+ }
7330
+ }
7331
+ ],
7332
+ "expect": {
7333
+ "requests": 2,
7334
+ "outcome": "ok"
7335
+ }
7336
+ },
7337
+ {
7338
+ "name": "register-oauth-error-is-never-retried",
7339
+ "args": {
7340
+ "redirectUris": [
7341
+ "http://127.0.0.1:8765/callback"
7342
+ ]
7343
+ },
7344
+ "responses": [
7345
+ {
7346
+ "status": 400,
7347
+ "body": {
7348
+ "error": "slow_down",
7349
+ "error_description": "too many requests; slow down and try again"
7350
+ }
7351
+ }
7352
+ ],
7353
+ "expect": {
7354
+ "requests": 1,
7355
+ "outcome": "oauth",
7356
+ "errorCode": "slow_down"
7357
+ }
7358
+ }
7359
+ ],
7360
+ "errors": [
7361
+ {
7362
+ "name": "a-refused-redirect-uri",
7363
+ "status": 400,
7364
+ "body": {
7365
+ "error": "invalid_redirect_uri",
7366
+ "error_description": "redirect_uris[0] must be https, or http on a loopback address"
7367
+ },
7368
+ "expect": {
7369
+ "type": "oauth",
7370
+ "errorCode": "invalid_redirect_uri",
7371
+ "errorDescription": "redirect_uris[0] must be https, or http on a loopback address",
7372
+ "status": 400,
7373
+ "kind": "bad_request",
7374
+ "retryable": false,
7375
+ "message": "invalid_redirect_uri: redirect_uris[0] must be https, or http on a loopback address"
7376
+ }
7377
+ },
7378
+ {
7379
+ "name": "refused-metadata",
7380
+ "status": 400,
7381
+ "body": {
7382
+ "error": "invalid_client_metadata",
7383
+ "error_description": "grant_types must include authorization_code"
7384
+ },
7385
+ "expect": {
7386
+ "type": "oauth",
7387
+ "errorCode": "invalid_client_metadata",
7388
+ "errorDescription": "grant_types must include authorization_code",
7389
+ "status": 400,
7390
+ "kind": "bad_request",
7391
+ "retryable": false,
7392
+ "message": "invalid_client_metadata: grant_types must include authorization_code"
7393
+ }
7394
+ },
7395
+ {
7396
+ "name": "a-server-without-registration",
7397
+ "status": 404,
7398
+ "rawBody": "Not Found",
7399
+ "expect": {
7400
+ "type": "client",
7401
+ "status": 404,
7402
+ "kind": "bad_request",
7403
+ "retryable": false
7404
+ }
7405
+ }
7406
+ ]
6810
7407
  }
6811
7408
  },
6812
7409
  "bogons": {