2025-07-18 13:22:25 -04:00
|
|
|
import {
|
|
|
|
JSONParse as json_parse,
|
|
|
|
JSONStringify as json_stringify,
|
|
|
|
} from "https://unpkg.com/json-with-bigint@3.4.4/json-with-bigint.js";
|
|
|
|
|
2025-07-19 00:44:12 -04:00
|
|
|
/// PKCE key generation.
|
|
|
|
export const PKCE = {
|
|
|
|
/// Create a verifier for [`PKCE::challenge`].
|
|
|
|
verifier: async (length) => {
|
|
|
|
let text = "";
|
|
|
|
const possible =
|
|
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
|
|
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
|
|
text += possible.charAt(
|
|
|
|
Math.floor(Math.random() * possible.length),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return text;
|
|
|
|
},
|
|
|
|
/// Create the challenge needed to request a user token.
|
|
|
|
challenge: async (verifier) => {
|
|
|
|
const data = new TextEncoder().encode(verifier);
|
|
|
|
const digest = await window.crypto.subtle.digest("SHA-256", data);
|
|
|
|
return btoa(
|
|
|
|
String.fromCharCode.apply(null, [...new Uint8Array(digest)]),
|
|
|
|
)
|
|
|
|
.replace(/\+/g, "-")
|
|
|
|
.replace(/\//g, "_")
|
|
|
|
.replace(/=+$/, "");
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
export default function tetratto({
|
|
|
|
host = "https://tetratto.com",
|
|
|
|
api_key = null,
|
|
|
|
app_id = 0n,
|
|
|
|
user_token = null,
|
|
|
|
user_verifier = null,
|
|
|
|
user_id = 0n,
|
|
|
|
}) {
|
|
|
|
const GRANT_URL = `${host}/auth/connections_link/app/${app_id}`;
|
|
|
|
|
2025-07-18 13:22:25 -04:00
|
|
|
function api_promise(res) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
if (res.ok) {
|
|
|
|
resolve(res.payload);
|
|
|
|
} else {
|
|
|
|
reject(res.message);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-07-19 00:44:12 -04:00
|
|
|
// app data
|
2025-07-18 13:22:25 -04:00
|
|
|
async function app() {
|
2025-07-19 00:44:12 -04:00
|
|
|
if (!api_key) {
|
|
|
|
throw Error("No API key provided.");
|
|
|
|
}
|
|
|
|
|
2025-07-18 13:22:25 -04:00
|
|
|
return api_promise(
|
|
|
|
json_parse(
|
|
|
|
await (
|
2025-07-19 00:44:12 -04:00
|
|
|
await fetch(`${host}/api/v1/app_data/app`, {
|
2025-07-18 13:22:25 -04:00
|
|
|
method: "GET",
|
|
|
|
headers: {
|
|
|
|
"Atto-Secret-Key": api_key,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
).text(),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-07-23 14:44:47 -04:00
|
|
|
async function check_ip(ip) {
|
|
|
|
if (!api_key) {
|
|
|
|
throw Error("No API key provided.");
|
|
|
|
}
|
|
|
|
|
|
|
|
return api_promise(
|
|
|
|
json_parse(
|
|
|
|
await (
|
|
|
|
await fetch(`${host}/api/v1/bans/${ip}`, {
|
|
|
|
method: "GET",
|
|
|
|
headers: {
|
|
|
|
"Atto-Secret-Key": api_key,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
).text(),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-07-18 13:22:25 -04:00
|
|
|
async function query(body) {
|
2025-07-19 00:44:12 -04:00
|
|
|
if (!api_key) {
|
|
|
|
throw Error("No API key provided.");
|
|
|
|
}
|
|
|
|
|
2025-07-18 13:22:25 -04:00
|
|
|
return api_promise(
|
|
|
|
json_parse(
|
|
|
|
await (
|
2025-07-19 00:44:12 -04:00
|
|
|
await fetch(`${host}/api/v1/app_data/query`, {
|
2025-07-18 13:22:25 -04:00
|
|
|
method: "POST",
|
|
|
|
headers: {
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
"Atto-Secret-Key": api_key,
|
|
|
|
},
|
|
|
|
body: json_stringify(body),
|
|
|
|
})
|
|
|
|
).text(),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function insert(key, value) {
|
2025-07-19 00:44:12 -04:00
|
|
|
if (!api_key) {
|
|
|
|
throw Error("No API key provided.");
|
|
|
|
}
|
|
|
|
|
2025-07-18 13:22:25 -04:00
|
|
|
return api_promise(
|
|
|
|
json_parse(
|
|
|
|
await (
|
2025-07-19 00:44:12 -04:00
|
|
|
await fetch(`${host}/api/v1/app_data`, {
|
2025-07-18 13:22:25 -04:00
|
|
|
method: "POST",
|
|
|
|
headers: {
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
"Atto-Secret-Key": api_key,
|
|
|
|
},
|
|
|
|
body: json_stringify({
|
|
|
|
key,
|
|
|
|
value,
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
).text(),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-07-19 15:31:06 -04:00
|
|
|
async function update(id, value) {
|
|
|
|
if (!api_key) {
|
|
|
|
throw Error("No API key provided.");
|
|
|
|
}
|
|
|
|
|
|
|
|
return api_promise(
|
|
|
|
json_parse(
|
|
|
|
await (
|
|
|
|
await fetch(`${host}/api/v1/app_data/${id}/value`, {
|
|
|
|
method: "POST",
|
|
|
|
headers: {
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
"Atto-Secret-Key": api_key,
|
|
|
|
},
|
|
|
|
body: json_stringify({
|
|
|
|
value,
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
).text(),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-07-19 23:21:01 -04:00
|
|
|
async function rename(id, key) {
|
|
|
|
if (!api_key) {
|
|
|
|
throw Error("No API key provided.");
|
|
|
|
}
|
|
|
|
|
|
|
|
return api_promise(
|
|
|
|
json_parse(
|
|
|
|
await (
|
|
|
|
await fetch(`${host}/api/v1/app_data/${id}/key`, {
|
|
|
|
method: "POST",
|
|
|
|
headers: {
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
"Atto-Secret-Key": api_key,
|
|
|
|
},
|
|
|
|
body: json_stringify({
|
|
|
|
key,
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
).text(),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-07-18 13:22:25 -04:00
|
|
|
async function remove(id) {
|
2025-07-19 00:44:12 -04:00
|
|
|
if (!api_key) {
|
|
|
|
throw Error("No API key provided.");
|
|
|
|
}
|
|
|
|
|
2025-07-18 13:22:25 -04:00
|
|
|
return api_promise(
|
|
|
|
json_parse(
|
|
|
|
await (
|
2025-07-19 00:44:12 -04:00
|
|
|
await fetch(`${host}/api/v1/app_data/${id}`, {
|
2025-07-18 13:22:25 -04:00
|
|
|
method: "DELETE",
|
|
|
|
headers: {
|
|
|
|
"Atto-Secret-Key": api_key,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
).text(),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function remove_query(body) {
|
2025-07-19 00:44:12 -04:00
|
|
|
if (!api_key) {
|
|
|
|
throw Error("No API key provided.");
|
|
|
|
}
|
|
|
|
|
2025-07-18 13:22:25 -04:00
|
|
|
return api_promise(
|
|
|
|
json_parse(
|
|
|
|
await (
|
2025-07-19 00:44:12 -04:00
|
|
|
await fetch(`${host}/api/v1/app_data/query`, {
|
2025-07-18 13:22:25 -04:00
|
|
|
method: "DELETE",
|
|
|
|
headers: {
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
"Atto-Secret-Key": api_key,
|
|
|
|
},
|
|
|
|
body: json_stringify(body),
|
|
|
|
})
|
|
|
|
).text(),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-07-19 00:44:12 -04:00
|
|
|
// user connection
|
|
|
|
/// Extract the verifier, token, and user ID from the URL.
|
|
|
|
function extract_verifier_token_uid() {
|
|
|
|
const search = new URLSearchParams(window.location.search);
|
|
|
|
return [
|
|
|
|
search.get("verifier"),
|
|
|
|
search.get("token"),
|
|
|
|
BigInt(search.get("uid")),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Accept a connection grant and store it in localStorage.
|
|
|
|
function localstorage_accept_connection() {
|
|
|
|
const [verifier, token, uid] = extract_verifier_token_uid();
|
|
|
|
window.localStorage.setItem("atto:grant.verifier", verifier);
|
|
|
|
window.localStorage.setItem("atto:grant.token", token);
|
|
|
|
window.localStorage.setItem("atto:grant.user_id", uid);
|
|
|
|
}
|
|
|
|
|
2025-07-19 02:00:04 -04:00
|
|
|
async function refresh_token() {
|
2025-07-19 00:44:12 -04:00
|
|
|
return api_promise(
|
|
|
|
json_parse(
|
|
|
|
await (
|
|
|
|
await fetch(
|
|
|
|
`${host}/api/v1/auth/user/${user_id}/grants/${app_id}/refresh`,
|
|
|
|
{
|
|
|
|
method,
|
|
|
|
headers: {
|
|
|
|
"Content-Type": "application/json",
|
2025-07-19 02:00:04 -04:00
|
|
|
"X-Cookie": `Atto-Grant=${user_token}`,
|
2025-07-19 00:44:12 -04:00
|
|
|
},
|
|
|
|
body: json_stringify({
|
2025-07-19 02:00:04 -04:00
|
|
|
verifier: user_verifier,
|
2025-07-19 00:44:12 -04:00
|
|
|
}),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
).text(),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function request({
|
2025-07-19 02:00:04 -04:00
|
|
|
route,
|
2025-07-19 00:44:12 -04:00
|
|
|
method = "POST",
|
|
|
|
content_type = "application/json",
|
2025-07-19 02:00:04 -04:00
|
|
|
body = {},
|
2025-07-19 00:44:12 -04:00
|
|
|
}) {
|
|
|
|
if (!user_token) {
|
|
|
|
throw Error("No user token provided.");
|
|
|
|
}
|
|
|
|
|
|
|
|
return api_promise(
|
|
|
|
json_parse(
|
|
|
|
await (
|
2025-07-19 02:00:04 -04:00
|
|
|
await fetch(`${host}/api/v1/${route}`, {
|
2025-07-19 00:44:12 -04:00
|
|
|
method,
|
|
|
|
headers: {
|
2025-07-19 02:00:04 -04:00
|
|
|
"Content-Type":
|
|
|
|
method === "GET" ? null : content_type,
|
|
|
|
"X-Cookie": `Atto-Grant=${user_token}`,
|
2025-07-19 00:44:12 -04:00
|
|
|
},
|
|
|
|
body:
|
2025-07-19 02:00:04 -04:00
|
|
|
method === "GET"
|
|
|
|
? null
|
|
|
|
: content_type === "application/json"
|
|
|
|
? json_stringify(body)
|
|
|
|
: body,
|
2025-07-19 00:44:12 -04:00
|
|
|
})
|
|
|
|
).text(),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// ...
|
2025-07-18 13:22:25 -04:00
|
|
|
return {
|
2025-07-19 02:00:04 -04:00
|
|
|
user_id,
|
|
|
|
user_token,
|
|
|
|
user_verifier,
|
|
|
|
app_id,
|
|
|
|
api_key,
|
2025-07-19 00:44:12 -04:00
|
|
|
// app data
|
2025-07-18 13:22:25 -04:00
|
|
|
app,
|
2025-07-23 14:44:47 -04:00
|
|
|
check_ip,
|
2025-07-18 13:22:25 -04:00
|
|
|
query,
|
|
|
|
insert,
|
2025-07-19 15:31:06 -04:00
|
|
|
update,
|
2025-07-19 23:21:01 -04:00
|
|
|
rename,
|
2025-07-18 13:22:25 -04:00
|
|
|
remove,
|
|
|
|
remove_query,
|
2025-07-19 00:44:12 -04:00
|
|
|
// user connection
|
|
|
|
GRANT_URL,
|
|
|
|
extract_verifier_token_uid,
|
|
|
|
refresh_token,
|
|
|
|
localstorage_accept_connection,
|
|
|
|
request,
|
2025-07-18 13:22:25 -04:00
|
|
|
};
|
|
|
|
}
|
2025-07-19 00:44:12 -04:00
|
|
|
|
|
|
|
export function from_localstorage({
|
|
|
|
host = "https://tetratto.com",
|
|
|
|
app_id = 0n,
|
|
|
|
}) {
|
|
|
|
const user_verifier = window.localStorage.getItem("atto:grant.verifier");
|
|
|
|
const user_token = window.localStorage.getItem("atto:grant.token");
|
|
|
|
const user_id = window.localStorage.getItem("atto:grant.user_id");
|
|
|
|
|
|
|
|
return tetratto({
|
|
|
|
host,
|
|
|
|
app_id,
|
|
|
|
user_verifier,
|
|
|
|
user_id,
|
|
|
|
user_token,
|
|
|
|
});
|
|
|
|
}
|