@aluvia/sdk 1.0.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/LICENSE +21 -0
- package/README.md +423 -0
- package/dist/cjs/api/AluviaApi.js +51 -0
- package/dist/cjs/api/account.js +155 -0
- package/dist/cjs/api/geos.js +76 -0
- package/dist/cjs/api/request.js +84 -0
- package/dist/cjs/api/types.js +2 -0
- package/dist/cjs/client/AluviaClient.js +325 -0
- package/dist/cjs/client/ConfigManager.js +303 -0
- package/dist/cjs/client/ProxyServer.js +182 -0
- package/dist/cjs/client/adapters.js +49 -0
- package/dist/cjs/client/logger.js +52 -0
- package/dist/cjs/client/rules.js +128 -0
- package/dist/cjs/client/types.js +3 -0
- package/dist/cjs/errors.js +49 -0
- package/dist/cjs/index.js +16 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/api/AluviaApi.js +47 -0
- package/dist/esm/api/account.js +152 -0
- package/dist/esm/api/geos.js +73 -0
- package/dist/esm/api/request.js +81 -0
- package/dist/esm/api/types.js +1 -0
- package/dist/esm/client/AluviaClient.js +321 -0
- package/dist/esm/client/ConfigManager.js +299 -0
- package/dist/esm/client/ProxyServer.js +178 -0
- package/dist/esm/client/adapters.js +39 -0
- package/dist/esm/client/logger.js +48 -0
- package/dist/esm/client/rules.js +124 -0
- package/dist/esm/client/types.js +2 -0
- package/dist/esm/errors.js +42 -0
- package/dist/esm/index.js +7 -0
- package/dist/types/api/AluviaApi.d.ts +29 -0
- package/dist/types/api/account.d.ts +41 -0
- package/dist/types/api/geos.d.ts +5 -0
- package/dist/types/api/request.d.ts +20 -0
- package/dist/types/api/types.d.ts +30 -0
- package/dist/types/client/AluviaClient.d.ts +50 -0
- package/dist/types/client/ConfigManager.d.ts +100 -0
- package/dist/types/client/ProxyServer.d.ts +47 -0
- package/dist/types/client/adapters.d.ts +26 -0
- package/dist/types/client/logger.d.ts +33 -0
- package/dist/types/client/rules.d.ts +34 -0
- package/dist/types/client/types.d.ts +194 -0
- package/dist/types/errors.d.ts +25 -0
- package/dist/types/index.d.ts +5 -0
- package/package.json +65 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Rule engine for hostname matching and proxy decision
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.matchPattern = matchPattern;
|
|
5
|
+
exports.shouldProxy = shouldProxy;
|
|
6
|
+
/**
|
|
7
|
+
* Match a hostname against a pattern.
|
|
8
|
+
*
|
|
9
|
+
* Supported patterns:
|
|
10
|
+
* - '*' matches any hostname
|
|
11
|
+
* - '*.example.com' matches subdomains of example.com (but not example.com itself)
|
|
12
|
+
* - 'example.com' exact match
|
|
13
|
+
* - 'google.*' matches google.com, google.co.uk, etc.
|
|
14
|
+
*
|
|
15
|
+
* @param hostname - The hostname to match
|
|
16
|
+
* @param pattern - The pattern to match against
|
|
17
|
+
* @returns true if hostname matches pattern
|
|
18
|
+
*/
|
|
19
|
+
function matchPattern(hostname, pattern) {
|
|
20
|
+
// Normalize inputs to lowercase
|
|
21
|
+
const normalizedHostname = hostname.trim().toLowerCase();
|
|
22
|
+
const normalizedPattern = pattern.trim().toLowerCase();
|
|
23
|
+
if (!normalizedHostname)
|
|
24
|
+
return false;
|
|
25
|
+
if (!normalizedPattern)
|
|
26
|
+
return false;
|
|
27
|
+
// Universal wildcard matches everything
|
|
28
|
+
if (normalizedPattern === '*') {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
// Exact match
|
|
32
|
+
if (normalizedHostname === normalizedPattern) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
// Prefix wildcard: *.example.com matches subdomains
|
|
36
|
+
if (normalizedPattern.startsWith('*.')) {
|
|
37
|
+
const suffix = normalizedPattern.slice(1); // '.example.com'
|
|
38
|
+
// Must end with the suffix and have something before it
|
|
39
|
+
if (normalizedHostname.endsWith(suffix)) {
|
|
40
|
+
const prefix = normalizedHostname.slice(0, -suffix.length);
|
|
41
|
+
// Prefix must not be empty. This matches any subdomain depth (for example, foo.example.com and foo.bar.example.com).
|
|
42
|
+
return prefix.length > 0;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
// Suffix wildcard: google.* matches google.com, google.co.uk, etc.
|
|
47
|
+
if (normalizedPattern.endsWith('.*')) {
|
|
48
|
+
const prefix = normalizedPattern.slice(0, -2); // 'google'
|
|
49
|
+
// Must start with prefix followed by a dot
|
|
50
|
+
if (normalizedHostname.startsWith(prefix + '.')) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Determine if a hostname should be proxied based on rules.
|
|
59
|
+
*
|
|
60
|
+
* Rules semantics:
|
|
61
|
+
* - [] (empty) → no proxy (return false)
|
|
62
|
+
* - ['*'] → proxy everything
|
|
63
|
+
* - ['example.com'] → proxy only example.com
|
|
64
|
+
* - ['*.google.com'] → proxy subdomains of google.com
|
|
65
|
+
* - ['*', '-example.com'] → proxy everything except example.com
|
|
66
|
+
* - ['AUTO', 'example.com'] → AUTO is placeholder (ignored), proxy example.com
|
|
67
|
+
*
|
|
68
|
+
* Negative patterns (prefixed with '-') exclude hosts from proxying.
|
|
69
|
+
* If '*' is in rules, default is to proxy unless excluded.
|
|
70
|
+
* Without '*', only explicitly matched patterns are proxied.
|
|
71
|
+
*
|
|
72
|
+
* @param hostname - The hostname to check
|
|
73
|
+
* @param rules - Array of rule patterns
|
|
74
|
+
* @returns true if the hostname should be proxied
|
|
75
|
+
*/
|
|
76
|
+
function shouldProxy(hostname, rules) {
|
|
77
|
+
const normalizedHostname = hostname.trim();
|
|
78
|
+
if (!normalizedHostname)
|
|
79
|
+
return false;
|
|
80
|
+
// Empty rules means no proxy
|
|
81
|
+
if (!rules || rules.length === 0) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const normalizedRules = rules
|
|
85
|
+
.filter((r) => typeof r === 'string')
|
|
86
|
+
.map((r) => r.trim())
|
|
87
|
+
.filter((r) => r.length > 0);
|
|
88
|
+
// Filter out AUTO placeholder
|
|
89
|
+
const effectiveRules = normalizedRules.filter((r) => r.toUpperCase() !== 'AUTO');
|
|
90
|
+
// If no effective rules after filtering, no proxy
|
|
91
|
+
if (effectiveRules.length === 0) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
// Separate positive and negative rules
|
|
95
|
+
const negativeRules = [];
|
|
96
|
+
const positiveRules = [];
|
|
97
|
+
for (const rule of effectiveRules) {
|
|
98
|
+
if (rule.startsWith('-')) {
|
|
99
|
+
const neg = rule.slice(1).trim(); // Remove the '-' prefix
|
|
100
|
+
if (neg.length > 0)
|
|
101
|
+
negativeRules.push(neg);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
positiveRules.push(rule);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Check if hostname matches any negative rule
|
|
108
|
+
for (const negRule of negativeRules) {
|
|
109
|
+
if (matchPattern(normalizedHostname, negRule)) {
|
|
110
|
+
// Excluded by negative rule
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Check if we have a catch-all '*'
|
|
115
|
+
const hasCatchAll = positiveRules.includes('*');
|
|
116
|
+
if (hasCatchAll) {
|
|
117
|
+
// With catch-all, proxy everything not excluded by negative rules
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
// Without catch-all, check if hostname matches any positive rule
|
|
121
|
+
for (const posRule of positiveRules) {
|
|
122
|
+
if (matchPattern(normalizedHostname, posRule)) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// No match found
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Error classes for Aluvia Client
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ProxyStartError = exports.ApiError = exports.InvalidApiKeyError = exports.MissingApiKeyError = void 0;
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when the apiKey is not provided to AluviaClient.
|
|
7
|
+
*/
|
|
8
|
+
class MissingApiKeyError extends Error {
|
|
9
|
+
constructor(message = 'Aluvia connection apiKey is required') {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'MissingApiKeyError';
|
|
12
|
+
Object.setPrototypeOf(this, MissingApiKeyError.prototype);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.MissingApiKeyError = MissingApiKeyError;
|
|
16
|
+
/**
|
|
17
|
+
* Thrown when the API returns 401 or 403, indicating the apiKey is invalid.
|
|
18
|
+
*/
|
|
19
|
+
class InvalidApiKeyError extends Error {
|
|
20
|
+
constructor(message = 'Invalid or expired Aluvia connection apiKey') {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'InvalidApiKeyError';
|
|
23
|
+
Object.setPrototypeOf(this, InvalidApiKeyError.prototype);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.InvalidApiKeyError = InvalidApiKeyError;
|
|
27
|
+
/**
|
|
28
|
+
* Thrown for general API errors (non-2xx responses other than auth errors).
|
|
29
|
+
*/
|
|
30
|
+
class ApiError extends Error {
|
|
31
|
+
constructor(message, statusCode) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'ApiError';
|
|
34
|
+
this.statusCode = statusCode;
|
|
35
|
+
Object.setPrototypeOf(this, ApiError.prototype);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.ApiError = ApiError;
|
|
39
|
+
/**
|
|
40
|
+
* Thrown when the local proxy server fails to start.
|
|
41
|
+
*/
|
|
42
|
+
class ProxyStartError extends Error {
|
|
43
|
+
constructor(message = 'Failed to start local proxy server') {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = 'ProxyStartError';
|
|
46
|
+
Object.setPrototypeOf(this, ProxyStartError.prototype);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.ProxyStartError = ProxyStartError;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Aluvia Client Node
|
|
3
|
+
// Main entry point
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.ProxyStartError = exports.ApiError = exports.InvalidApiKeyError = exports.MissingApiKeyError = exports.AluviaApi = exports.AluviaClient = void 0;
|
|
6
|
+
// Public class
|
|
7
|
+
var AluviaClient_js_1 = require("./client/AluviaClient.js");
|
|
8
|
+
Object.defineProperty(exports, "AluviaClient", { enumerable: true, get: function () { return AluviaClient_js_1.AluviaClient; } });
|
|
9
|
+
var AluviaApi_js_1 = require("./api/AluviaApi.js");
|
|
10
|
+
Object.defineProperty(exports, "AluviaApi", { enumerable: true, get: function () { return AluviaApi_js_1.AluviaApi; } });
|
|
11
|
+
// Public error classes
|
|
12
|
+
var errors_js_1 = require("./errors.js");
|
|
13
|
+
Object.defineProperty(exports, "MissingApiKeyError", { enumerable: true, get: function () { return errors_js_1.MissingApiKeyError; } });
|
|
14
|
+
Object.defineProperty(exports, "InvalidApiKeyError", { enumerable: true, get: function () { return errors_js_1.InvalidApiKeyError; } });
|
|
15
|
+
Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return errors_js_1.ApiError; } });
|
|
16
|
+
Object.defineProperty(exports, "ProxyStartError", { enumerable: true, get: function () { return errors_js_1.ProxyStartError; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { requestCore } from './request.js';
|
|
2
|
+
import { createAccountApi } from './account.js';
|
|
3
|
+
import { createGeosApi } from './geos.js';
|
|
4
|
+
import { MissingApiKeyError } from '../errors.js';
|
|
5
|
+
export class AluviaApi {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
const apiKey = String(options.apiKey ?? '').trim();
|
|
8
|
+
if (!apiKey) {
|
|
9
|
+
throw new MissingApiKeyError('Aluvia apiKey is required');
|
|
10
|
+
}
|
|
11
|
+
this.apiKey = apiKey;
|
|
12
|
+
this.apiBaseUrl = options.apiBaseUrl ?? 'https://api.aluvia.io/v1';
|
|
13
|
+
this.timeoutMs = options.timeoutMs;
|
|
14
|
+
this.fetchImpl = options.fetch;
|
|
15
|
+
const ctx = {
|
|
16
|
+
request: async (args) => {
|
|
17
|
+
return await requestCore({
|
|
18
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
19
|
+
apiKey: this.apiKey,
|
|
20
|
+
method: args.method,
|
|
21
|
+
path: args.path,
|
|
22
|
+
query: args.query,
|
|
23
|
+
body: args.body,
|
|
24
|
+
headers: args.headers,
|
|
25
|
+
ifNoneMatch: args.etag,
|
|
26
|
+
timeoutMs: this.timeoutMs,
|
|
27
|
+
fetch: this.fetchImpl,
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
this.account = createAccountApi(ctx);
|
|
32
|
+
this.geos = createGeosApi(ctx);
|
|
33
|
+
}
|
|
34
|
+
async request(args) {
|
|
35
|
+
return await requestCore({
|
|
36
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
37
|
+
apiKey: this.apiKey,
|
|
38
|
+
method: args.method,
|
|
39
|
+
path: args.path,
|
|
40
|
+
query: args.query,
|
|
41
|
+
body: args.body,
|
|
42
|
+
headers: args.headers,
|
|
43
|
+
timeoutMs: this.timeoutMs,
|
|
44
|
+
fetch: this.fetchImpl,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { ApiError, InvalidApiKeyError } from '../errors.js';
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null;
|
|
4
|
+
}
|
|
5
|
+
function asErrorEnvelope(value) {
|
|
6
|
+
if (!isRecord(value))
|
|
7
|
+
return null;
|
|
8
|
+
if (value['success'] !== false)
|
|
9
|
+
return null;
|
|
10
|
+
const error = value['error'];
|
|
11
|
+
if (!isRecord(error))
|
|
12
|
+
return null;
|
|
13
|
+
const code = error['code'];
|
|
14
|
+
const message = error['message'];
|
|
15
|
+
if (typeof code !== 'string' || typeof message !== 'string')
|
|
16
|
+
return null;
|
|
17
|
+
return { success: false, error: { code, message, details: error['details'] } };
|
|
18
|
+
}
|
|
19
|
+
function unwrapSuccess(value) {
|
|
20
|
+
if (!isRecord(value))
|
|
21
|
+
return null;
|
|
22
|
+
if (value['success'] === true && 'data' in value) {
|
|
23
|
+
return value.data;
|
|
24
|
+
}
|
|
25
|
+
if ('data' in value) {
|
|
26
|
+
return value.data;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function formatErrorDetails(details) {
|
|
31
|
+
if (details == null)
|
|
32
|
+
return '';
|
|
33
|
+
try {
|
|
34
|
+
const json = JSON.stringify(details);
|
|
35
|
+
if (!json)
|
|
36
|
+
return '';
|
|
37
|
+
return json.length > 500 ? `${json.slice(0, 500)}…` : json;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return String(details);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function throwForNon2xx(result) {
|
|
44
|
+
const status = result.status;
|
|
45
|
+
if (status === 401 || status === 403) {
|
|
46
|
+
throw new InvalidApiKeyError(`Authentication failed (HTTP ${status}). Check token validity and that you are using an account API token for account endpoints.`);
|
|
47
|
+
}
|
|
48
|
+
const maybeError = asErrorEnvelope(result.body);
|
|
49
|
+
if (maybeError) {
|
|
50
|
+
const details = formatErrorDetails(maybeError.error.details);
|
|
51
|
+
const detailsSuffix = details ? ` details=${details}` : '';
|
|
52
|
+
throw new ApiError(`API request failed (HTTP ${status}) code=${maybeError.error.code} message=${maybeError.error.message}${detailsSuffix}`, status);
|
|
53
|
+
}
|
|
54
|
+
throw new ApiError(`API request failed (HTTP ${status})`, status);
|
|
55
|
+
}
|
|
56
|
+
async function requestAndUnwrap(ctx, args) {
|
|
57
|
+
const result = await ctx.request(args);
|
|
58
|
+
if (result.status < 200 || result.status >= 300) {
|
|
59
|
+
throwForNon2xx(result);
|
|
60
|
+
}
|
|
61
|
+
const data = unwrapSuccess(result.body);
|
|
62
|
+
if (data == null) {
|
|
63
|
+
throw new ApiError('API response missing expected success envelope data', result.status);
|
|
64
|
+
}
|
|
65
|
+
return { data, etag: result.etag };
|
|
66
|
+
}
|
|
67
|
+
export function createAccountApi(ctx) {
|
|
68
|
+
return {
|
|
69
|
+
get: async () => {
|
|
70
|
+
const { data } = await requestAndUnwrap(ctx, {
|
|
71
|
+
method: 'GET',
|
|
72
|
+
path: '/account',
|
|
73
|
+
});
|
|
74
|
+
return data;
|
|
75
|
+
},
|
|
76
|
+
usage: {
|
|
77
|
+
get: async (params = {}) => {
|
|
78
|
+
const { data } = await requestAndUnwrap(ctx, {
|
|
79
|
+
method: 'GET',
|
|
80
|
+
path: '/account/usage',
|
|
81
|
+
query: {
|
|
82
|
+
start: params.start,
|
|
83
|
+
end: params.end,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
return data;
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
payments: {
|
|
90
|
+
list: async (params = {}) => {
|
|
91
|
+
const { data } = await requestAndUnwrap(ctx, {
|
|
92
|
+
method: 'GET',
|
|
93
|
+
path: '/account/payments',
|
|
94
|
+
query: {
|
|
95
|
+
start: params.start,
|
|
96
|
+
end: params.end,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
return data;
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
connections: {
|
|
103
|
+
list: async () => {
|
|
104
|
+
const { data } = await requestAndUnwrap(ctx, {
|
|
105
|
+
method: 'GET',
|
|
106
|
+
path: '/account/connections',
|
|
107
|
+
});
|
|
108
|
+
return data;
|
|
109
|
+
},
|
|
110
|
+
create: async (body) => {
|
|
111
|
+
const { data } = await requestAndUnwrap(ctx, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
path: '/account/connections',
|
|
114
|
+
body,
|
|
115
|
+
});
|
|
116
|
+
return data;
|
|
117
|
+
},
|
|
118
|
+
get: async (connectionId, options = {}) => {
|
|
119
|
+
const result = await ctx.request({
|
|
120
|
+
method: 'GET',
|
|
121
|
+
path: `/account/connections/${String(connectionId)}`,
|
|
122
|
+
etag: options.etag ?? null,
|
|
123
|
+
});
|
|
124
|
+
if (result.status === 304)
|
|
125
|
+
return null;
|
|
126
|
+
if (result.status < 200 || result.status >= 300) {
|
|
127
|
+
throwForNon2xx(result);
|
|
128
|
+
}
|
|
129
|
+
const data = unwrapSuccess(result.body);
|
|
130
|
+
if (data == null) {
|
|
131
|
+
throw new ApiError('API response missing expected success envelope data', result.status);
|
|
132
|
+
}
|
|
133
|
+
return data;
|
|
134
|
+
},
|
|
135
|
+
patch: async (connectionId, body) => {
|
|
136
|
+
const { data } = await requestAndUnwrap(ctx, {
|
|
137
|
+
method: 'PATCH',
|
|
138
|
+
path: `/account/connections/${String(connectionId)}`,
|
|
139
|
+
body,
|
|
140
|
+
});
|
|
141
|
+
return data;
|
|
142
|
+
},
|
|
143
|
+
delete: async (connectionId) => {
|
|
144
|
+
const { data } = await requestAndUnwrap(ctx, {
|
|
145
|
+
method: 'DELETE',
|
|
146
|
+
path: `/account/connections/${String(connectionId)}`,
|
|
147
|
+
});
|
|
148
|
+
return data;
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ApiError, InvalidApiKeyError } from '../errors.js';
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null;
|
|
4
|
+
}
|
|
5
|
+
function asErrorEnvelope(value) {
|
|
6
|
+
if (!isRecord(value))
|
|
7
|
+
return null;
|
|
8
|
+
if (value['success'] !== false)
|
|
9
|
+
return null;
|
|
10
|
+
const error = value['error'];
|
|
11
|
+
if (!isRecord(error))
|
|
12
|
+
return null;
|
|
13
|
+
const code = error['code'];
|
|
14
|
+
const message = error['message'];
|
|
15
|
+
if (typeof code !== 'string' || typeof message !== 'string')
|
|
16
|
+
return null;
|
|
17
|
+
return { success: false, error: { code, message, details: error['details'] } };
|
|
18
|
+
}
|
|
19
|
+
function formatErrorDetails(details) {
|
|
20
|
+
if (details == null)
|
|
21
|
+
return '';
|
|
22
|
+
try {
|
|
23
|
+
const json = JSON.stringify(details);
|
|
24
|
+
if (!json)
|
|
25
|
+
return '';
|
|
26
|
+
return json.length > 500 ? `${json.slice(0, 500)}…` : json;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return String(details);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function unwrapSuccessArray(value) {
|
|
33
|
+
if (!isRecord(value))
|
|
34
|
+
return null;
|
|
35
|
+
if (value['success'] === true && Array.isArray(value.data)) {
|
|
36
|
+
return value.data;
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(value.data)) {
|
|
39
|
+
return value.data;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function throwForNon2xx(result) {
|
|
44
|
+
const status = result.status;
|
|
45
|
+
if (status === 401 || status === 403) {
|
|
46
|
+
throw new InvalidApiKeyError(`Authentication failed (HTTP ${status}). Check token validity and that you are using an account API token for account endpoints.`);
|
|
47
|
+
}
|
|
48
|
+
const maybeError = asErrorEnvelope(result.body);
|
|
49
|
+
if (maybeError) {
|
|
50
|
+
const details = formatErrorDetails(maybeError.error.details);
|
|
51
|
+
const detailsSuffix = details ? ` details=${details}` : '';
|
|
52
|
+
throw new ApiError(`API request failed (HTTP ${status}) code=${maybeError.error.code} message=${maybeError.error.message}${detailsSuffix}`, status);
|
|
53
|
+
}
|
|
54
|
+
throw new ApiError(`API request failed (HTTP ${status})`, status);
|
|
55
|
+
}
|
|
56
|
+
export function createGeosApi(ctx) {
|
|
57
|
+
return {
|
|
58
|
+
list: async () => {
|
|
59
|
+
const result = await ctx.request({
|
|
60
|
+
method: 'GET',
|
|
61
|
+
path: '/geos',
|
|
62
|
+
});
|
|
63
|
+
if (result.status < 200 || result.status >= 300) {
|
|
64
|
+
throwForNon2xx(result);
|
|
65
|
+
}
|
|
66
|
+
const data = unwrapSuccessArray(result.body);
|
|
67
|
+
if (data == null) {
|
|
68
|
+
throw new ApiError('API response missing expected success envelope data', result.status);
|
|
69
|
+
}
|
|
70
|
+
return data;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ApiError } from '../errors.js';
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
3
|
+
function joinUrl(baseUrl, path) {
|
|
4
|
+
const base = baseUrl.replace(/\/+$/, '');
|
|
5
|
+
const p = path.startsWith('/') ? path : `/${path}`;
|
|
6
|
+
return `${base}${p}`;
|
|
7
|
+
}
|
|
8
|
+
function buildQueryString(query) {
|
|
9
|
+
if (!query)
|
|
10
|
+
return '';
|
|
11
|
+
const params = new URLSearchParams();
|
|
12
|
+
for (const [key, value] of Object.entries(query)) {
|
|
13
|
+
if (value == null)
|
|
14
|
+
continue;
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
for (const v of value)
|
|
17
|
+
params.append(key, String(v));
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
params.set(key, String(value));
|
|
21
|
+
}
|
|
22
|
+
const qs = params.toString();
|
|
23
|
+
return qs ? `?${qs}` : '';
|
|
24
|
+
}
|
|
25
|
+
function isJsonResponse(contentType) {
|
|
26
|
+
if (!contentType)
|
|
27
|
+
return false;
|
|
28
|
+
return contentType.toLowerCase().includes('application/json');
|
|
29
|
+
}
|
|
30
|
+
export async function requestCore(options) {
|
|
31
|
+
const url = `${joinUrl(options.apiBaseUrl, options.path)}${buildQueryString(options.query)}`;
|
|
32
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
33
|
+
if (typeof fetchImpl !== 'function') {
|
|
34
|
+
throw new Error('globalThis.fetch is not available; Node 18+ is required');
|
|
35
|
+
}
|
|
36
|
+
const headers = {
|
|
37
|
+
Accept: 'application/json',
|
|
38
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
39
|
+
...(options.headers ?? {}),
|
|
40
|
+
};
|
|
41
|
+
if (options.ifNoneMatch)
|
|
42
|
+
headers['If-None-Match'] = options.ifNoneMatch;
|
|
43
|
+
const hasJsonBody = options.body !== undefined && options.body !== null;
|
|
44
|
+
if (hasJsonBody)
|
|
45
|
+
headers['Content-Type'] = 'application/json';
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
48
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
49
|
+
try {
|
|
50
|
+
let response;
|
|
51
|
+
try {
|
|
52
|
+
response = await fetchImpl(url, {
|
|
53
|
+
method: options.method,
|
|
54
|
+
headers,
|
|
55
|
+
body: hasJsonBody ? JSON.stringify(options.body) : undefined,
|
|
56
|
+
signal: controller.signal,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (controller.signal.aborted) {
|
|
61
|
+
throw new ApiError(`Request timed out after ${timeoutMs}ms`, 408);
|
|
62
|
+
}
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
const etag = response.headers.get('etag');
|
|
66
|
+
if (response.status === 204 || response.status === 304) {
|
|
67
|
+
return { status: response.status, etag, body: null };
|
|
68
|
+
}
|
|
69
|
+
const contentType = response.headers.get('content-type');
|
|
70
|
+
if (!isJsonResponse(contentType)) {
|
|
71
|
+
return { status: response.status, etag, body: null };
|
|
72
|
+
}
|
|
73
|
+
const text = await response.text();
|
|
74
|
+
if (!text)
|
|
75
|
+
return { status: response.status, etag, body: null };
|
|
76
|
+
return { status: response.status, etag, body: JSON.parse(text) };
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
clearTimeout(timeoutId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|