@axium/client 0.27.0 → 0.28.0
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/assets/theme.css +1 -0
- package/dist/access.js +8 -5
- package/dist/cache.d.ts +47 -14
- package/dist/cache.js +91 -40
- package/dist/cli/auth.d.ts +2 -0
- package/dist/cli/auth.js +140 -0
- package/dist/cli/cache.d.ts +80 -0
- package/dist/cli/cache.js +155 -0
- package/dist/cli/config.d.ts +0 -3
- package/dist/cli/config.js +5 -21
- package/dist/cli/index.js +45 -151
- package/dist/cli/sync.d.ts +17 -0
- package/dist/cli/sync.js +24 -0
- package/dist/config.d.ts +0 -31
- package/dist/config.js +0 -9
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/preferences.d.ts +5 -0
- package/dist/{apps.js → preferences.js} +12 -12
- package/dist/requests.d.ts +2 -2
- package/dist/user.d.ts +4 -1
- package/dist/user.js +6 -4
- package/lib/AppPreferences.svelte +4 -3
- package/package.json +2 -2
- package/dist/apps.d.ts +0 -5
package/assets/theme.css
CHANGED
|
@@ -50,6 +50,7 @@ html {
|
|
|
50
50
|
|
|
51
51
|
--border-disabled: 1px solid hsl(0 5 calc(var(--bg-light) - var(--light-step)));
|
|
52
52
|
--border-accent: 1px solid hsl(var(--hue) 10 calc(var(--bg-light) + (var(--light-step) * 3)));
|
|
53
|
+
--border-alt: 1px solid var(--bg-alt);
|
|
53
54
|
--border-strong: 1px solid hsl(var(--hue) 20 calc(var(--bg-light) + (var(--light-step) * 3)));
|
|
54
55
|
|
|
55
56
|
--border-error: 1px solid hsl(0 50 var(--fg-light));
|
package/dist/access.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Cache from './cache.js';
|
|
2
2
|
import { fetchAPI } from './requests.js';
|
|
3
|
+
const cache = new Cache((itemType, itemId) => fetchAPI('GET', 'acl/:itemType/:itemId', {}, itemType, itemId), {
|
|
4
|
+
ttl: 3600_000,
|
|
5
|
+
});
|
|
3
6
|
export async function updateACL(itemType, itemId, target, permissions) {
|
|
4
7
|
const result = await fetchAPI('PATCH', 'acl/:itemType/:itemId', { target, permissions }, itemType, itemId);
|
|
5
|
-
cache.invalidate(
|
|
8
|
+
cache.invalidate(itemType, itemId);
|
|
6
9
|
return result;
|
|
7
10
|
}
|
|
8
11
|
export async function getACL(itemType, itemId) {
|
|
9
|
-
return await cache.
|
|
12
|
+
return await cache.get(itemType, itemId);
|
|
10
13
|
}
|
|
11
14
|
export async function addToACL(itemType, itemId, target) {
|
|
12
15
|
const result = await fetchAPI('PUT', 'acl/:itemType/:itemId', target, itemType, itemId);
|
|
13
|
-
cache.invalidate(
|
|
16
|
+
cache.invalidate(itemType, itemId);
|
|
14
17
|
return result;
|
|
15
18
|
}
|
|
16
19
|
export async function removeFromACL(itemType, itemId, target) {
|
|
17
20
|
const result = await fetchAPI('DELETE', 'acl/:itemType/:itemId', target, itemType, itemId);
|
|
18
|
-
cache.invalidate(
|
|
21
|
+
cache.invalidate(itemType, itemId);
|
|
19
22
|
return result;
|
|
20
23
|
}
|
package/dist/cache.d.ts
CHANGED
|
@@ -1,20 +1,53 @@
|
|
|
1
|
-
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
export interface CacheOptions {
|
|
2
3
|
/** Maximum number of items the cache can hold */
|
|
3
4
|
itemLimit: number;
|
|
5
|
+
/** @todo replace with `Temporal.DurationLike` */
|
|
6
|
+
ttl: number;
|
|
4
7
|
}
|
|
8
|
+
export declare const CacheData: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
9
|
+
$timestamp: z.ZodCoercedDate<unknown>;
|
|
10
|
+
}, z.core.$loose>>;
|
|
11
|
+
/** @todo replace `$timestamp` with `Temporal.InstantLike` */
|
|
12
|
+
export interface CacheData<V> extends Record<string, V & {
|
|
13
|
+
$timestamp: number;
|
|
14
|
+
}> {
|
|
15
|
+
}
|
|
16
|
+
declare const kTimestamp: unique symbol;
|
|
5
17
|
/**
|
|
6
|
-
*
|
|
7
|
-
*/
|
|
8
|
-
export declare function create(cacheName: string, options?: Partial<Options>): void;
|
|
9
|
-
/**
|
|
10
|
-
* Use a cache for some arbitrary operation.
|
|
18
|
+
* Cache some arbitrary operation.
|
|
11
19
|
* This is primarily intended for de-duplicating API requests
|
|
12
|
-
* @param cacheName The name of the cache to use, e.g. `'users'`
|
|
13
|
-
* @param key The key for the item in the cache. This can be anything used for the key of a `Map`
|
|
14
|
-
* @param miss The function to run on a cache miss
|
|
15
|
-
* @remarks
|
|
16
|
-
* Note that a cache will automatically be created if it doesn't already exist
|
|
17
20
|
*/
|
|
18
|
-
export declare
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
export declare class Cache<Keys extends string[], V extends object> {
|
|
22
|
+
/**
|
|
23
|
+
* Function to run when there is a cache miss
|
|
24
|
+
* @
|
|
25
|
+
*/
|
|
26
|
+
protected readonly miss: (...keys: Keys) => V | Promise<V>;
|
|
27
|
+
protected items: Map<string, V & {
|
|
28
|
+
[kTimestamp]: number;
|
|
29
|
+
}>;
|
|
30
|
+
protected pending: Map<string, Promise<V>>;
|
|
31
|
+
readonly options: CacheOptions;
|
|
32
|
+
private onWrite?;
|
|
33
|
+
constructor(
|
|
34
|
+
/**
|
|
35
|
+
* Function to run when there is a cache miss
|
|
36
|
+
* @
|
|
37
|
+
*/
|
|
38
|
+
miss: (...keys: Keys) => V | Promise<V>, options?: Partial<CacheOptions>);
|
|
39
|
+
protected key(keys: Keys): string;
|
|
40
|
+
get size(): number;
|
|
41
|
+
protected valid(timestamp?: number): boolean;
|
|
42
|
+
protected write(key: string, value: V | Promise<V>): void;
|
|
43
|
+
get(...keys: Keys): V | Promise<V>;
|
|
44
|
+
set(...args: [...keys: Keys, value: V | Promise<V>]): void;
|
|
45
|
+
invalidate(...keys: Keys): void;
|
|
46
|
+
persist(onWrite: (data: CacheData<V>) => void,
|
|
47
|
+
/** @todo replace `$timestamp` with `Temporal.InstantLike` */
|
|
48
|
+
existingContent?: Record<string, V & {
|
|
49
|
+
$timestamp?: number;
|
|
50
|
+
}>): void;
|
|
51
|
+
toJSON(): CacheData<V>;
|
|
52
|
+
}
|
|
53
|
+
export default Cache;
|
package/dist/cache.js
CHANGED
|
@@ -1,48 +1,99 @@
|
|
|
1
|
-
|
|
1
|
+
import * as z from 'zod';
|
|
2
2
|
const defaultCacheOptions = {
|
|
3
3
|
itemLimit: 10_000,
|
|
4
|
+
ttl: 3600_000,
|
|
4
5
|
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
_caches[cacheName] = {
|
|
12
|
-
items: new Map(),
|
|
13
|
-
...defaultCacheOptions,
|
|
14
|
-
...options,
|
|
15
|
-
};
|
|
6
|
+
export const CacheData = z.record(z.string(), z.looseObject({
|
|
7
|
+
/** @todo replace with temporal instant */
|
|
8
|
+
$timestamp: z.coerce.date(),
|
|
9
|
+
}));
|
|
10
|
+
function isThenable(value) {
|
|
11
|
+
return !!(value && typeof value == 'object' && 'then' in value && typeof value.then == 'function');
|
|
16
12
|
}
|
|
13
|
+
const kTimestamp = Symbol('kTimestamp');
|
|
17
14
|
/**
|
|
18
|
-
*
|
|
15
|
+
* Cache some arbitrary operation.
|
|
19
16
|
* This is primarily intended for de-duplicating API requests
|
|
20
|
-
* @param cacheName The name of the cache to use, e.g. `'users'`
|
|
21
|
-
* @param key The key for the item in the cache. This can be anything used for the key of a `Map`
|
|
22
|
-
* @param miss The function to run on a cache miss
|
|
23
|
-
* @remarks
|
|
24
|
-
* Note that a cache will automatically be created if it doesn't already exist
|
|
25
17
|
*/
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
18
|
+
export class Cache {
|
|
19
|
+
miss;
|
|
20
|
+
items = new Map();
|
|
21
|
+
pending = new Map();
|
|
22
|
+
options;
|
|
23
|
+
onWrite;
|
|
24
|
+
constructor(
|
|
25
|
+
/**
|
|
26
|
+
* Function to run when there is a cache miss
|
|
27
|
+
* @
|
|
28
|
+
*/
|
|
29
|
+
miss, options = {}) {
|
|
30
|
+
this.miss = miss;
|
|
31
|
+
this.options = { ...defaultCacheOptions, ...options };
|
|
32
|
+
}
|
|
33
|
+
key(keys) {
|
|
34
|
+
return keys.join(':');
|
|
35
|
+
}
|
|
36
|
+
get size() {
|
|
37
|
+
return this.items.size;
|
|
38
|
+
}
|
|
39
|
+
valid(timestamp) {
|
|
40
|
+
return !this.options.ttl || !timestamp || timestamp.valueOf() + this.options.ttl < Date.now();
|
|
41
|
+
}
|
|
42
|
+
write(key, value) {
|
|
43
|
+
if (isThenable(value)) {
|
|
44
|
+
this.pending.set(key, value);
|
|
45
|
+
void value.then(resolved => {
|
|
46
|
+
this.write(key, resolved);
|
|
47
|
+
this.pending.delete(key);
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (this.items.size >= this.options.itemLimit) {
|
|
52
|
+
const [key] = this.items.entries().next().value;
|
|
53
|
+
this.items.delete(key);
|
|
54
|
+
}
|
|
55
|
+
this.items.set(key, Object.assign(value, { [kTimestamp]: Date.now() }));
|
|
56
|
+
this.onWrite?.(this.toJSON());
|
|
57
|
+
}
|
|
58
|
+
get(...keys) {
|
|
59
|
+
const key = this.key(keys);
|
|
60
|
+
const pending = this.pending.get(key);
|
|
61
|
+
if (pending)
|
|
62
|
+
return pending;
|
|
63
|
+
const cached = this.items.get(key);
|
|
64
|
+
if (cached) {
|
|
65
|
+
if (this.valid(cached[kTimestamp]))
|
|
66
|
+
return cached;
|
|
67
|
+
this.items.delete(key);
|
|
68
|
+
}
|
|
69
|
+
const result = this.miss(...keys);
|
|
70
|
+
this.write(key, result);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
set(...args) {
|
|
74
|
+
const value = args.pop();
|
|
75
|
+
const key = this.key(args);
|
|
76
|
+
this.write(key, value);
|
|
77
|
+
}
|
|
78
|
+
invalidate(...keys) {
|
|
79
|
+
this.items.delete(this.key(keys));
|
|
80
|
+
}
|
|
81
|
+
persist(onWrite,
|
|
82
|
+
/** @todo replace `$timestamp` with `Temporal.InstantLike` */
|
|
83
|
+
existingContent) {
|
|
84
|
+
this.onWrite = onWrite;
|
|
85
|
+
if (!existingContent)
|
|
86
|
+
return;
|
|
87
|
+
for (const [key, value] of Object.entries(existingContent)) {
|
|
88
|
+
const { $timestamp = Date.now() } = value;
|
|
89
|
+
if (!this.valid($timestamp))
|
|
90
|
+
continue;
|
|
91
|
+
delete value.$timestamp;
|
|
92
|
+
this.items.set(key, Object.assign(value, { [kTimestamp]: $timestamp }));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
toJSON() {
|
|
96
|
+
return Object.fromEntries(this.items.entries().map(([k, v]) => [k, { ...v, $timestamp: v[kTimestamp] }]));
|
|
97
|
+
}
|
|
48
98
|
}
|
|
99
|
+
export default Cache;
|
package/dist/cli/auth.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
import { NewSessionResponse } from '@axium/core';
|
|
54
|
+
import * as io from 'ioium/node';
|
|
55
|
+
import { createServer } from 'node:http';
|
|
56
|
+
import { createInterface } from 'node:readline/promises';
|
|
57
|
+
import { styleText } from 'node:util';
|
|
58
|
+
import { config, resolveServerURL } from '../config.js';
|
|
59
|
+
import { prefix, setPrefix, setToken } from '../requests.js';
|
|
60
|
+
import { getCurrentSession } from '../user.js';
|
|
61
|
+
import * as cache from './cache.js';
|
|
62
|
+
import { saveConfig } from './config.js';
|
|
63
|
+
import * as os from 'node:os';
|
|
64
|
+
import $pkg from '../../package.json' with { type: 'json' };
|
|
65
|
+
export const clientUA = `Axium Client/${$pkg.version} (${os.type()}; ${process.arch})`;
|
|
66
|
+
export async function login(url) {
|
|
67
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
68
|
+
try {
|
|
69
|
+
const rl = __addDisposableResource(env_1, createInterface({
|
|
70
|
+
input: process.stdin,
|
|
71
|
+
output: process.stdout,
|
|
72
|
+
}), false);
|
|
73
|
+
rl.on('SIGINT', () => io.exit('Aborted.', 7));
|
|
74
|
+
if (prefix[0] != '/')
|
|
75
|
+
url ||= prefix;
|
|
76
|
+
url ||= await rl.question('Axium server URL: ');
|
|
77
|
+
url = resolveServerURL(url);
|
|
78
|
+
setPrefix(url);
|
|
79
|
+
const sessionReady = Promise.withResolvers();
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
81
|
+
const server = createServer(async (req, res) => {
|
|
82
|
+
res.setHeader('access-control-allow-origin', '*');
|
|
83
|
+
res.setHeader('access-control-allow-methods', '*');
|
|
84
|
+
res.setHeader('access-control-allow-headers', '*');
|
|
85
|
+
if (req.method == 'HEAD' || req.method == 'OPTIONS') {
|
|
86
|
+
res.writeHead(200).end();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!req.headers['content-type']?.endsWith('/json')) {
|
|
90
|
+
res.writeHead(415).end('Unexpected content type');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (req.method !== 'POST') {
|
|
94
|
+
res.writeHead(405).end('Unexpected request method');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const { promise: bodyReady, resolve, reject } = Promise.withResolvers();
|
|
98
|
+
let body = '';
|
|
99
|
+
req.on('data', chunk => (body += chunk.toString()));
|
|
100
|
+
req.on('end', resolve);
|
|
101
|
+
req.on('error', reject);
|
|
102
|
+
try {
|
|
103
|
+
await bodyReady;
|
|
104
|
+
res.writeHead(200).end();
|
|
105
|
+
sessionReady.resolve(NewSessionResponse.parse(JSON.parse(body)));
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
res.statusCode = 500;
|
|
109
|
+
res.end('Internal server error: ' + io.errorText(e));
|
|
110
|
+
sessionReady.reject(io.errorText(e));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
const serverReady = Promise.withResolvers();
|
|
114
|
+
server.listen(() => {
|
|
115
|
+
const { port } = server.address();
|
|
116
|
+
serverReady.resolve(port);
|
|
117
|
+
});
|
|
118
|
+
server.on('error', e => io.exit('Failed to start local callback server: ' + io.errorText(e), 5));
|
|
119
|
+
const port = await serverReady.promise;
|
|
120
|
+
const authURL = new URL(`/login/client?port=${port}&client=${encodeURIComponent(clientUA)}`, url).href;
|
|
121
|
+
console.log('Authenticate by visiting this URL in your browser: ' + styleText('underline', authURL));
|
|
122
|
+
const { token } = await sessionReady.promise.catch(e => io.exit('Failed to obtain session: ' + e, 6));
|
|
123
|
+
setToken(token);
|
|
124
|
+
server.close();
|
|
125
|
+
const session = await io.track('Verifying session', getCurrentSession().catch(e => io.exit(e, 6)));
|
|
126
|
+
io.debug('Session UUID: ' + session.id);
|
|
127
|
+
console.log(`Welcome ${session.user.name}! Your session is valid until ${session.expires.toLocaleDateString()}.`);
|
|
128
|
+
config.token = token;
|
|
129
|
+
config.server = url;
|
|
130
|
+
saveConfig();
|
|
131
|
+
await cache.update(true);
|
|
132
|
+
}
|
|
133
|
+
catch (e_1) {
|
|
134
|
+
env_1.error = e_1;
|
|
135
|
+
env_1.hasError = true;
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
__disposeResources(env_1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
export declare const dir: string;
|
|
3
|
+
declare const create: unique symbol;
|
|
4
|
+
export declare class Handle<S extends z.ZodObject> {
|
|
5
|
+
private init;
|
|
6
|
+
private _data?;
|
|
7
|
+
readonly path: string;
|
|
8
|
+
get schema(): S;
|
|
9
|
+
private constructor();
|
|
10
|
+
load(): void;
|
|
11
|
+
save(): void;
|
|
12
|
+
update(): Promise<void>;
|
|
13
|
+
isValid(): Promise<boolean>;
|
|
14
|
+
get data(): z.infer<S>;
|
|
15
|
+
static [create]<S extends z.ZodObject>(init: Init<S>): Handle<S>;
|
|
16
|
+
}
|
|
17
|
+
export interface Init<S extends z.ZodObject> {
|
|
18
|
+
path: string;
|
|
19
|
+
schema: S;
|
|
20
|
+
update(existing?: z.infer<S>): z.infer<S> | Promise<z.infer<S>>;
|
|
21
|
+
isValid(data: z.infer<S>, lastUpdatedTs: number): boolean | Promise<boolean>;
|
|
22
|
+
onLoad?(data: z.infer<S>): unknown;
|
|
23
|
+
}
|
|
24
|
+
export declare function useAt<S extends z.ZodObject>(init: Init<S>): Handle<S>;
|
|
25
|
+
export declare const meta: Handle<z.ZodObject<{
|
|
26
|
+
fetched: z.ZodInt;
|
|
27
|
+
session: z.ZodObject<{
|
|
28
|
+
id: z.ZodUUID;
|
|
29
|
+
userId: z.ZodUUID;
|
|
30
|
+
name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
31
|
+
expires: z.ZodCoercedDate<unknown>;
|
|
32
|
+
created: z.ZodCoercedDate<unknown>;
|
|
33
|
+
elevated: z.ZodBoolean;
|
|
34
|
+
user: z.ZodObject<{
|
|
35
|
+
id: z.ZodUUID;
|
|
36
|
+
name: z.ZodString;
|
|
37
|
+
email: z.ZodEmail;
|
|
38
|
+
emailVerified: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
|
|
39
|
+
preferences: z.ZodLazy<z.ZodObject<{
|
|
40
|
+
debug: z.ZodDefault<z.ZodBoolean>;
|
|
41
|
+
}, z.core.$strip>>;
|
|
42
|
+
roles: z.ZodArray<z.ZodString>;
|
|
43
|
+
tags: z.ZodArray<z.ZodString>;
|
|
44
|
+
registeredAt: z.ZodCoercedDate<unknown>;
|
|
45
|
+
isAdmin: z.ZodBoolean;
|
|
46
|
+
isSuspended: z.ZodBoolean;
|
|
47
|
+
}, z.core.$strip>;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
apps: z.ZodArray<z.ZodObject<{
|
|
50
|
+
id: z.ZodString;
|
|
51
|
+
name: z.ZodOptional<z.ZodString>;
|
|
52
|
+
image: z.ZodOptional<z.ZodString>;
|
|
53
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
54
|
+
}, z.core.$strip>>;
|
|
55
|
+
}, z.core.$loose>>;
|
|
56
|
+
export declare const sync: Handle<z.ZodObject<{
|
|
57
|
+
objects: z.ZodArray<z.ZodObject<{
|
|
58
|
+
$type: z.ZodString;
|
|
59
|
+
id: z.ZodUUID;
|
|
60
|
+
}, z.core.$loose>>;
|
|
61
|
+
index: z.ZodCoercedBigInt<unknown>;
|
|
62
|
+
}, z.core.$strip>>;
|
|
63
|
+
export declare function load(): void;
|
|
64
|
+
export declare function update(force?: boolean): Promise<void>;
|
|
65
|
+
export declare function clear(): void;
|
|
66
|
+
export interface CacheInfo {
|
|
67
|
+
path: string;
|
|
68
|
+
size?: bigint;
|
|
69
|
+
exists: boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface CacheInfoLocal extends CacheInfo {
|
|
72
|
+
valid: boolean;
|
|
73
|
+
fromAPI?: false;
|
|
74
|
+
}
|
|
75
|
+
export interface CacheInfoAPI extends CacheInfo {
|
|
76
|
+
entries: number;
|
|
77
|
+
fromAPI: true;
|
|
78
|
+
}
|
|
79
|
+
export declare function info(): AsyncGenerator<CacheInfoLocal | CacheInfoAPI>;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { App, Session, SyncState, User } from '@axium/core';
|
|
2
|
+
import * as io from 'ioium/node';
|
|
3
|
+
import { existsSync, mkdirSync, statSync, unlinkSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path/posix';
|
|
6
|
+
import * as z from 'zod';
|
|
7
|
+
import { CacheData } from '../cache.js';
|
|
8
|
+
import { fetchAPI } from '../requests.js';
|
|
9
|
+
import { apiUserCache, getCurrentSession } from '../user.js';
|
|
10
|
+
export const dir = join(process.env.XDG_CACHE_HOME || join(homedir(), '.cache'), 'axium');
|
|
11
|
+
mkdirSync(dir, { recursive: true });
|
|
12
|
+
const create = Symbol('Handle::new');
|
|
13
|
+
export class Handle {
|
|
14
|
+
init;
|
|
15
|
+
_data;
|
|
16
|
+
path;
|
|
17
|
+
get schema() {
|
|
18
|
+
return this.init.schema;
|
|
19
|
+
}
|
|
20
|
+
constructor(init) {
|
|
21
|
+
this.init = init;
|
|
22
|
+
this.path = resolve(dir, init.path);
|
|
23
|
+
}
|
|
24
|
+
load() {
|
|
25
|
+
if (!existsSync(this.path)) {
|
|
26
|
+
io.debug('Ignoring missing cache file:', this.path);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
this._data = io.readJSON(this.path, this.schema);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
io.warn(`Failed to load cache from '${this.path}':\n${e}`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.init.onLoad?.(this._data);
|
|
37
|
+
}
|
|
38
|
+
save() {
|
|
39
|
+
if (!this._data)
|
|
40
|
+
throw new ReferenceError('Cache data is not loaded');
|
|
41
|
+
try {
|
|
42
|
+
io.writeJSON(this.path, this._data);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
io.error(e);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async update() {
|
|
49
|
+
this._data = await this.init.update(this._data);
|
|
50
|
+
this.init.onLoad?.(this._data);
|
|
51
|
+
this.save();
|
|
52
|
+
}
|
|
53
|
+
async isValid() {
|
|
54
|
+
if (!this._data)
|
|
55
|
+
return false;
|
|
56
|
+
return this.init.isValid(this._data, statSync(this.path).mtimeMs);
|
|
57
|
+
}
|
|
58
|
+
get data() {
|
|
59
|
+
return this._data;
|
|
60
|
+
}
|
|
61
|
+
static [create](init) {
|
|
62
|
+
return new Handle(init);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const handles = [];
|
|
66
|
+
export function useAt(init) {
|
|
67
|
+
const handle = Handle[create](init);
|
|
68
|
+
mkdirSync(dirname(handle.path), { recursive: true });
|
|
69
|
+
handles.push(handle);
|
|
70
|
+
return handle;
|
|
71
|
+
}
|
|
72
|
+
const _dayMs = 24 * 3600_000;
|
|
73
|
+
export const meta = useAt({
|
|
74
|
+
path: 'meta.json',
|
|
75
|
+
schema: z.looseObject({
|
|
76
|
+
fetched: z.int(),
|
|
77
|
+
session: Session.extend({ user: User }),
|
|
78
|
+
apps: App.array(),
|
|
79
|
+
}),
|
|
80
|
+
async update() {
|
|
81
|
+
const [session, apps] = await io.track('Fetching metadata', Promise.all([getCurrentSession(), fetchAPI('GET', 'apps')]));
|
|
82
|
+
return { fetched: Date.now(), session, apps };
|
|
83
|
+
},
|
|
84
|
+
isValid: meta => meta.fetched + _dayMs > Date.now(),
|
|
85
|
+
});
|
|
86
|
+
export const sync = useAt({
|
|
87
|
+
path: 'sync.json',
|
|
88
|
+
schema: SyncState,
|
|
89
|
+
async update(sync) {
|
|
90
|
+
if (!sync)
|
|
91
|
+
return await fetchAPI('GET', 'sync/init');
|
|
92
|
+
const diff = await fetchAPI('GET', 'sync', { since: sync.index });
|
|
93
|
+
const deleted = new Set(diff.deleted);
|
|
94
|
+
const objects = sync.objects.filter(o => !deleted.has(o.id));
|
|
95
|
+
const existing = Object.fromEntries(objects.map(o => [o.id, o]));
|
|
96
|
+
for (const obj of diff.created)
|
|
97
|
+
objects.push(obj);
|
|
98
|
+
for (const updated of diff.updated) {
|
|
99
|
+
const base = existing[updated.id];
|
|
100
|
+
if (!base)
|
|
101
|
+
throw new ReferenceError("Can not update object because it isn't cached");
|
|
102
|
+
if (base.$type !== updated.$type)
|
|
103
|
+
throw new ReferenceError(`Type mismatch whilst updating cache object: currently ${base.$type}, incoming ${updated.$type}`);
|
|
104
|
+
Object.assign(base, updated);
|
|
105
|
+
}
|
|
106
|
+
return { objects, index: diff.index };
|
|
107
|
+
},
|
|
108
|
+
async isValid({ index }) {
|
|
109
|
+
const md = await fetchAPI('GET', 'sync/metadata');
|
|
110
|
+
return md.index >= index;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
const persistedAPICaches = [];
|
|
114
|
+
function persistAPI(cache, path) {
|
|
115
|
+
path = resolve(dir, path);
|
|
116
|
+
let data;
|
|
117
|
+
try {
|
|
118
|
+
data = io.readJSON(path, CacheData);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// missing
|
|
122
|
+
}
|
|
123
|
+
cache.persist(data => io.writeJSON(path, data), data);
|
|
124
|
+
persistedAPICaches.push({ path, cache });
|
|
125
|
+
}
|
|
126
|
+
export function load() {
|
|
127
|
+
for (const handle of handles)
|
|
128
|
+
handle.load();
|
|
129
|
+
persistAPI(apiUserCache, 'users.json');
|
|
130
|
+
}
|
|
131
|
+
export async function update(force = false) {
|
|
132
|
+
for (const handle of handles) {
|
|
133
|
+
if (force || !(await handle.isValid()))
|
|
134
|
+
await handle.update();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export function clear() {
|
|
138
|
+
for (const handle of handles)
|
|
139
|
+
unlinkSync(handle.path);
|
|
140
|
+
for (const { path } of persistedAPICaches)
|
|
141
|
+
unlinkSync(path);
|
|
142
|
+
}
|
|
143
|
+
export async function* info() {
|
|
144
|
+
for (const handle of handles) {
|
|
145
|
+
const { path } = handle;
|
|
146
|
+
const exists = existsSync(path);
|
|
147
|
+
const { size } = exists ? statSync(path, { bigint: true }) : {};
|
|
148
|
+
yield { path, exists, size, valid: await handle.isValid() };
|
|
149
|
+
}
|
|
150
|
+
for (const { path, cache } of persistedAPICaches) {
|
|
151
|
+
const exists = existsSync(path);
|
|
152
|
+
const { size } = exists ? statSync(path, { bigint: true }) : {};
|
|
153
|
+
yield { path, exists, size, entries: cache.size, fromAPI: true };
|
|
154
|
+
}
|
|
155
|
+
}
|
package/dist/cli/config.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export declare const configDir: string;
|
|
2
|
-
export declare const cacheDir: string;
|
|
3
2
|
export declare function session(): {
|
|
4
3
|
id: string;
|
|
5
4
|
userId: string;
|
|
@@ -24,5 +23,3 @@ export declare function session(): {
|
|
|
24
23
|
};
|
|
25
24
|
export declare function loadConfig(safe: boolean): Promise<void>;
|
|
26
25
|
export declare function saveConfig(): void;
|
|
27
|
-
export declare const _dayMs: number;
|
|
28
|
-
export declare function updateCache(force: boolean): Promise<void>;
|
package/dist/cli/config.js
CHANGED
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
import * as io from 'ioium/node';
|
|
2
1
|
import { loadPlugin } from '@axium/core/node/plugins';
|
|
2
|
+
import * as io from 'ioium/node';
|
|
3
3
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path/posix';
|
|
6
6
|
import { ClientConfig, config } from '../config.js';
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
7
|
+
import { setPrefix, setToken } from '../requests.js';
|
|
8
|
+
import * as cache from './cache.js';
|
|
9
9
|
export const configDir = join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'axium');
|
|
10
10
|
mkdirSync(configDir, { recursive: true });
|
|
11
11
|
const axcConfig = join(configDir, 'config.json');
|
|
12
12
|
if (!existsSync(axcConfig))
|
|
13
13
|
writeFileSync(axcConfig, '{}');
|
|
14
|
-
export const cacheDir = join(process.env.XDG_CACHE_HOME || join(homedir(), '.cache'), 'axium');
|
|
15
|
-
mkdirSync(cacheDir, { recursive: true });
|
|
16
14
|
export function session() {
|
|
17
15
|
if (!config.token)
|
|
18
16
|
io.exit('Not logged in.', 4);
|
|
19
|
-
if (!
|
|
17
|
+
if (!cache.meta.data)
|
|
20
18
|
io.exit('No session data available.', 3);
|
|
21
|
-
return
|
|
19
|
+
return cache.meta.data.session;
|
|
22
20
|
}
|
|
23
21
|
export async function loadConfig(safe) {
|
|
24
22
|
try {
|
|
@@ -38,17 +36,3 @@ export function saveConfig() {
|
|
|
38
36
|
io.writeJSON(axcConfig, config);
|
|
39
37
|
io.debug('Saved config to ' + axcConfig);
|
|
40
38
|
}
|
|
41
|
-
export const _dayMs = 24 * 3600_000;
|
|
42
|
-
export async function updateCache(force) {
|
|
43
|
-
if (!force && config.cache && config.cache.fetched + _dayMs > Date.now())
|
|
44
|
-
return;
|
|
45
|
-
const [session, apps] = await io.track('Fetching metadata', Promise.all([getCurrentSession(), fetchAPI('GET', 'apps')]));
|
|
46
|
-
config.cache = { fetched: Date.now(), session, apps };
|
|
47
|
-
try {
|
|
48
|
-
io.writeJSON(axcConfig, config);
|
|
49
|
-
}
|
|
50
|
-
catch (e) {
|
|
51
|
-
io.error(e);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,77 +1,26 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
|
-
|
|
3
|
-
if (value !== null && value !== void 0) {
|
|
4
|
-
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
5
|
-
var dispose, inner;
|
|
6
|
-
if (async) {
|
|
7
|
-
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
8
|
-
dispose = value[Symbol.asyncDispose];
|
|
9
|
-
}
|
|
10
|
-
if (dispose === void 0) {
|
|
11
|
-
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
12
|
-
dispose = value[Symbol.dispose];
|
|
13
|
-
if (async) inner = dispose;
|
|
14
|
-
}
|
|
15
|
-
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
16
|
-
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
17
|
-
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
18
|
-
}
|
|
19
|
-
else if (async) {
|
|
20
|
-
env.stack.push({ async: true });
|
|
21
|
-
}
|
|
22
|
-
return value;
|
|
23
|
-
};
|
|
24
|
-
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
25
|
-
return function (env) {
|
|
26
|
-
function fail(e) {
|
|
27
|
-
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
28
|
-
env.hasError = true;
|
|
29
|
-
}
|
|
30
|
-
var r, s = 0;
|
|
31
|
-
function next() {
|
|
32
|
-
while (r = env.stack.pop()) {
|
|
33
|
-
try {
|
|
34
|
-
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
35
|
-
if (r.dispose) {
|
|
36
|
-
var result = r.dispose.call(r.value);
|
|
37
|
-
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
38
|
-
}
|
|
39
|
-
else s |= 1;
|
|
40
|
-
}
|
|
41
|
-
catch (e) {
|
|
42
|
-
fail(e);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
46
|
-
if (env.hasError) throw env.error;
|
|
47
|
-
}
|
|
48
|
-
return next();
|
|
49
|
-
};
|
|
50
|
-
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
51
|
-
var e = new Error(message);
|
|
52
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
53
|
-
});
|
|
2
|
+
import { formatBytes } from '@axium/core';
|
|
54
3
|
import { outputDaemonStatus, pluginText } from '@axium/core/node';
|
|
55
4
|
import { _findPlugin, plugins } from '@axium/core/plugins';
|
|
56
5
|
import { CommanderError, program } from 'commander';
|
|
57
6
|
import * as io from 'ioium/node';
|
|
58
|
-
import {
|
|
59
|
-
import * as os from 'node:os';
|
|
60
|
-
import { createInterface } from 'node:readline/promises';
|
|
7
|
+
import { basename } from 'node:path';
|
|
61
8
|
import { styleText } from 'node:util';
|
|
62
9
|
import * as z from 'zod';
|
|
63
10
|
import $pkg from '../../package.json' with { type: 'json' };
|
|
64
|
-
import { config
|
|
65
|
-
import { prefix,
|
|
66
|
-
import {
|
|
67
|
-
import {
|
|
11
|
+
import { config } from '../config.js';
|
|
12
|
+
import { prefix, useUserAgent } from '../requests.js';
|
|
13
|
+
import { logout } from '../user.js';
|
|
14
|
+
import { clientUA, login } from './auth.js';
|
|
15
|
+
import * as cache from './cache.js';
|
|
16
|
+
import { loadConfig, saveConfig, session } from './config.js';
|
|
68
17
|
const safe = z.stringbool().default(false).parse(process.env.SAFE?.toLowerCase()) || process.argv.includes('--safe');
|
|
69
18
|
const debug = z.stringbool().default(false).parse(process.env.DEBUG?.toLowerCase()) || process.argv.includes('--debug');
|
|
70
19
|
if (debug)
|
|
71
20
|
io._setDebugOutput(true);
|
|
72
|
-
const clientUA = `Axium Client/${$pkg.version} (${os.type()}; ${process.arch})`;
|
|
73
21
|
useUserAgent(clientUA);
|
|
74
22
|
await loadConfig(safe);
|
|
23
|
+
cache.load();
|
|
75
24
|
process.on('SIGHUP', () => {
|
|
76
25
|
io.info('Reloading configuration due to SIGHUP.');
|
|
77
26
|
void loadConfig(safe);
|
|
@@ -84,7 +33,7 @@ program
|
|
|
84
33
|
.configureHelp({ showGlobalOptions: true })
|
|
85
34
|
.option('--debug', 'override debug mode')
|
|
86
35
|
.option('--no-debug', 'override debug mode')
|
|
87
|
-
.option('--refresh
|
|
36
|
+
.option('--refresh', 'Force an update of caches from server', false)
|
|
88
37
|
.option('--cache-only', 'Run entirely from local cache, even if it is expired.', false)
|
|
89
38
|
.option('--safe', 'do not execute code from plugins', false)
|
|
90
39
|
.hook('preAction', async (axc, action) => {
|
|
@@ -92,101 +41,20 @@ program
|
|
|
92
41
|
if (!config.token)
|
|
93
42
|
return;
|
|
94
43
|
if (!opt.cacheOnly && action.name() != 'login')
|
|
95
|
-
await
|
|
44
|
+
await cache.update(opt.refresh);
|
|
96
45
|
});
|
|
97
46
|
program.on('option:debug', () => io._setDebugOutput(true));
|
|
98
|
-
program
|
|
99
|
-
.command('login')
|
|
100
|
-
.description('Log in to your account on an Axium server')
|
|
101
|
-
.argument('[server]', 'Axium server URL')
|
|
102
|
-
.action(async (url) => {
|
|
103
|
-
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
104
|
-
try {
|
|
105
|
-
const rl = __addDisposableResource(env_1, createInterface({
|
|
106
|
-
input: process.stdin,
|
|
107
|
-
output: process.stdout,
|
|
108
|
-
}), false);
|
|
109
|
-
rl.on('SIGINT', () => io.exit('Aborted.', 7));
|
|
110
|
-
if (prefix[0] != '/')
|
|
111
|
-
url ||= prefix;
|
|
112
|
-
url ||= await rl.question('Axium server URL: ');
|
|
113
|
-
url = resolveServerURL(url);
|
|
114
|
-
setPrefix(url);
|
|
115
|
-
const sessionReady = Promise.withResolvers();
|
|
116
|
-
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
117
|
-
const server = createServer(async (req, res) => {
|
|
118
|
-
res.setHeader('access-control-allow-origin', '*');
|
|
119
|
-
res.setHeader('access-control-allow-methods', '*');
|
|
120
|
-
res.setHeader('access-control-allow-headers', '*');
|
|
121
|
-
if (req.method == 'HEAD' || req.method == 'OPTIONS') {
|
|
122
|
-
res.writeHead(200).end();
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
if (!req.headers['content-type']?.endsWith('/json')) {
|
|
126
|
-
res.writeHead(415).end('Unexpected content type');
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (req.method !== 'POST') {
|
|
130
|
-
res.writeHead(405).end('Unexpected request method');
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const { promise: bodyReady, resolve, reject } = Promise.withResolvers();
|
|
134
|
-
let body = '';
|
|
135
|
-
req.on('data', chunk => (body += chunk.toString()));
|
|
136
|
-
req.on('end', resolve);
|
|
137
|
-
req.on('error', reject);
|
|
138
|
-
try {
|
|
139
|
-
await bodyReady;
|
|
140
|
-
sessionReady.resolve(JSON.parse(body));
|
|
141
|
-
res.writeHead(200).end();
|
|
142
|
-
}
|
|
143
|
-
catch (e) {
|
|
144
|
-
res.statusCode = 500;
|
|
145
|
-
res.end('Internal server error: ' + e.message);
|
|
146
|
-
sessionReady.reject(e.message);
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
const serverReady = Promise.withResolvers();
|
|
150
|
-
server.listen(() => {
|
|
151
|
-
const { port } = server.address();
|
|
152
|
-
serverReady.resolve(port);
|
|
153
|
-
});
|
|
154
|
-
server.on('error', e => io.exit('Failed to start local callback server: ' + e.message, 5));
|
|
155
|
-
const port = await serverReady.promise;
|
|
156
|
-
const authURL = new URL(`/login/client?port=${port}&client=${encodeURIComponent(clientUA)}`, url).href;
|
|
157
|
-
console.log('Authenticate by visiting this URL in your browser: ' + styleText('underline', authURL));
|
|
158
|
-
const { token } = await sessionReady.promise.catch(e => io.exit('Failed to obtain session: ' + e, 6));
|
|
159
|
-
setToken(token);
|
|
160
|
-
server.close();
|
|
161
|
-
const session = await io.track('Verifying session', getCurrentSession().catch(e => io.exit(e.message, 6)));
|
|
162
|
-
io.debug('Session UUID: ' + session.id);
|
|
163
|
-
console.log(`Welcome ${session.user.name}! Your session is valid until ${session.expires.toLocaleDateString()}.`);
|
|
164
|
-
config.token = token;
|
|
165
|
-
config.server = url;
|
|
166
|
-
saveConfig();
|
|
167
|
-
await updateCache(true);
|
|
168
|
-
}
|
|
169
|
-
catch (e_1) {
|
|
170
|
-
env_1.error = e_1;
|
|
171
|
-
env_1.hasError = true;
|
|
172
|
-
}
|
|
173
|
-
finally {
|
|
174
|
-
__disposeResources(env_1);
|
|
175
|
-
}
|
|
176
|
-
});
|
|
47
|
+
program.command('login').description('Log in to your account on an Axium server').argument('[server]', 'Axium server URL').action(login);
|
|
177
48
|
program.command('logout').action(async () => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (!config.cache)
|
|
181
|
-
io.exit('No session data available.', 3);
|
|
182
|
-
await logout(config.cache.session.userId, config.cache.session.id);
|
|
49
|
+
const { id, userId } = session();
|
|
50
|
+
await logout(userId, id);
|
|
183
51
|
});
|
|
184
52
|
program.command('status').action(() => {
|
|
185
53
|
if (!config.token)
|
|
186
54
|
return console.log('Not logged in.');
|
|
187
55
|
if (!config.cache)
|
|
188
56
|
return console.log('No session data available.');
|
|
189
|
-
const { session } =
|
|
57
|
+
const { session } = cache.meta.data;
|
|
190
58
|
console.log('Logged in to', new URL(prefix).host);
|
|
191
59
|
console.log(styleText('whiteBright', 'Session:'), 'valid until', session.expires.toLocaleDateString(), styleText('dim', `(${session.id})`));
|
|
192
60
|
const { user } = session;
|
|
@@ -205,8 +73,8 @@ program
|
|
|
205
73
|
for (const plugin of plugins.values())
|
|
206
74
|
await plugin._client?.run();
|
|
207
75
|
});
|
|
208
|
-
const
|
|
209
|
-
|
|
76
|
+
const axcPlugin = program.command('plugin').alias('plugins').description('Manage plugins');
|
|
77
|
+
axcPlugin
|
|
210
78
|
.command('list')
|
|
211
79
|
.alias('ls')
|
|
212
80
|
.description('List loaded plugins')
|
|
@@ -226,7 +94,7 @@ axiumPlugin
|
|
|
226
94
|
console.log(plugin.name, opt.versions ? plugin.version : '');
|
|
227
95
|
}
|
|
228
96
|
});
|
|
229
|
-
|
|
97
|
+
axcPlugin
|
|
230
98
|
.command('info')
|
|
231
99
|
.description('Get information about a plugin')
|
|
232
100
|
.argument('<plugin>', 'the plugin to get information about')
|
|
@@ -235,7 +103,7 @@ axiumPlugin
|
|
|
235
103
|
for (const line of pluginText(plugin))
|
|
236
104
|
console.log(line);
|
|
237
105
|
});
|
|
238
|
-
|
|
106
|
+
axcPlugin
|
|
239
107
|
.command('remove')
|
|
240
108
|
.alias('rm')
|
|
241
109
|
.description('Remove a plugin')
|
|
@@ -246,6 +114,32 @@ axiumPlugin
|
|
|
246
114
|
plugins.delete(plugin.name);
|
|
247
115
|
saveConfig();
|
|
248
116
|
});
|
|
117
|
+
const axcCache = program.command('cache').description('Manage the local cache');
|
|
118
|
+
axcCache
|
|
119
|
+
.command('info')
|
|
120
|
+
.description('Show information about what is being cached')
|
|
121
|
+
.action(async () => {
|
|
122
|
+
let size = 0n, files = 0;
|
|
123
|
+
for await (const info of cache.info()) {
|
|
124
|
+
process.stdout.write(basename(info.path) + ':');
|
|
125
|
+
if (!info.exists) {
|
|
126
|
+
console.log(styleText('dim', ' (missing)'));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
files++;
|
|
130
|
+
size += info.size;
|
|
131
|
+
console.log('', styleText('blue', formatBytes(info.size)) + ',', info.fromAPI ? info.entries + ' entries' : info.valid ? styleText('green', 'valid') : styleText('yellow', 'invalid'));
|
|
132
|
+
}
|
|
133
|
+
console.log('Caching', styleText('blue', formatBytes(size)), 'across', styleText('blueBright', files.toString()), 'files');
|
|
134
|
+
});
|
|
135
|
+
axcCache.command('clear').description('Clear the local cache').action(cache.clear);
|
|
136
|
+
axcCache
|
|
137
|
+
.command('refresh')
|
|
138
|
+
.description('Update local caches')
|
|
139
|
+
.option('-f, --force', 'Force a refresh even if the cache is still valid')
|
|
140
|
+
.action(async (opt) => {
|
|
141
|
+
await cache.update(opt.force);
|
|
142
|
+
});
|
|
249
143
|
try {
|
|
250
144
|
await program.parseAsync();
|
|
251
145
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ZodObject, ZodUUID } from 'zod';
|
|
2
|
+
export interface $Objects {
|
|
3
|
+
}
|
|
4
|
+
export type ObjectType = keyof $Objects extends never ? string : keyof $Objects;
|
|
5
|
+
type ObjectValues = keyof $Objects extends never ? Record<string, {
|
|
6
|
+
id: string;
|
|
7
|
+
}[]> : {
|
|
8
|
+
[K in keyof $Objects]: ($Objects[K] & {
|
|
9
|
+
id: string;
|
|
10
|
+
})[];
|
|
11
|
+
};
|
|
12
|
+
export declare function useSchema<Type extends ObjectType, S extends ZodObject<{
|
|
13
|
+
id: ZodUUID;
|
|
14
|
+
}>>(type: Type, schema: S): void;
|
|
15
|
+
export declare function get<Type extends ObjectType>(type: Type): ObjectValues[Type];
|
|
16
|
+
export declare function save<Type extends ObjectType>(type: Type, objects: ObjectValues[Type]): void;
|
|
17
|
+
export {};
|
package/dist/cli/sync.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { sync as syncCache } from './cache.js';
|
|
2
|
+
let _byType;
|
|
3
|
+
function byType() {
|
|
4
|
+
_byType ||= Object.groupBy(syncCache.data.objects, o => o.$type);
|
|
5
|
+
return _byType;
|
|
6
|
+
}
|
|
7
|
+
const schemas = new Map();
|
|
8
|
+
export function useSchema(type, schema) {
|
|
9
|
+
schemas.set(type, schema);
|
|
10
|
+
}
|
|
11
|
+
export function get(type) {
|
|
12
|
+
const value = byType()[type] || [];
|
|
13
|
+
const schema = schemas.get(type);
|
|
14
|
+
if (!schema)
|
|
15
|
+
return value;
|
|
16
|
+
return value.map(obj => schema.parse(obj));
|
|
17
|
+
}
|
|
18
|
+
export function save(type, objects) {
|
|
19
|
+
_byType ||= {};
|
|
20
|
+
_byType[type] = objects;
|
|
21
|
+
syncCache.data.objects = syncCache.data.objects.filter(o => o.$type !== type);
|
|
22
|
+
for (const object of objects)
|
|
23
|
+
syncCache.data.objects.push(Object.assign(object, { $type: type }));
|
|
24
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -2,37 +2,6 @@ import * as z from 'zod';
|
|
|
2
2
|
export declare const ClientConfig: z.ZodObject<{
|
|
3
3
|
token: z.ZodOptional<z.ZodNullable<z.ZodBase64URL>>;
|
|
4
4
|
server: z.ZodOptional<z.ZodNullable<z.ZodURL>>;
|
|
5
|
-
cache: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
6
|
-
fetched: z.ZodInt;
|
|
7
|
-
session: z.ZodObject<{
|
|
8
|
-
id: z.ZodUUID;
|
|
9
|
-
userId: z.ZodUUID;
|
|
10
|
-
name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
11
|
-
expires: z.ZodCoercedDate<unknown>;
|
|
12
|
-
created: z.ZodCoercedDate<unknown>;
|
|
13
|
-
elevated: z.ZodBoolean;
|
|
14
|
-
user: z.ZodObject<{
|
|
15
|
-
id: z.ZodUUID;
|
|
16
|
-
name: z.ZodString;
|
|
17
|
-
email: z.ZodEmail;
|
|
18
|
-
emailVerified: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
|
|
19
|
-
preferences: z.ZodLazy<z.ZodObject<{
|
|
20
|
-
debug: z.ZodDefault<z.ZodBoolean>;
|
|
21
|
-
}, z.core.$strip>>;
|
|
22
|
-
roles: z.ZodArray<z.ZodString>;
|
|
23
|
-
tags: z.ZodArray<z.ZodString>;
|
|
24
|
-
registeredAt: z.ZodCoercedDate<unknown>;
|
|
25
|
-
isAdmin: z.ZodBoolean;
|
|
26
|
-
isSuspended: z.ZodBoolean;
|
|
27
|
-
}, z.core.$strip>;
|
|
28
|
-
}, z.core.$strip>;
|
|
29
|
-
apps: z.ZodArray<z.ZodObject<{
|
|
30
|
-
id: z.ZodString;
|
|
31
|
-
name: z.ZodOptional<z.ZodString>;
|
|
32
|
-
image: z.ZodOptional<z.ZodString>;
|
|
33
|
-
icon: z.ZodOptional<z.ZodString>;
|
|
34
|
-
}, z.core.$strip>>;
|
|
35
|
-
}, z.core.$loose>>>;
|
|
36
5
|
plugins: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
37
6
|
}, z.core.$loose>;
|
|
38
7
|
export interface ClientConfig extends z.infer<typeof ClientConfig> {
|
package/dist/config.js
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
import { debug, warn } from 'ioium';
|
|
2
|
-
import { App, Session, User } from '@axium/core';
|
|
3
2
|
import * as z from 'zod';
|
|
4
3
|
export const ClientConfig = z.looseObject({
|
|
5
4
|
token: z.base64url().nullish(),
|
|
6
5
|
server: z.url().nullish(),
|
|
7
|
-
// Cache to reduce server load:
|
|
8
|
-
cache: z
|
|
9
|
-
.looseObject({
|
|
10
|
-
fetched: z.int(),
|
|
11
|
-
session: Session.extend({ user: User }),
|
|
12
|
-
apps: App.array(),
|
|
13
|
-
})
|
|
14
|
-
.nullish(),
|
|
15
6
|
plugins: z.string().array().default([]),
|
|
16
7
|
});
|
|
17
8
|
export const config = {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from './access.js';
|
|
2
|
-
export * from './
|
|
3
|
-
export *
|
|
2
|
+
export * as preferences from './preferences.js';
|
|
3
|
+
export * from './cache.js';
|
|
4
4
|
export * from './config.js';
|
|
5
5
|
export * from './locales.js';
|
|
6
6
|
export * from './requests.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from './access.js';
|
|
2
|
-
export * from './
|
|
3
|
-
export *
|
|
2
|
+
export * as preferences from './preferences.js';
|
|
3
|
+
export * from './cache.js';
|
|
4
4
|
export * from './config.js';
|
|
5
5
|
export * from './locales.js';
|
|
6
6
|
export * from './requests.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AppPreferences } from '@axium/core';
|
|
2
|
+
export declare function get<A extends string>(userId: string, appId: A): Promise<AppPreferences<A>>;
|
|
3
|
+
export declare function set<A extends string>(userId: string, appId: A, preferences: AppPreferences<A>): Promise<AppPreferences<A>>;
|
|
4
|
+
export declare function clear<A extends string>(userId: string, appId: A): Promise<AppPreferences<A>>;
|
|
5
|
+
export declare function appPref<A extends string, K extends keyof AppPreferences<A>>(userId: string, appId: A, key: K): Promise<AppPreferences<A>[K]>;
|
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
import { appPreferences } from '@axium/core';
|
|
2
|
-
import
|
|
2
|
+
import Cache from './cache.js';
|
|
3
3
|
import { fetchAPI } from './requests.js';
|
|
4
|
-
|
|
4
|
+
const cache = new Cache(async (userId, appId) => {
|
|
5
5
|
const schema = appPreferences.get(appId);
|
|
6
6
|
if (!schema)
|
|
7
7
|
throw new Error(`Missing schema for "${appId}"`);
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return
|
|
8
|
+
const result = await fetchAPI('GET', 'users/:id/preferences/:appId', {}, userId, appId);
|
|
9
|
+
return schema.parse(result);
|
|
10
|
+
});
|
|
11
|
+
export async function get(userId, appId) {
|
|
12
|
+
return (await cache.get(userId, appId));
|
|
13
13
|
}
|
|
14
|
-
export async function
|
|
14
|
+
export async function set(userId, appId, preferences) {
|
|
15
15
|
const schema = appPreferences.get(appId);
|
|
16
16
|
if (!schema)
|
|
17
17
|
throw new Error(`Missing schema for "${appId}"`);
|
|
18
18
|
const result = await fetchAPI('POST', 'users/:id/preferences/:appId', preferences, userId, appId);
|
|
19
|
-
cache.
|
|
19
|
+
cache.set(userId, appId, result);
|
|
20
20
|
return schema.parse(result);
|
|
21
21
|
}
|
|
22
|
-
export async function
|
|
22
|
+
export async function clear(userId, appId) {
|
|
23
23
|
const schema = appPreferences.get(appId);
|
|
24
24
|
if (!schema)
|
|
25
25
|
throw new Error(`Missing schema for "${appId}"`);
|
|
26
26
|
const result = await fetchAPI('DELETE', 'users/:id/preferences/:appId', {}, userId, appId);
|
|
27
|
-
cache.invalidate(
|
|
27
|
+
cache.invalidate(userId, appId);
|
|
28
28
|
return schema.parse(result);
|
|
29
29
|
}
|
|
30
30
|
export async function appPref(userId, appId, key) {
|
|
31
|
-
const pref = await
|
|
31
|
+
const pref = await get(userId, appId);
|
|
32
32
|
return pref[key];
|
|
33
33
|
}
|
package/dist/requests.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { APIParameters,
|
|
1
|
+
import type { APIParameters, Endpoint, RequestBody, Result } from '@axium/core/api';
|
|
2
2
|
import { $API } from '@axium/core/api';
|
|
3
3
|
import type { RequestMethod } from '@axium/core/requests';
|
|
4
4
|
export declare let token: string | null;
|
|
@@ -9,4 +9,4 @@ export declare function setPrefix(value: string): void;
|
|
|
9
9
|
* Only for use on non-browser clients, e.g. Node.js
|
|
10
10
|
*/
|
|
11
11
|
export declare function useUserAgent(ua: string | null): void;
|
|
12
|
-
export declare function fetchAPI<const E extends Endpoint, const M extends keyof $API[E] & RequestMethod>(method: M, endpoint: E, data?: RequestBody<M, E>, ...params: APIParameters<E>): Promise<M
|
|
12
|
+
export declare function fetchAPI<const E extends Endpoint, const M extends keyof $API[E] & RequestMethod>(method: M, endpoint: E, data?: RequestBody<M, E>, ...params: APIParameters<E>): Promise<Result<M, E>>;
|
package/dist/user.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { NewSessionResponse, Passkey, PasskeyChangeable, Session, User, UserPublic, Verification } from '@axium/core';
|
|
2
2
|
import * as z from 'zod';
|
|
3
|
+
import Cache from './cache.js';
|
|
3
4
|
export declare function login(userId: string): Promise<NewSessionResponse>;
|
|
4
5
|
/**
|
|
5
6
|
* Create an elevated session for the user to perform sensitive actions.
|
|
@@ -14,7 +15,9 @@ export declare function logout(userId: string, ...sessionId: string[]): Promise<
|
|
|
14
15
|
export declare function logoutAll(userId: string): Promise<Session[]>;
|
|
15
16
|
export declare function logoutCurrentSession(): Promise<Session>;
|
|
16
17
|
export declare function register(_data: Record<string, unknown>): Promise<void>;
|
|
17
|
-
|
|
18
|
+
declare const userCache: Cache<[string], UserPublic>;
|
|
19
|
+
export { userCache as apiUserCache };
|
|
20
|
+
export declare function userInfo(userId: string): Promise<UserPublic>;
|
|
18
21
|
export declare function updateUser(userId: string, data: Record<string, FormDataEntryValue>): Promise<User>;
|
|
19
22
|
export declare function fullUserInfo(userId: string): Promise<User & {
|
|
20
23
|
sessions: Session[];
|
package/dist/user.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { startAuthentication, startRegistration } from '@simplewebauthn/browser';
|
|
2
2
|
import * as z from 'zod';
|
|
3
3
|
import { fetchAPI } from './requests.js';
|
|
4
|
-
import
|
|
4
|
+
import Cache from './cache.js';
|
|
5
5
|
export async function login(userId) {
|
|
6
6
|
const options = await fetchAPI('PUT', 'users/:id/auth', { type: 'login' }, userId);
|
|
7
7
|
const response = await startAuthentication({ optionsJSON: options });
|
|
@@ -62,14 +62,16 @@ function _checkId(userId) {
|
|
|
62
62
|
throw e instanceof z.core.$ZodError ? z.prettifyError(e) : e;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
const userCache = new Cache((userId) => fetchAPI('GET', 'users/:id', {}, userId), { ttl: 86400_000 });
|
|
66
|
+
export { userCache as apiUserCache };
|
|
65
67
|
export async function userInfo(userId) {
|
|
66
68
|
_checkId(userId);
|
|
67
|
-
return await
|
|
69
|
+
return await userCache.get(userId);
|
|
68
70
|
}
|
|
69
71
|
export async function updateUser(userId, data) {
|
|
70
72
|
_checkId(userId);
|
|
71
73
|
const result = await fetchAPI('PATCH', 'users/:id', data, userId);
|
|
72
|
-
|
|
74
|
+
userCache.set(userId, result);
|
|
73
75
|
return result;
|
|
74
76
|
}
|
|
75
77
|
export async function fullUserInfo(userId) {
|
|
@@ -86,7 +88,7 @@ export async function deleteUser(userId, deletingId = userId) {
|
|
|
86
88
|
const response = await startAuthentication({ optionsJSON: options });
|
|
87
89
|
await fetchAPI('POST', 'users/:id/auth', response, deletingId);
|
|
88
90
|
const result = await fetchAPI('DELETE', 'users/:id', response, userId);
|
|
89
|
-
|
|
91
|
+
userCache.invalidate(userId);
|
|
90
92
|
return result;
|
|
91
93
|
}
|
|
92
94
|
export async function emailVerificationEnabled(userId) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { text } from '@axium/client';
|
|
3
|
+
import { preferences as uap } from '@axium/client';
|
|
3
4
|
import { structurallyEqual } from 'utilium';
|
|
4
5
|
import type { ZodObject } from 'zod';
|
|
5
6
|
import ZodInput from './ZodInput.svelte';
|
|
@@ -12,7 +13,7 @@
|
|
|
12
13
|
_parentDialog,
|
|
13
14
|
}: { userId: string; appId: string; schema: ZodObject; _parentDialog?: HTMLDialogElement } = $props();
|
|
14
15
|
|
|
15
|
-
let initialValue = $state(await
|
|
16
|
+
let initialValue = $state(await uap.get(userId, appId));
|
|
16
17
|
let currentValue = $state({ ...initialValue });
|
|
17
18
|
|
|
18
19
|
function cancel() {
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
async function save() {
|
|
24
|
-
initialValue = await
|
|
25
|
+
initialValue = await uap.set(userId, appId, currentValue);
|
|
25
26
|
if (!_parentDialog) return;
|
|
26
27
|
_parentDialog.close();
|
|
27
28
|
toast('success', text('AppPreferences.toast_saved'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"author": "James Prevett <jp@jamespre.dev>",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"build": "tsc"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@axium/core": ">=0.
|
|
48
|
+
"@axium/core": ">=0.32.0",
|
|
49
49
|
"ioium": "^1.0.2",
|
|
50
50
|
"semver": "^7.7.4",
|
|
51
51
|
"svelte": "^5.36.0",
|
package/dist/apps.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { AppPreferences } from '@axium/core';
|
|
2
|
-
export declare function getAppPreferences<A extends string>(userId: string, appId: A): Promise<AppPreferences<A>>;
|
|
3
|
-
export declare function setAppPreferences<A extends string>(userId: string, appId: A, preferences: AppPreferences<A>): Promise<AppPreferences<A>>;
|
|
4
|
-
export declare function clearAppPreferences<A extends string>(userId: string, appId: A): Promise<AppPreferences<A>>;
|
|
5
|
-
export declare function appPref<A extends string, K extends keyof AppPreferences<A>>(userId: string, appId: A, key: K): Promise<AppPreferences<A>[K]>;
|