Collection of themes/skins for the Fossil SCM

⌈⌋ ⎇ branch:  Fossil Skins Extra


Check-in [54a2eae9c2]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Add caps and mtime columns, some doc fixes.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 54a2eae9c279c043d5f2d8c25515ff00650917cd
User & Date: mario 2021-04-10 22:45:09
Context
2021-04-10
22:45
charset= instead of encoding= check-in: d059f0486e user: mario tags: trunk
22:45
Add caps and mtime columns, some doc fixes. check-in: 54a2eae9c2 user: mario tags: trunk
2021-04-09
04:34
micropub: expanded properties declaration check-in: 2a5697b856 user: mario tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to extroot/auth.

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

48

49
50
51
52
53
54
55
#
# ## SETUP
#
# So this requires installation in fossils extroot: (internal cgi scripts),
# and might not work in chroot jails (due to php-cgi shebang). Copy the script
# into ext/, preferrably without .php or .cgi extension, and chmod +x it.
#
# If invoked without any parameters, this fossil-cgi script will initialize
# the `fx_auth` database table.
#
# User accounts are required to list homepages in the `info` column. Each
# needs at least one, so requests can't be used to approve arbitrary urls.
#
# Afterwards configure your homepage to allow its use as IndieAuth id:
# <link rel=authorization_endpoint href="http://fossil.domain/ext/auth.cgi">
#
#
# ## TOKEN
#
# The fx_auth table contains individual columns now to record previous
# authorization requests.
# Most of which is unnecessary to keep after the initial request. This is
# largely for debugging. If /token support gets implemented, it might either
# create distinct code entries, or just add "scope": etc. to the existing

# entries - and reuse the auth keys as OAuth Bearer token.

#
#
# ## PROTOCOL
#
# Authorization request:
#    ?me=https://user.example.org/
#    &client_id=http://app.example.com/







<
<
<
|
|


|





|
|
<
<
>
|
>







24
25
26
27
28
29
30



31
32
33
34
35
36
37
38
39
40
41
42


43
44
45
46
47
48
49
50
51
52
#
# ## SETUP
#
# So this requires installation in fossils extroot: (internal cgi scripts),
# and might not work in chroot jails (due to php-cgi shebang). Copy the script
# into ext/, preferrably without .php or .cgi extension, and chmod +x it.
#



# User accounts are required to list an url in `homepages` or `info` column.
# Each needs at least one, so requests can't be used to impersonate.
#
# Afterwards configure your homepage to allow its use as IndieAuth id:
# <link rel=authorization_endpoint href="http://fossil.domain/ext/auth">
#
#
# ## TOKEN
#
# The fx_auth table contains individual columns now to record previous
# authorization requests. Most of which is unnecessary to keep after the
# initial request. This is largely for debugging.


#
# The /token endpoint will use the same entries however, and upgrade from
# `code` to `token` on request.  Which the /micropub handler then verifies.
#
#
# ## PROTOCOL
#
# Authorization request:
#    ?me=https://user.example.org/
#    &client_id=http://app.example.com/
66
67
68
69
70
71
72

73
74
75
76
77
78
79
80
81
# Response:
#    Location: https://app.example.com/login/callback?code=...&state=1234567890
#
# Verification request:
#    ?code=$1y$.....
#    &client_id=http://app.example.com/
#    &redirect_uri=http://app.example.com/login/callback

# Response:
#    { "me": "https://user.example.org/" }
#
#

if ($_REQUEST["dbg"]) {
    error_reporting(E_ALL); ini_set("display_errors", 1);
}








>

|







63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# Response:
#    Location: https://app.example.com/login/callback?code=...&state=1234567890
#
# Verification request:
#    ?code=$1y$.....
#    &client_id=http://app.example.com/
#    &redirect_uri=http://app.example.com/login/callback
#    &code_verifier=raw-pre-sha256-cnt
# Response:
#    { "me": "https://user.example.org/", "scope": "...", "access_token": "..." }
#
#

if ($_REQUEST["dbg"]) {
    error_reporting(E_ALL); ini_set("display_errors", 1);
}

96
97
98
99
100
101
102
103

104
105
106
107
108
109
110

111
112
113
114
115
116
117
118

























119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
        return $db->query($sql);
    }
}
function create_table() {
    db("CREATE TABLE IF NOT EXISTS `fx_auth` (  -- IndieAuth token table
        `code` TEXT,            -- OAuth authorization code
        `type` TEXT,            -- one of id,code,token,revoked
        `scope` TEXT,           -- permissions (create,update,delete)

        `login` TEXT,           -- fossil user account
        `me` TEXT,              -- https://userwebid.example.org/
        `client_id` TEXT,       -- https://remoteapp.example.com/
        `redirect_uri` TEXT,    -- https://app/login/callback
        `state` TEXT,           -- remote session id (12345..)
        `code_challenge` TEXT,  -- pre-hashed secret for later token req
        `code_challenge_m` TEXT,-- hash method for secret

        `expires` INT           -- code valid until timestamp
    )");
}

#-- fossil HTML output
function page_html($html) {
    header("Content-Type: text/html; encoding=utf-8");
    $svg = <<<SVG

























     <svg height=270 width=215 style='float:left; margin-right: 30pt;' viewBox='0 0 42.967861 53.77858'> <g transform='translate(-26.926707,-72.244048)' id='layer1'>
       <path id='path828' d='m 58.667032,73.126526 c -6.35947,0.04444 -12.71895,0.08888 -19.078418,0.133327 -3.853683,17.29335 -7.707367,34.586687 -11.56105,51.880037 4.67086,-10e-4 9.341719,-0.002 14.012578,-0.003 0.17811,-6.32623 0.35623,-12.65246 0.53434,-18.97869 2.04552,-0.85886 4.09104,-1.71773 6.13657,-2.57659 2.13889,0.8959 4.27777,1.79179 6.41666,2.68769 -0.10594,5.96161 0.64059,11.93241 0.17825,17.88368 -0.82143,1.27915 1.29889,0.50748 2.05533,0.71827 3.82023,0.002 7.64045,0.005 11.46067,0.007 -3.38498,-17.25073 -6.76995,-34.501457 -10.15493,-51.752194 z m -9.48934,7.518922 c 4.76639,-0.180988 8.59732,5.026339 7.02166,9.521985 -1.23655,4.60395 -7.34109,6.7331 -11.17174,3.89414 -4.034474,-2.54196 -4.263284,-9.002574 -0.41875,-11.82358 1.28595,-1.025868 2.92405,-1.596645 4.56883,-1.592545 z m 5.68285,44.224172 c 0.32724,0 0.0986,0 0,0 z'
       style='fill:#aeea47;fill-opacity:1;stroke:#4a5848;stroke-width:1.76499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.99456518' />
     </g></svg>
SVG;
    print("<div class='fossil-doc' data-title='IndieAuth'>\n$svg\n$html\n</div>");
}
function missing_param($name) {
    die(page_html("<h2>Missing input</h2><p>URL lacks <code>&$name=</code> parameter.<p>Can't process as IndieAuth/OAuth request."));
}
function page_md($text) {
    header("Content-Type: text/x-markdown; encoding=utf-8");
    print($text);
}
function h($s) {
    return htmlspecialchars($s, ENT_QUOTES|ENT_HTML5, "UTF-8");
}









|
>







>






|

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|


|







|







94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
        return $db->query($sql);
    }
}
function create_table() {
    db("CREATE TABLE IF NOT EXISTS `fx_auth` (  -- IndieAuth token table
        `code` TEXT,            -- OAuth authorization code
        `type` TEXT,            -- one of id,code,token,revoked
        `scope` TEXT,           -- requested permissions (create,update,delete)
        `caps` TEXT,            -- fossil permissions (askmnw3C)
        `login` TEXT,           -- fossil user account
        `me` TEXT,              -- https://userwebid.example.org/
        `client_id` TEXT,       -- https://remoteapp.example.com/
        `redirect_uri` TEXT,    -- https://app/login/callback
        `state` TEXT,           -- remote session id (12345..)
        `code_challenge` TEXT,  -- pre-hashed secret for later token req
        `code_challenge_m` TEXT,-- hash method for secret
        `mtime` INT,            -- token entry last modified
        `expires` INT           -- code valid until timestamp
    )");
}

#-- fossil HTML output
function page_html($html) {
    header("Content-Type: text/html; charset=utf-8");
    $svg = <<<SVG
<svg style='float:left; margin-right: 30pt;' width="280" height="268" version="1.1" viewBox="0 0 56.048 53.779" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 <defs>
  <linearGradient id="linearGradient2066" x1="63.409" x2="36.085" y1="109.16" y2="82.295" gradientTransform="matrix(3.7795 0 0 3.7795 -57.385 -274.06)" gradientUnits="userSpaceOnUse">
   <stop stop-color="#7aba0f" offset="0"/>
   <stop stop-color="#bfe47f" offset="1"/>
  </linearGradient>
  <linearGradient id="linearGradient1185" x1="52.613" x2="47.211" y1="84.095" y2="93.583" gradientUnits="userSpaceOnUse">
   <stop stop-color="#fff" stop-opacity=".9" offset="0"/>
   <stop stop-color="#f9faf9" stop-opacity=".5" offset="1"/>
  </linearGradient>
  <filter id="filter1275" x="-.024063" y="-.023937" width="1.0481" height="1.0479" color-interpolation-filters="sRGB">
   <feGaussianBlur stdDeviation="0.12728711"/>
  </filter>
 </defs>
 <g transform="translate(-15.183 -72.511)">
  <g transform="matrix(1.6139 0 0 1.5192 51.943 -52.074)">
   <path d="m7.8389 85.485-3.8743 6.0476c-23.226-3.1157-33.318 9.3314-8.599 22.962-24.906-4.8059-27.229-30.998 12.473-29.01z" fill="#fe230f"/>
   <path d="m4.7884 90.276-2.5066 4.8015c-21.825-2.5224-22.803 6.515-7.0408 19.345-14.144-5.3082-27.76-25.757 9.5474-24.147z" fill="#fe5d0f"/>
   <path d="m2.399 94.78-2.1773 3.8912c-18.675-1.7607-14.894 3.7679-5.0765 15.772-7.8444-5.7192-23.168-22.385 7.2538-19.663z" fill="#fea70f"/>
  </g>
  <path transform="matrix(.26458 0 0 .26458 15.183 72.511)" d="m169.4 3.3359c-24.036 0.16796-48.072 0.33592-72.107 0.50391-9.4009 42.186-18.802 84.372-28.203 126.56 11.159 17.745 24.195 38.517 37.898 54.234 0.52981-18.818 1.06-37.635 1.5898-56.453 7.7311-3.2461 15.462-6.4922 23.193-9.7383 8.084 3.3861 16.168 6.7721 24.252 10.158-0.40041 22.532 2.4212 45.099 0.67383 67.592-3.1046 4.8346 4.9086 1.9182 7.7676 2.7148 14.439 0.00756 28.878 0.01783 43.316 0.02539-12.794-65.199-25.587-130.4-38.381-195.6zm-34.188 28.412c17.222 0.54857 30.629 19.534 24.859 35.994-4.6736 17.401-27.745 25.447-42.223 14.717-15.248-9.6074-16.115-34.025-1.584-44.688 4.8603-3.8773 11.053-6.0331 17.27-6.0176 0.56296-0.021376 1.1222-0.023555 1.6777-0.005859zm-75.895 142.51c-1.9066 8.556-3.8141 17.112-5.7207 25.668l52.961-0.01172c0.11707-4.1582 0.23449-8.3164 0.35157-12.475-15.189-3.0869-32.389-7.5965-47.592-13.182zm95.695 24.646c1.2368 0 0.37266 0 0 0z" fill="url(#linearGradient2066)" stroke="#4a5848" stroke-width="6.6709"/>
  <ellipse cx="50.446" cy="88.237" rx="6.3477" ry="6.3811" fill="url(#linearGradient1185)" filter="url(#filter1275)" opacity=".9"/>
 </g>
</svg>

     <!--svg height=270 width=215 style='float:left; margin-right: 30pt;' viewBox='0 0 42.967861 53.77858'> <g transform='translate(-26.926707,-72.244048)' id='layer1'>
       <path id='path828' d='m 58.667032,73.126526 c -6.35947,0.04444 -12.71895,0.08888 -19.078418,0.133327 -3.853683,17.29335 -7.707367,34.586687 -11.56105,51.880037 4.67086,-10e-4 9.341719,-0.002 14.012578,-0.003 0.17811,-6.32623 0.35623,-12.65246 0.53434,-18.97869 2.04552,-0.85886 4.09104,-1.71773 6.13657,-2.57659 2.13889,0.8959 4.27777,1.79179 6.41666,2.68769 -0.10594,5.96161 0.64059,11.93241 0.17825,17.88368 -0.82143,1.27915 1.29889,0.50748 2.05533,0.71827 3.82023,0.002 7.64045,0.005 11.46067,0.007 -3.38498,-17.25073 -6.76995,-34.501457 -10.15493,-51.752194 z m -9.48934,7.518922 c 4.76639,-0.180988 8.59732,5.026339 7.02166,9.521985 -1.23655,4.60395 -7.34109,6.7331 -11.17174,3.89414 -4.034474,-2.54196 -4.263284,-9.002574 -0.41875,-11.82358 1.28595,-1.025868 2.92405,-1.596645 4.56883,-1.592545 z m 5.68285,44.224172 c 0.32724,0 0.0986,0 0,0 z'
       style='fill:#aeea47;fill-opacity:1;stroke:#4a5848;stroke-width:1.76499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.99456518' />
     </g></svg-->
SVG;
    print("<div class='fossil-doc' data-title='IndieAuth'>\n$svg\n$html\n</div>");
}
function missing_param($name) {
    die(page_html("<h2>Missing input</h2><p>URL lacks <code>&$name=</code> parameter.<p>Can't process as IndieAuth/OAuth request."));
}
function page_md($text) {
    header("Content-Type: text/x-markdown; charset=utf-8");
    print($text);
}
function h($s) {
    return htmlspecialchars($s, ENT_QUOTES|ENT_HTML5, "UTF-8");
}


186
187
188
189
190
191
192

193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213

214
215
216
217
218
219
220
221
}

#-- initial authorization request
function process_request() {

    # input params
    $user = $_SERVER["FOSSIL_USER"];

    $secret = $_SERVER["FOSSIL_NONCE"];
    $me = $_REQUEST["me"] or missing_param("me");
    $client_id = $_REQUEST["client_id"] or missing_param("client_id");
    $redirect_uri = $_REQUEST["redirect_uri"] or missing_param("redirect_uri");
    $state = $_REQUEST["state"] ?: "";
    $code_challenge = $_REQUEST["code_challenge"] ?: "";
    $code_challenge_m = $_REQUEST["code_challenge_method"] ?: "S256";
    $response_type = $_REQUEST["response_type"] ?: "id";
    $h = "h";

    # check if $me is allowed
    if (!allowed_identity($user, $me, $may_autologin)) {
        return page_html("<h2>Invalid identity</h2> User doesn't have '{$h($me)}' reserved in user table.");
    }
    
    # new code // hashing the properties is a bit overkill, any random id would suffice
    $code = password_hash("$me/$client_id/$state/$secret", PASSWORD_DEFAULT);
    db("
        INSERT INTO fx_auth
        (`code`, `type`, `login`, `me`, `client_id`, `redirect_uri`, `state`, `code_challenge`, `code_challenge_m`, `expires`)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",

        [$code, $response_type, $user, $me, $client_id, $redirect_uri, $state, $code_challenge, $code_challenge_m, time()+300]
    );

    # construct confirmation+redirect url
    $url= $redirect_uri
        . (strstr($redirect_uri, "?") ? "&" : "?")
        . "code=" . urlencode($code) . "&state=" . urlencode($state);
    







>



















|
|
>
|







211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
}

#-- initial authorization request
function process_request() {

    # input params
    $user = $_SERVER["FOSSIL_USER"];
    $caps = preg_replace("/[^abcfhikkmnsw3C]/", "", $_SERVER["FOSSIL_CAPABILITIES"]);
    $secret = $_SERVER["FOSSIL_NONCE"];
    $me = $_REQUEST["me"] or missing_param("me");
    $client_id = $_REQUEST["client_id"] or missing_param("client_id");
    $redirect_uri = $_REQUEST["redirect_uri"] or missing_param("redirect_uri");
    $state = $_REQUEST["state"] ?: "";
    $code_challenge = $_REQUEST["code_challenge"] ?: "";
    $code_challenge_m = $_REQUEST["code_challenge_method"] ?: "S256";
    $response_type = $_REQUEST["response_type"] ?: "id";
    $h = "h";

    # check if $me is allowed
    if (!allowed_identity($user, $me, $may_autologin)) {
        return page_html("<h2>Invalid identity</h2> User doesn't have '{$h($me)}' reserved in user table.");
    }
    
    # new code // hashing the properties is a bit overkill, any random id would suffice
    $code = password_hash("$me/$client_id/$state/$secret", PASSWORD_DEFAULT);
    db("
        INSERT INTO fx_auth
        (`code`,  `type`,  `caps`, `login`, `me`, `client_id`, `redirect_uri`, `state`, `code_challenge`, `code_challenge_m`, `mtime`, `expires`)
        VALUES
        ( ?,       ?,          ?,      ?,     ?,    ?,          ?,              ?,       ?,                ?,                  ?,       ?)",
        [$code, $response_type, $caps, $user, $me, $client_id, $redirect_uri, $state, $code_challenge, $code_challenge_m, time(), time()+300]
    );

    # construct confirmation+redirect url
    $url= $redirect_uri
        . (strstr($redirect_uri, "?") ? "&" : "?")
        . "code=" . urlencode($code) . "&state=" . urlencode($state);
    
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315

316
317
318
319
320
321
322
#-- run
if (empty($_POST["redirect_target"]) and !empty($_REQUEST["code"])) {  # ?code=… when the remote app verifies the response
    verify_code();
}
elseif (empty($_SERVER["FOSSIL_USER"])) {   # user must be signed in at this point
    page_html("<h2>Not logged in</h2>\n Request can't be authorized, unless you're <a href='../login'>logged in</a>.");
}
https://fossil.include-once.org/fossil-skins/ext/auth?client_id=https%3A%2F%2Ftokens-pls.herokuapp.com&code_challenge=5l8nYg8xcgwcEg2TN0SWeSJgjOejWZqu2CvtBnNjgcE&code_challenge_method=S256&nonce=4f4f480199de98745a0e6cebda898a34&redirect_uri=https%3A%2F%2Ftokens-pls.herokuapp.com%2Fcallback&response_type=code&scope=draft&state=aa823b9cef0ae758b6bf61a67dd464ca987770338eb7e1160d945c874efdf3b8
elseif (!empty($_REQUEST["me"])) {   # ?me=… starts an authorization request
    process_request();
}
elseif (!empty($_POST["confirm"])) {   # ?redirect_target=…  for confirmation button
    confirm();
}
else {

    page_html("
       <h3>Authorization endpoint</h3>
       There was no ?code= or ?me= parameter,<br> so not an actual Indie/OAuth request.
       <ul>
         <li>The <code>fx_auth</code> table is now configured.
         <li>Users still need to register a web address in the info/homepage field. (See <a href='user_config'>ext/user_config</a>.)
         <li>And then declare this endpoint on their personal homepage:<br>







<







>







328
329
330
331
332
333
334

335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
#-- run
if (empty($_POST["redirect_target"]) and !empty($_REQUEST["code"])) {  # ?code=… when the remote app verifies the response
    verify_code();
}
elseif (empty($_SERVER["FOSSIL_USER"])) {   # user must be signed in at this point
    page_html("<h2>Not logged in</h2>\n Request can't be authorized, unless you're <a href='../login'>logged in</a>.");
}

elseif (!empty($_REQUEST["me"])) {   # ?me=… starts an authorization request
    process_request();
}
elseif (!empty($_POST["confirm"])) {   # ?redirect_target=…  for confirmation button
    confirm();
}
else {
    db("SELECT 1");
    page_html("
       <h3>Authorization endpoint</h3>
       There was no ?code= or ?me= parameter,<br> so not an actual Indie/OAuth request.
       <ul>
         <li>The <code>fx_auth</code> table is now configured.
         <li>Users still need to register a web address in the info/homepage field. (See <a href='user_config'>ext/user_config</a>.)
         <li>And then declare this endpoint on their personal homepage:<br>

Changes to extroot/token.

10
11
12
13
14
15
16

17
18
19
20
21
22
23
# state: untested
# depends: php:sqlite
# doc: https://indieweb.org/obtaining-an-access-token
# config: -
#
# Counterpart to the `auth` cgi extension. This basically just
# upgrades the authorization code to an access token internally.

#
# Request: POST .../token
#    ?grant_type=authorization_code
#    &me=https://userwebid.example.org/
#    &code=$2y...
#    &redirect_uri=http://app.example.com/login/callback
#    &client_id=https://app.example.com







>







10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# state: untested
# depends: php:sqlite
# doc: https://indieweb.org/obtaining-an-access-token
# config: -
#
# Counterpart to the `auth` cgi extension. This basically just
# upgrades the authorization code to an access token internally.
# The $2y$-hash token stays, the `fx_auth.type` becomes 'token'.
#
# Request: POST .../token
#    ?grant_type=authorization_code
#    &me=https://userwebid.example.org/
#    &code=$2y...
#    &redirect_uri=http://app.example.com/login/callback
#    &client_id=https://app.example.com
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
    elseif ($client_id != $token["client_id"]) {
        json_response(["error" =>  "access_denied", "error_description" => "wrong client_id"]);
    }
    elseif (in_array($token["type"], ["id", "revoked"]) or empty($token["scope"])) {
        json_response(["error" =>  "invalid_scope", "error_description" => "authorization code (response_type=id) not useable for access token upgrade"]);
    }
    else {
        db("UPDATE fx_auth SET `type`=?, expires=? WHERE code=?", ["token", time()+3600, $code]);
        json_response([
            "access_token" => $code, #substr($code, 7),
            "token_type" => "Bearer",
            "scope" => $token["scope"],
            "me" => $token["me"],
        ]);
    }
}

function fix_code(&$code) {
    if (strpos($code, '$2y$10$') === 0) { $code = '$2y$10$' . $code; }
}

#-- test validity of Authorization: Bearer TOKENCODE
function verify_bearer($code) {
    fix_code($code);
    # find token
    clean_expired_token();







|










|







120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
    elseif ($client_id != $token["client_id"]) {
        json_response(["error" =>  "access_denied", "error_description" => "wrong client_id"]);
    }
    elseif (in_array($token["type"], ["id", "revoked"]) or empty($token["scope"])) {
        json_response(["error" =>  "invalid_scope", "error_description" => "authorization code (response_type=id) not useable for access token upgrade"]);
    }
    else {
        db("UPDATE fx_auth SET `type`=?, mtime=?, expires=? WHERE code=?", ["token", time(), time()+3600, $code]);
        json_response([
            "access_token" => $code, #substr($code, 7),
            "token_type" => "Bearer",
            "scope" => $token["scope"],
            "me" => $token["me"],
        ]);
    }
}

function fix_code(&$code) {
    if (strpos($code, '$2y$10$') !== 0) { $code = '$2y$10$' . $code; }
}

#-- test validity of Authorization: Bearer TOKENCODE
function verify_bearer($code) {
    fix_code($code);
    # find token
    clean_expired_token();