@faynosync/sdk-js 0.1.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/.github/workflows/create-release.yml +80 -0
- package/CHANGELOG.md +17 -0
- package/LICENSE +201 -0
- package/README.md +259 -0
- package/dist/client.d.ts +24 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +211 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +81 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/system.d.ts +3 -0
- package/dist/system.d.ts.map +1 -0
- package/dist/system.js +11 -0
- package/dist/system.js.map +1 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/examples/basic/README.md +17 -0
- package/examples/basic/index.ts +75 -0
- package/examples/custom-fetch/README.md +29 -0
- package/examples/custom-fetch/index.ts +94 -0
- package/examples/edge-fallback/README.md +24 -0
- package/examples/edge-fallback/index.ts +77 -0
- package/jest.config.js +8 -0
- package/package.json +25 -0
- package/src/client.ts +264 -0
- package/src/errors.ts +72 -0
- package/src/index.ts +17 -0
- package/src/system.ts +7 -0
- package/src/types.ts +27 -0
- package/tests/client.test.ts +519 -0
- package/tests/errors.test.ts +97 -0
- package/tests/helpers.ts +30 -0
- package/tests/system.test.ts +23 -0
- package/tsconfig.json +17 -0
- package/tsconfig.test.json +8 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
import { Client, RequestFailedError } from '../../src';
|
|
3
|
+
import type { UpdateResponse, UpdateSource } from '../../src';
|
|
4
|
+
|
|
5
|
+
const DEVICE_ID = 'device-1';
|
|
6
|
+
|
|
7
|
+
const keepAliveAgent = new https.Agent({
|
|
8
|
+
keepAlive: true,
|
|
9
|
+
maxSockets: 10,
|
|
10
|
+
timeout: 5_000,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const customFetch: typeof globalThis.fetch = (input, init) => {
|
|
14
|
+
return globalThis.fetch(input, {
|
|
15
|
+
...init,
|
|
16
|
+
agent: keepAliveAgent,
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const client = new Client({
|
|
21
|
+
baseURL: 'http://localhost:9000',
|
|
22
|
+
fetch: customFetch,
|
|
23
|
+
timeoutMs: 10_000,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
async function main(): Promise<void> {
|
|
27
|
+
let resp: UpdateResponse;
|
|
28
|
+
try {
|
|
29
|
+
resp = await client.checkForUpdates({
|
|
30
|
+
owner: 'admin',
|
|
31
|
+
appName: 'test',
|
|
32
|
+
version: '0.0.0.1',
|
|
33
|
+
channel: 'nightly',
|
|
34
|
+
platform: 'darwin',
|
|
35
|
+
arch: 'arm64',
|
|
36
|
+
deviceId: DEVICE_ID,
|
|
37
|
+
});
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (err instanceof RequestFailedError) {
|
|
40
|
+
console.error('update check request failed:', err.message);
|
|
41
|
+
} else {
|
|
42
|
+
console.error('invalid update check options:', (err as Error).message);
|
|
43
|
+
}
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
printUpdateResponse(resp);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function printUpdateResponse(resp: UpdateResponse): void {
|
|
51
|
+
const pad = ' ';
|
|
52
|
+
|
|
53
|
+
console.log('-- Update check response --');
|
|
54
|
+
console.log(`${pad}update_available: ${resp.updateAvailable}`);
|
|
55
|
+
console.log(`${pad}critical: ${resp.critical}`);
|
|
56
|
+
console.log(`${pad}is_intermediate_required: ${resp.isIntermediateRequired}`);
|
|
57
|
+
console.log(`${pad}possible_rollback: ${resp.possibleRollback}`);
|
|
58
|
+
console.log(`${pad}source: ${formatUpdateSource(resp.source)}`);
|
|
59
|
+
|
|
60
|
+
if (resp.updateUrl !== '') {
|
|
61
|
+
console.log(`${pad}update_url: ${resp.updateUrl}`);
|
|
62
|
+
} else {
|
|
63
|
+
console.log(`${pad}update_url: (empty)`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (resp.changelog !== '') {
|
|
67
|
+
console.log(`${pad}changelog:`);
|
|
68
|
+
for (const line of resp.changelog.trimEnd().split('\n')) {
|
|
69
|
+
console.log(`${pad} ${line}`);
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
console.log(`${pad}changelog: (empty)`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (resp.packageUrls.length === 0) {
|
|
76
|
+
console.log(`${pad}package_urls: (none)`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(`${pad}package_urls:`);
|
|
81
|
+
for (const pkg of resp.packageUrls) {
|
|
82
|
+
console.log(`${pad} ${pkg.package.padEnd(12)} ${pkg.url}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatUpdateSource(source: UpdateSource): string {
|
|
87
|
+
switch (source) {
|
|
88
|
+
case 'edge': return 'edge';
|
|
89
|
+
case 'api': return 'api';
|
|
90
|
+
default: return 'unknown';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
main();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# edge-fallback
|
|
2
|
+
|
|
3
|
+
Update check with a static edge CDN configured alongside the base API.
|
|
4
|
+
|
|
5
|
+
Demonstrates:
|
|
6
|
+
|
|
7
|
+
- Configuring `edgeURL` for a static JSON edge response
|
|
8
|
+
- Automatic fallback to `baseURL` when the edge misses or fails
|
|
9
|
+
- Passing `deviceId` to trigger a telemetry beacon after a successful edge hit
|
|
10
|
+
- Reading `resp.source` to see which endpoint served the response
|
|
11
|
+
|
|
12
|
+
## How it works
|
|
13
|
+
|
|
14
|
+
1. The SDK requests `GET {edgeURL}/responses/{owner}/{appName}/{channel}/{platform}/{arch}/{version}.json`.
|
|
15
|
+
2. On HTTP 200 with valid JSON, the response is returned with `source: 'edge'` and a telemetry beacon is sent to `{baseURL}/telemetry/beacon`.
|
|
16
|
+
3. On any failure (404, network error, invalid JSON), the SDK retries against `{baseURL}/checkVersion`.
|
|
17
|
+
|
|
18
|
+
## Run
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npx ts-node examples/edge-fallback/index.ts
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Expects a faynoSync server at `http://localhost:9000` and an edge server at `http://cb-faynosync-s3-public.web.garage.localhost:3902`.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Client, RequestFailedError } from '../../src';
|
|
2
|
+
import type { UpdateResponse, UpdateSource } from '../../src';
|
|
3
|
+
|
|
4
|
+
const client = new Client({
|
|
5
|
+
baseURL: 'http://localhost:9000',
|
|
6
|
+
edgeURL: 'http://cb-faynosync-s3-public.web.garage.localhost:3902',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
async function main(): Promise<void> {
|
|
10
|
+
let resp: UpdateResponse;
|
|
11
|
+
try {
|
|
12
|
+
resp = await client.checkForUpdates({
|
|
13
|
+
owner: 'admin',
|
|
14
|
+
appName: 'test',
|
|
15
|
+
version: '0.0.0.1',
|
|
16
|
+
channel: 'nightly',
|
|
17
|
+
platform: 'darwin',
|
|
18
|
+
arch: 'arm64',
|
|
19
|
+
deviceId: 'device-1-fnsj',
|
|
20
|
+
});
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err instanceof RequestFailedError) {
|
|
23
|
+
console.error('update check request failed:', err.message);
|
|
24
|
+
} else {
|
|
25
|
+
console.error('invalid update check options:', (err as Error).message);
|
|
26
|
+
}
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
printUpdateResponse(resp);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function printUpdateResponse(resp: UpdateResponse): void {
|
|
34
|
+
const pad = ' ';
|
|
35
|
+
|
|
36
|
+
console.log('-- Update check response --');
|
|
37
|
+
console.log(`${pad}update_available: ${resp.updateAvailable}`);
|
|
38
|
+
console.log(`${pad}critical: ${resp.critical}`);
|
|
39
|
+
console.log(`${pad}is_intermediate_required: ${resp.isIntermediateRequired}`);
|
|
40
|
+
console.log(`${pad}possible_rollback: ${resp.possibleRollback}`);
|
|
41
|
+
console.log(`${pad}source: ${formatUpdateSource(resp.source)}`);
|
|
42
|
+
|
|
43
|
+
if (resp.updateUrl !== '') {
|
|
44
|
+
console.log(`${pad}update_url: ${resp.updateUrl}`);
|
|
45
|
+
} else {
|
|
46
|
+
console.log(`${pad}update_url: (empty)`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (resp.changelog !== '') {
|
|
50
|
+
console.log(`${pad}changelog:`);
|
|
51
|
+
for (const line of resp.changelog.trimEnd().split('\n')) {
|
|
52
|
+
console.log(`${pad} ${line}`);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
console.log(`${pad}changelog: (empty)`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (resp.packageUrls.length === 0) {
|
|
59
|
+
console.log(`${pad}package_urls: (none)`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(`${pad}package_urls:`);
|
|
64
|
+
for (const pkg of resp.packageUrls) {
|
|
65
|
+
console.log(`${pad} ${pkg.package.padEnd(12)} ${pkg.url}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatUpdateSource(source: UpdateSource): string {
|
|
70
|
+
switch (source) {
|
|
71
|
+
case 'edge': return 'edge';
|
|
72
|
+
case 'api': return 'api';
|
|
73
|
+
default: return 'unknown';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
main();
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@faynosync/sdk-js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "JavaScript/TypeScript SDK for faynoSync",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"lint": "tsc --project tsconfig.test.json"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/jest": "^29.5.14",
|
|
20
|
+
"@types/node": "^20.17.0",
|
|
21
|
+
"jest": "^29.7.0",
|
|
22
|
+
"ts-jest": "^29.2.0",
|
|
23
|
+
"typescript": "^5.7.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import type { CheckOptions, PackageUpdateURL, UpdateResponse, UpdateSource } from './types';
|
|
2
|
+
import {
|
|
3
|
+
CheckError,
|
|
4
|
+
EndpointError,
|
|
5
|
+
ErrInvalidBaseURL,
|
|
6
|
+
ErrInvalidEdgeURL,
|
|
7
|
+
ErrMissingAppName,
|
|
8
|
+
ErrMissingBaseURL,
|
|
9
|
+
ErrMissingOwner,
|
|
10
|
+
ErrMissingVersion,
|
|
11
|
+
ValidationError,
|
|
12
|
+
} from './errors';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
15
|
+
const USER_AGENT = 'faynosync-js/1.0';
|
|
16
|
+
|
|
17
|
+
export interface Config {
|
|
18
|
+
readonly baseURL: string;
|
|
19
|
+
readonly edgeURL?: string;
|
|
20
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
21
|
+
readonly timeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface RawUpdateResponse {
|
|
25
|
+
update_available?: boolean;
|
|
26
|
+
update_url?: string;
|
|
27
|
+
changelog?: string;
|
|
28
|
+
critical?: boolean;
|
|
29
|
+
is_intermediate_required?: boolean;
|
|
30
|
+
possible_rollback?: boolean;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class Client {
|
|
35
|
+
private readonly baseURL: string;
|
|
36
|
+
private readonly edgeURL: string;
|
|
37
|
+
private readonly fetchFn: typeof globalThis.fetch;
|
|
38
|
+
private readonly timeoutMs: number;
|
|
39
|
+
|
|
40
|
+
constructor(cfg: Config) {
|
|
41
|
+
this.baseURL = cfg.baseURL;
|
|
42
|
+
this.edgeURL = cfg.edgeURL ?? '';
|
|
43
|
+
this.fetchFn = cfg.fetch ?? globalThis.fetch.bind(globalThis);
|
|
44
|
+
this.timeoutMs = cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async checkForUpdates(opts: CheckOptions, signal?: AbortSignal): Promise<UpdateResponse> {
|
|
48
|
+
this.validateConfig();
|
|
49
|
+
validateCheckOptions(opts);
|
|
50
|
+
|
|
51
|
+
const sig = signal ?? null;
|
|
52
|
+
let edgeError: Error | undefined;
|
|
53
|
+
|
|
54
|
+
if (this.edgeURL !== '') {
|
|
55
|
+
try {
|
|
56
|
+
const resp = await this.checkEdge(opts, sig);
|
|
57
|
+
if (opts.deviceId) {
|
|
58
|
+
try {
|
|
59
|
+
await this.sendEdgeTelemetryBeacon(opts, !resp.updateAvailable, sig);
|
|
60
|
+
} catch {
|
|
61
|
+
// telemetry failures don't affect the update check result
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { ...resp, source: 'edge' };
|
|
65
|
+
} catch (err) {
|
|
66
|
+
edgeError = err as Error;
|
|
67
|
+
if (signal?.aborted === true) {
|
|
68
|
+
throw new CheckError(edgeError);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const resp = await this.checkAPI(opts, sig);
|
|
75
|
+
return { ...resp, source: 'api' };
|
|
76
|
+
} catch (apiError) {
|
|
77
|
+
throw new CheckError(edgeError, apiError as Error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private validateConfig(): void {
|
|
82
|
+
if (this.baseURL.trim() === '') {
|
|
83
|
+
throw ErrMissingBaseURL;
|
|
84
|
+
}
|
|
85
|
+
parseAbsoluteURL(this.baseURL, ErrInvalidBaseURL);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private async checkEdge(opts: CheckOptions, signal: AbortSignal | null): Promise<UpdateResponse> {
|
|
89
|
+
const url = this.buildEdgeCheckURL(opts);
|
|
90
|
+
return this.doUpdateRequest(url, opts.deviceId, 'edge', signal);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async checkAPI(opts: CheckOptions, signal: AbortSignal | null): Promise<UpdateResponse> {
|
|
94
|
+
const url = this.buildAPICheckURL(opts);
|
|
95
|
+
return this.doUpdateRequest(url, opts.deviceId, 'api', signal);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private async doUpdateRequest(
|
|
99
|
+
url: string,
|
|
100
|
+
deviceId: string | undefined,
|
|
101
|
+
source: UpdateSource,
|
|
102
|
+
signal: AbortSignal | null,
|
|
103
|
+
): Promise<UpdateResponse> {
|
|
104
|
+
const headers: Record<string, string> = {
|
|
105
|
+
Accept: 'application/json',
|
|
106
|
+
'User-Agent': USER_AGENT,
|
|
107
|
+
};
|
|
108
|
+
if (deviceId) {
|
|
109
|
+
headers['X-Device-ID'] = deviceId;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const reqSignal = createRequestSignal(signal, this.timeoutMs);
|
|
113
|
+
|
|
114
|
+
let res: Response;
|
|
115
|
+
try {
|
|
116
|
+
res = await this.fetchFn(url, { headers, signal: reqSignal });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
throw new EndpointError(source, url, undefined, err as Error);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (res.status !== 200) {
|
|
122
|
+
await res.body?.cancel();
|
|
123
|
+
throw new EndpointError(source, url, res.status);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let raw: RawUpdateResponse;
|
|
127
|
+
try {
|
|
128
|
+
raw = (await res.json()) as RawUpdateResponse;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
throw new EndpointError(source, url, undefined, err as Error);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return parseUpdateResponse(raw);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private buildAPICheckURL(opts: CheckOptions): string {
|
|
137
|
+
const u = parseAbsoluteURL(this.baseURL, ErrInvalidBaseURL);
|
|
138
|
+
u.pathname = joinURLPath(u.pathname, 'checkVersion');
|
|
139
|
+
u.search = new URLSearchParams({
|
|
140
|
+
app_name: opts.appName,
|
|
141
|
+
version: opts.version,
|
|
142
|
+
channel: opts.channel ?? '',
|
|
143
|
+
platform: opts.platform ?? '',
|
|
144
|
+
arch: opts.arch ?? '',
|
|
145
|
+
owner: opts.owner,
|
|
146
|
+
}).toString();
|
|
147
|
+
return u.toString();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private buildEdgeCheckURL(opts: CheckOptions): string {
|
|
151
|
+
const u = parseAbsoluteURL(this.edgeURL, ErrInvalidEdgeURL);
|
|
152
|
+
const segments = [
|
|
153
|
+
'responses',
|
|
154
|
+
opts.owner,
|
|
155
|
+
opts.appName,
|
|
156
|
+
opts.channel ?? '',
|
|
157
|
+
opts.platform ?? '',
|
|
158
|
+
opts.arch ?? '',
|
|
159
|
+
`${opts.version}.json`,
|
|
160
|
+
];
|
|
161
|
+
const base = (u.origin + u.pathname).replace(/\/+$/, '');
|
|
162
|
+
return `${base}/${segments.map(encodeURIComponent).join('/')}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private buildEdgeTelemetryBeaconURL(opts: CheckOptions, isLatest: boolean): string {
|
|
166
|
+
const u = parseAbsoluteURL(this.baseURL, ErrInvalidBaseURL);
|
|
167
|
+
u.pathname = joinURLPath(u.pathname, 'telemetry/beacon');
|
|
168
|
+
u.search = new URLSearchParams({
|
|
169
|
+
app_name: opts.appName,
|
|
170
|
+
version: opts.version,
|
|
171
|
+
channel: opts.channel ?? '',
|
|
172
|
+
platform: opts.platform ?? '',
|
|
173
|
+
arch: opts.arch ?? '',
|
|
174
|
+
owner: opts.owner,
|
|
175
|
+
is_latest: String(isLatest),
|
|
176
|
+
}).toString();
|
|
177
|
+
return u.toString();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private async sendEdgeTelemetryBeacon(
|
|
181
|
+
opts: CheckOptions,
|
|
182
|
+
isLatest: boolean,
|
|
183
|
+
signal: AbortSignal | null,
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
const url = this.buildEdgeTelemetryBeaconURL(opts, isLatest);
|
|
186
|
+
const headers: Record<string, string> = { 'User-Agent': USER_AGENT };
|
|
187
|
+
if (opts.deviceId) {
|
|
188
|
+
headers['X-Device-ID'] = opts.deviceId;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const reqSignal = createRequestSignal(signal, this.timeoutMs);
|
|
192
|
+
const res = await this.fetchFn(url, { headers, signal: reqSignal });
|
|
193
|
+
await res.body?.cancel();
|
|
194
|
+
if (res.status !== 200) {
|
|
195
|
+
throw new EndpointError('edge', url, res.status);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function validateCheckOptions(opts: CheckOptions): void {
|
|
201
|
+
if (!opts.owner) throw ErrMissingOwner;
|
|
202
|
+
if (!opts.appName) throw ErrMissingAppName;
|
|
203
|
+
if (!opts.version) throw ErrMissingVersion;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseAbsoluteURL(raw: string, sentinel: ValidationError): URL {
|
|
207
|
+
let u: URL;
|
|
208
|
+
try {
|
|
209
|
+
u = new URL(raw);
|
|
210
|
+
} catch {
|
|
211
|
+
throw sentinel;
|
|
212
|
+
}
|
|
213
|
+
if (!u.protocol || !u.hostname) {
|
|
214
|
+
throw sentinel;
|
|
215
|
+
}
|
|
216
|
+
return u;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function joinURLPath(basePath: string, segment: string): string {
|
|
220
|
+
if (basePath === '' || basePath === '/') {
|
|
221
|
+
return `/${segment}`;
|
|
222
|
+
}
|
|
223
|
+
return `${basePath.replace(/\/+$/, '')}/${segment}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function createRequestSignal(userSignal: AbortSignal | null, timeoutMs: number): AbortSignal {
|
|
227
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
228
|
+
if (userSignal === null) return timeoutSignal;
|
|
229
|
+
|
|
230
|
+
if (userSignal.aborted) {
|
|
231
|
+
const c = new AbortController();
|
|
232
|
+
c.abort(userSignal.reason);
|
|
233
|
+
return c.signal;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const controller = new AbortController();
|
|
237
|
+
const abort = (reason: unknown) => controller.abort(reason);
|
|
238
|
+
userSignal.addEventListener('abort', () => abort(userSignal.reason), { once: true });
|
|
239
|
+
timeoutSignal.addEventListener('abort', () => abort(timeoutSignal.reason), { once: true });
|
|
240
|
+
return controller.signal;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function parseUpdateResponse(raw: RawUpdateResponse): UpdateResponse {
|
|
244
|
+
const packageUrls: PackageUpdateURL[] = [];
|
|
245
|
+
|
|
246
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
247
|
+
if (key.startsWith('update_url_') && typeof value === 'string') {
|
|
248
|
+
packageUrls.push({ package: key.slice('update_url_'.length), url: value });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
packageUrls.sort((a, b) => a.package.localeCompare(b.package));
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
updateAvailable: raw.update_available ?? false,
|
|
256
|
+
updateUrl: raw.update_url ?? '',
|
|
257
|
+
changelog: raw.changelog ?? '',
|
|
258
|
+
critical: raw.critical ?? false,
|
|
259
|
+
isIntermediateRequired: raw.is_intermediate_required ?? false,
|
|
260
|
+
possibleRollback: raw.possible_rollback ?? false,
|
|
261
|
+
packageUrls,
|
|
262
|
+
source: 'unknown',
|
|
263
|
+
};
|
|
264
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { UpdateSource } from './types';
|
|
2
|
+
|
|
3
|
+
export class FaynoSyncError extends Error {
|
|
4
|
+
constructor(message: string) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'FaynoSyncError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ValidationError extends FaynoSyncError {
|
|
11
|
+
constructor(message: string) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'ValidationError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class RequestFailedError extends FaynoSyncError {
|
|
18
|
+
constructor(message: string) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'RequestFailedError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class EndpointError extends RequestFailedError {
|
|
25
|
+
constructor(
|
|
26
|
+
public readonly source: UpdateSource,
|
|
27
|
+
public readonly endpointUrl: string,
|
|
28
|
+
public readonly statusCode?: number,
|
|
29
|
+
cause?: Error,
|
|
30
|
+
) {
|
|
31
|
+
let msg: string;
|
|
32
|
+
if (statusCode !== undefined) {
|
|
33
|
+
msg = `faynosync: request failed: ${endpointUrl} returned HTTP ${statusCode}`;
|
|
34
|
+
} else if (cause !== undefined) {
|
|
35
|
+
msg = `faynosync: request failed: ${endpointUrl}: ${cause.message}`;
|
|
36
|
+
} else {
|
|
37
|
+
msg = `faynosync: request failed: ${endpointUrl}`;
|
|
38
|
+
}
|
|
39
|
+
super(msg);
|
|
40
|
+
this.name = 'EndpointError';
|
|
41
|
+
if (cause !== undefined) {
|
|
42
|
+
this.cause = cause;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class CheckError extends RequestFailedError {
|
|
48
|
+
constructor(
|
|
49
|
+
public readonly edgeError?: Error,
|
|
50
|
+
public readonly apiError?: Error,
|
|
51
|
+
) {
|
|
52
|
+
let msg: string;
|
|
53
|
+
if (edgeError !== undefined && apiError !== undefined) {
|
|
54
|
+
msg = `faynosync: request failed: edge failed: ${edgeError.message}; api failed: ${apiError.message}`;
|
|
55
|
+
} else if (edgeError !== undefined) {
|
|
56
|
+
msg = `faynosync: request failed: edge failed: ${edgeError.message}`;
|
|
57
|
+
} else if (apiError !== undefined) {
|
|
58
|
+
msg = `faynosync: request failed: api failed: ${apiError.message}`;
|
|
59
|
+
} else {
|
|
60
|
+
msg = 'faynosync: request failed';
|
|
61
|
+
}
|
|
62
|
+
super(msg);
|
|
63
|
+
this.name = 'CheckError';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const ErrMissingBaseURL = new ValidationError('faynosync: missing base URL');
|
|
68
|
+
export const ErrInvalidBaseURL = new ValidationError('faynosync: invalid base URL');
|
|
69
|
+
export const ErrInvalidEdgeURL = new ValidationError('faynosync: invalid edge URL');
|
|
70
|
+
export const ErrMissingOwner = new ValidationError('faynosync: missing owner');
|
|
71
|
+
export const ErrMissingAppName = new ValidationError('faynosync: missing app name');
|
|
72
|
+
export const ErrMissingVersion = new ValidationError('faynosync: missing version');
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { Client } from './client';
|
|
2
|
+
export type { Config } from './client';
|
|
3
|
+
export type { CheckOptions, PackageUpdateURL, UpdateResponse, UpdateSource } from './types';
|
|
4
|
+
export {
|
|
5
|
+
CheckError,
|
|
6
|
+
EndpointError,
|
|
7
|
+
ErrInvalidBaseURL,
|
|
8
|
+
ErrInvalidEdgeURL,
|
|
9
|
+
ErrMissingAppName,
|
|
10
|
+
ErrMissingBaseURL,
|
|
11
|
+
ErrMissingOwner,
|
|
12
|
+
ErrMissingVersion,
|
|
13
|
+
FaynoSyncError,
|
|
14
|
+
RequestFailedError,
|
|
15
|
+
ValidationError,
|
|
16
|
+
} from './errors';
|
|
17
|
+
export { systemArch, systemPlatform } from './system';
|
package/src/system.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type UpdateSource = 'unknown' | 'edge' | 'api';
|
|
2
|
+
|
|
3
|
+
export interface PackageUpdateURL {
|
|
4
|
+
readonly package: string;
|
|
5
|
+
readonly url: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CheckOptions {
|
|
9
|
+
readonly owner: string;
|
|
10
|
+
readonly appName: string;
|
|
11
|
+
readonly version: string;
|
|
12
|
+
readonly channel?: string;
|
|
13
|
+
readonly platform?: string;
|
|
14
|
+
readonly arch?: string;
|
|
15
|
+
readonly deviceId?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UpdateResponse {
|
|
19
|
+
readonly updateAvailable: boolean;
|
|
20
|
+
readonly updateUrl: string;
|
|
21
|
+
readonly changelog: string;
|
|
22
|
+
readonly critical: boolean;
|
|
23
|
+
readonly isIntermediateRequired: boolean;
|
|
24
|
+
readonly possibleRollback: boolean;
|
|
25
|
+
readonly packageUrls: readonly PackageUpdateURL[];
|
|
26
|
+
readonly source: UpdateSource;
|
|
27
|
+
}
|