@homecloud-platform/sdk 0.4.9 → 0.5.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/src/index.js CHANGED
@@ -1,274 +1,54 @@
1
- "use strict";
2
-
3
- const crypto = require("crypto");
4
- const { URL } = require("url");
5
- const fs = require("fs");
6
- const path = require("path");
7
-
8
- const DEFAULT_APEX = "holab.abrdns.com";
9
-
10
- function soUrl(apex) {
11
- return `https://so.${apex || DEFAULT_APEX}`;
12
- }
13
- function mqUrl(apex) {
14
- return `https://mq.${apex || DEFAULT_APEX}`;
15
- }
16
- function secretsUrl(apex) {
17
- return `https://secrets.${apex || DEFAULT_APEX}`;
18
- }
19
- function mailApiUrl(apex) {
20
- return `https://mailapi.${apex || DEFAULT_APEX}`;
21
- }
22
-
23
- function buildStringToSign({ method, path: reqPath, timestamp, accountId }) {
24
- return `${String(method).toUpperCase()}\n${reqPath}\n${timestamp}\n${accountId}`;
25
- }
26
-
27
- function signRequestHeaders({
28
- accessKeyId,
29
- secret,
30
- method,
31
- path: reqPath,
32
- accountId,
33
- sessionToken,
34
- }) {
35
- const ts = new Date();
36
- const timestamp = ts.toISOString().replace(/\.\d{3}Z$/, "Z");
37
- const stringToSign = buildStringToSign({
38
- method,
39
- path: reqPath,
40
- timestamp,
41
- accountId,
42
- });
43
- const signature = crypto.createHmac("sha256", secret).update(stringToSign).digest("hex");
44
- const headers = {
45
- "X-Homecloud-Access-Key-Id": accessKeyId,
46
- "X-Homecloud-Date": timestamp,
47
- "X-Homecloud-Signature": signature,
48
- };
49
- if (sessionToken) headers["X-Homecloud-Session-Token"] = sessionToken;
50
- return headers;
51
- }
52
-
53
- class HomeCloudError extends Error {
54
- constructor(message, { statusCode, detail } = {}) {
55
- super(message);
56
- this.name = "HomeCloudError";
57
- this.statusCode = statusCode;
58
- this.detail = detail;
59
- }
60
- }
61
-
62
- class HomeCloud {
63
- constructor({
64
- accessKeyId,
65
- secretAccessKey,
66
- accountId,
67
- apex = DEFAULT_APEX,
68
- sessionToken,
69
- dataPlaneBases,
70
- }) {
71
- this.accessKeyId = accessKeyId;
72
- this.secretAccessKey = secretAccessKey;
73
- this.accountId = accountId;
74
- this.apex = apex || DEFAULT_APEX;
75
- this.sessionToken = sessionToken || null;
76
- this.dataPlaneBases = dataPlaneBases || {};
77
- this.so = new SoAPI(this);
78
- this.mq = new MqAPI(this);
79
- this.secrets = new SecretsAPI(this);
80
- this.mail = new MailAPI(this);
81
- }
82
-
83
- static fromSts(sts, { accountId, apex } = {}) {
84
- const aid = accountId || process.env.HC_ACCOUNT_ID || "";
85
- let base = String(sts.base_url || sts.mail_base_url || "").replace(/\/$/, "");
86
- const resourceType = String(sts.resource_type || "").trim().toLowerCase();
87
- let resolvedApex = apex || process.env.HC_APEX || DEFAULT_APEX;
88
- const dataPlaneBases = {};
89
- if (base) {
90
- let host = "";
91
- try {
92
- host = new URL(base).hostname || "";
93
- } catch (_) {
94
- host = "";
95
- }
96
- if (resourceType === "mail") {
97
- if (host.startsWith("console.") || base.includes("/api/v1")) {
98
- if (host.startsWith("console.")) resolvedApex = resolvedApex || host.slice("console.".length);
99
- dataPlaneBases.mail = mailApiUrl(resolvedApex).replace(/\/$/, "");
100
- } else {
101
- dataPlaneBases.mail = base;
102
- if (host.startsWith("mailapi.")) resolvedApex = resolvedApex || host.slice("mailapi.".length);
103
- }
104
- } else if (["so", "mq", "secrets"].includes(resourceType)) {
105
- dataPlaneBases[resourceType] = base;
106
- const prefix = `${resourceType}.`;
107
- if (host.startsWith(prefix)) resolvedApex = resolvedApex || host.slice(prefix.length);
108
- }
109
- } else if (resourceType === "mail") {
110
- dataPlaneBases.mail = mailApiUrl(resolvedApex).replace(/\/$/, "");
111
- }
112
- return new HomeCloud({
113
- accessKeyId: String(sts.access_key_id),
114
- secretAccessKey: String(sts.secret_access_key),
115
- accountId: aid,
116
- apex: resolvedApex || DEFAULT_APEX,
117
- sessionToken: sts.session_token ? String(sts.session_token) : null,
118
- dataPlaneBases,
119
- });
120
- }
121
-
122
- static fromFunctionContext(context, { binding }) {
123
- const ctx = context || {};
124
- let stsMap = Object.assign({}, ctx.sts || {});
125
- if (!Object.keys(stsMap).length && process.env.HC_STS_JSON) {
126
- try {
127
- stsMap = JSON.parse(process.env.HC_STS_JSON);
128
- } catch (_) {
129
- stsMap = {};
130
- }
131
- }
132
- const entry = stsMap[binding];
133
- if (!entry || !entry.access_key_id) {
134
- throw new HomeCloudError(`STS binding '${binding}' not found in context.sts`);
135
- }
136
- return HomeCloud.fromSts(entry, {
137
- accountId: ctx.account_id || ctx.accountId || process.env.HC_ACCOUNT_ID,
138
- });
139
- }
140
-
141
- baseUrl(service) {
142
- if (this.dataPlaneBases[service]) return this.dataPlaneBases[service].replace(/\/$/, "");
143
- if (service === "so") return soUrl(this.apex);
144
- if (service === "mq") return mqUrl(this.apex);
145
- if (service === "secrets") return secretsUrl(this.apex);
146
- if (service === "mail") return mailApiUrl(this.apex);
147
- throw new HomeCloudError(`Unknown data-plane service: ${service}`);
148
- }
149
-
150
- async dataPlaneRequest(service, method, reqPath, { json, formData, body, headers } = {}) {
151
- const base = this.baseUrl(service);
152
- const url = new URL(reqPath.startsWith("http") ? reqPath : `${base}${reqPath}`);
153
- const signPath = url.pathname + (url.search || "");
154
- const auth = signRequestHeaders({
155
- accessKeyId: this.accessKeyId,
156
- secret: this.secretAccessKey,
157
- method,
158
- path: url.pathname,
159
- accountId: this.accountId,
160
- sessionToken: this.sessionToken,
161
- });
162
- const init = {
163
- method,
164
- headers: { ...auth, ...(headers || {}) },
165
- };
166
- if (formData) {
167
- init.body = formData;
168
- } else if (json !== undefined) {
169
- init.headers["Content-Type"] = "application/json";
170
- init.body = JSON.stringify(json);
171
- } else if (body !== undefined) {
172
- init.body = body;
173
- }
174
- const res = await fetch(url, init);
175
- const text = await res.text();
176
- let data = null;
177
- if (text) {
178
- try {
179
- data = JSON.parse(text);
180
- } catch (_) {
181
- data = { raw: text };
182
- }
183
- }
184
- if (!res.ok) {
185
- throw new HomeCloudError(
186
- (data && (data.detail || data.message || data.error)) || `HTTP ${res.status}`,
187
- { statusCode: res.status, detail: data }
188
- );
189
- }
190
- return data;
191
- }
192
- }
193
-
194
- class SoAPI {
195
- constructor(client) {
196
- this._c = client;
197
- }
198
-
199
- async upload(bucketName, filePath, { key } = {}) {
200
- const objectKey = key || path.basename(filePath);
201
- const accountId = this._c.accountId;
202
- const uploadPath = `/${accountId}/${bucketName}/objects`;
203
- const blob = fs.readFileSync(filePath);
204
- const form = new FormData();
205
- form.append("key", objectKey);
206
- form.append("file", new Blob([blob]), path.basename(filePath));
207
- return this._c.dataPlaneRequest("so", "POST", uploadPath, { formData: form });
208
- }
209
-
210
- async putJson(bucketName, objectKey, value) {
211
- const tmp = path.join(
212
- require("os").tmpdir(),
213
- `hc-sdk-${Date.now()}-${Math.random().toString(16).slice(2)}.json`
214
- );
215
- fs.writeFileSync(tmp, JSON.stringify(value, null, 2), "utf8");
216
- try {
217
- return await this.upload(bucketName, tmp, { key: objectKey });
218
- } finally {
219
- try {
220
- fs.unlinkSync(tmp);
221
- } catch (_) {}
222
- }
223
- }
224
- }
225
-
226
- class MqAPI {
227
- constructor(client) {
228
- this._c = client;
229
- }
230
-
231
- async send(queueName, body, { headers } = {}) {
232
- const accountId = this._c.accountId;
233
- const reqPath = `/${accountId}/${queueName}/messages`;
234
- const bodyStr = typeof body === "string" ? body : JSON.stringify(body);
235
- const payload = { body: bodyStr };
236
- if (headers) payload.headers = headers;
237
- return this._c.dataPlaneRequest("mq", "POST", reqPath, { json: payload });
238
- }
239
- }
240
-
241
- class SecretsAPI {
242
- constructor(client) {
243
- this._c = client;
244
- }
245
-
246
- async get(secretName) {
247
- const accountId = this._c.accountId;
248
- const reqPath = `/${accountId}/secrets/${encodeURIComponent(secretName)}`;
249
- return this._c.dataPlaneRequest("secrets", "GET", reqPath);
250
- }
251
- }
252
-
253
- class MailAPI {
254
- constructor(client) {
255
- this._c = client;
256
- }
257
-
258
- async listMessages({ mailbox = "INBOX", limit = 20 } = {}) {
259
- const accountId = this._c.accountId;
260
- const reqPath = `/${accountId}/mailboxes/${encodeURIComponent(mailbox)}/messages?limit=${limit}`;
261
- return this._c.dataPlaneRequest("mail", "GET", reqPath);
262
- }
263
- }
264
-
265
- module.exports = {
266
- HomeCloud,
267
- HomeCloudError,
268
- signRequestHeaders,
269
- soUrl,
270
- mqUrl,
271
- secretsUrl,
272
- mailApiUrl,
273
- DEFAULT_APEX,
274
- };
1
+ "use strict";
2
+
3
+ const { HomeCloud, AsyncHomeCloud } = require("./client");
4
+ const {
5
+ HomeCloudError,
6
+ NotConfiguredError,
7
+ NotLoggedInError,
8
+ ApiError,
9
+ BadRequestError,
10
+ UnauthorizedError,
11
+ PermissionDeniedError,
12
+ NotFoundError,
13
+ ConflictError,
14
+ RateLimitError,
15
+ ServiceUnavailableError,
16
+ errorFromStatus,
17
+ } = require("./errors");
18
+ const { signRequestHeaders, buildStringToSign, soObjectPaths } = require("./signing");
19
+ const {
20
+ DEFAULT_APEX,
21
+ soUrl,
22
+ mqUrl,
23
+ secretsUrl,
24
+ mailApiUrl,
25
+ consoleUrl,
26
+ functionUrl,
27
+ } = require("./defaults");
28
+
29
+ module.exports = {
30
+ HomeCloud,
31
+ AsyncHomeCloud,
32
+ HomeCloudError,
33
+ NotConfiguredError,
34
+ NotLoggedInError,
35
+ ApiError,
36
+ BadRequestError,
37
+ UnauthorizedError,
38
+ PermissionDeniedError,
39
+ NotFoundError,
40
+ ConflictError,
41
+ RateLimitError,
42
+ ServiceUnavailableError,
43
+ errorFromStatus,
44
+ signRequestHeaders,
45
+ buildStringToSign,
46
+ soObjectPaths,
47
+ soUrl,
48
+ mqUrl,
49
+ secretsUrl,
50
+ mailApiUrl,
51
+ consoleUrl,
52
+ functionUrl,
53
+ DEFAULT_APEX,
54
+ };
package/src/mail.js ADDED
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+
3
+ const { HomeCloudError } = require("./errors");
4
+
5
+ function mailConsolePathToDataPlane(p, accountId) {
6
+ const raw = String(p).replace(/^\/+/, "");
7
+ const prefix = `accounts/${accountId}/mail/`;
8
+ if (raw.startsWith(prefix)) return `/${accountId}/${raw.slice(prefix.length)}`;
9
+ if (raw.startsWith(`${accountId}/`)) return `/${raw}`;
10
+ throw new HomeCloudError(`Unexpected mail path for data plane: ${p}`);
11
+ }
12
+
13
+ class MailAPI {
14
+ constructor(client) {
15
+ this._c = client;
16
+ }
17
+
18
+ _useMailDataPlane() {
19
+ return Boolean(this._c.accessKeyId && this._c.dataPlaneBases.mail);
20
+ }
21
+
22
+ async _mailRequest(method, consolePath, { params } = {}) {
23
+ const accountId = this._c.accountId;
24
+ if (this._useMailDataPlane()) {
25
+ const dpPath = mailConsolePathToDataPlane(consolePath, accountId);
26
+ return this._c.dataPlaneRequest("mail", method, dpPath, { params });
27
+ }
28
+ this._c.requireConsole();
29
+ return this._c.consoleRequest(method, consolePath, { params });
30
+ }
31
+
32
+ async _mailRequestBytes(method, consolePath) {
33
+ const accountId = this._c.accountId;
34
+ if (this._useMailDataPlane()) {
35
+ const dpPath = mailConsolePathToDataPlane(consolePath, accountId);
36
+ return this._c.dataPlaneRequestBytes("mail", method, dpPath);
37
+ }
38
+ this._c.requireConsole();
39
+ return this._c.consoleRequestBytes(method, consolePath);
40
+ }
41
+
42
+ async listMailboxes() {
43
+ const accountId = this._c.accountId;
44
+ const data = await this._mailRequest("GET", `accounts/${accountId}/mail/mailboxes`);
45
+ return data.items || [];
46
+ }
47
+
48
+ async listMessages({
49
+ mailboxId = null,
50
+ folder = null,
51
+ direction = null,
52
+ status = null,
53
+ search = null,
54
+ limit = 50,
55
+ cursor = null,
56
+ // legacy helper used by early smoke
57
+ mailbox = null,
58
+ } = {}) {
59
+ const accountId = this._c.accountId;
60
+ if (mailbox && !mailboxId && this._useMailDataPlane()) {
61
+ // Early MVP path: /{account}/mailboxes/{name}/messages
62
+ const reqPath = `/${accountId}/mailboxes/${encodeURIComponent(mailbox)}/messages`;
63
+ return this._c.dataPlaneRequest("mail", "GET", reqPath, { params: { limit } });
64
+ }
65
+ const params = { limit };
66
+ if (mailboxId) params.mailbox_id = mailboxId;
67
+ if (folder) params.folder = folder;
68
+ if (direction) params.direction = direction;
69
+ if (status) params.status = status;
70
+ if (search) params.search = search;
71
+ if (cursor) params.cursor = cursor;
72
+ return this._mailRequest("GET", `accounts/${accountId}/mail/messages`, { params });
73
+ }
74
+
75
+ async getMessage(messageId) {
76
+ const accountId = this._c.accountId;
77
+ return this._mailRequest("GET", `accounts/${accountId}/mail/messages/${messageId}`);
78
+ }
79
+
80
+ async downloadAttachment(messageId, partId) {
81
+ const accountId = this._c.accountId;
82
+ return this._mailRequestBytes(
83
+ "GET",
84
+ `accounts/${accountId}/mail/messages/${messageId}/attachments/${partId}`
85
+ );
86
+ }
87
+ }
88
+
89
+ module.exports = { MailAPI };
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+
3
+ class AccountsAPI {
4
+ constructor(client) {
5
+ this._c = client;
6
+ }
7
+
8
+ async list() {
9
+ this._c.requireConsole();
10
+ const data = await this._c.consoleRequest("GET", "accounts");
11
+ return data.items || data || [];
12
+ }
13
+
14
+ async switch(accountRef) {
15
+ this._c.requireConsole();
16
+ return this._c.consoleRequest("POST", "accounts/switch", {
17
+ json: { account: accountRef },
18
+ });
19
+ }
20
+ }
21
+
22
+ class AppsAPI {
23
+ constructor(client) {
24
+ this._c = client;
25
+ }
26
+
27
+ async list() {
28
+ this._c.requireConsole();
29
+ const data = await this._c.consoleRequest(
30
+ "GET",
31
+ `accounts/${this._c.accountId}/applications`
32
+ );
33
+ return data.items || [];
34
+ }
35
+ }
36
+
37
+ class QueuesAPI {
38
+ constructor(client) {
39
+ this._c = client;
40
+ }
41
+
42
+ async list() {
43
+ this._c.requireConsole();
44
+ const data = await this._c.consoleRequest(
45
+ "GET",
46
+ `accounts/${this._c.accountId}/queues`
47
+ );
48
+ return data.items || [];
49
+ }
50
+ }
51
+
52
+ class FunctionsAPI {
53
+ constructor(client) {
54
+ this._c = client;
55
+ }
56
+
57
+ async list() {
58
+ this._c.requireConsole();
59
+ const data = await this._c.consoleRequest(
60
+ "GET",
61
+ `accounts/${this._c.accountId}/functions`
62
+ );
63
+ return data.items || [];
64
+ }
65
+
66
+ async url(name) {
67
+ this._c.requireConsole();
68
+ return this._c.consoleRequest(
69
+ "GET",
70
+ `accounts/${this._c.accountId}/functions/${name}/url`
71
+ );
72
+ }
73
+
74
+ async enableUrl(name, { publicUrl = false, rateLimitPerMinute = 60 } = {}) {
75
+ this._c.requireConsole();
76
+ return this._c.consoleRequest(
77
+ "POST",
78
+ `accounts/${this._c.accountId}/functions/${name}/url/enable`,
79
+ {
80
+ json: {
81
+ public_url_enabled: publicUrl,
82
+ rate_limit_per_minute: rateLimitPerMinute,
83
+ },
84
+ }
85
+ );
86
+ }
87
+
88
+ async disableUrl(name) {
89
+ this._c.requireConsole();
90
+ return this._c.consoleRequest(
91
+ "POST",
92
+ `accounts/${this._c.accountId}/functions/${name}/url/disable`
93
+ );
94
+ }
95
+
96
+ async invoke(name, payload = {}) {
97
+ this._c.requireAccessKey();
98
+ return this._c.functionUrlRequest(name, payload);
99
+ }
100
+
101
+ async logs(name) {
102
+ this._c.requireConsole();
103
+ const data = await this._c.consoleRequest(
104
+ "GET",
105
+ `accounts/${this._c.accountId}/functions/${name}/invocations`
106
+ );
107
+ return data.items || [];
108
+ }
109
+
110
+ async getInvocation(name, invocationId) {
111
+ this._c.requireConsole();
112
+ return this._c.consoleRequest(
113
+ "GET",
114
+ `accounts/${this._c.accountId}/functions/${name}/invocations/${invocationId}`
115
+ );
116
+ }
117
+ }
118
+
119
+ module.exports = { AccountsAPI, AppsAPI, QueuesAPI, FunctionsAPI };
package/src/mq.js ADDED
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+
3
+ class MqAPI {
4
+ constructor(client) {
5
+ this._c = client;
6
+ }
7
+
8
+ async send(queueName, body, { headers } = {}) {
9
+ this._c.requireAccessKey();
10
+ const accountId = this._c.accountId;
11
+ const reqPath = `/${accountId}/${queueName}/messages`;
12
+ const bodyStr = typeof body === "string" ? body : JSON.stringify(body);
13
+ const payload = { body: bodyStr };
14
+ if (headers) payload.headers = headers;
15
+ return this._c.dataPlaneRequest("mq", "POST", reqPath, { json: payload });
16
+ }
17
+
18
+ async receive(queueName, { maxMessages = 1, waitSeconds = 20 } = {}) {
19
+ this._c.requireAccessKey();
20
+ const accountId = this._c.accountId;
21
+ const reqPath = `/${accountId}/${queueName}/messages`;
22
+ const data = await this._c.dataPlaneRequest("mq", "GET", reqPath, {
23
+ params: { max_messages: maxMessages, wait_seconds: waitSeconds },
24
+ });
25
+ return data.items || [];
26
+ }
27
+ }
28
+
29
+ module.exports = { MqAPI };
package/src/secrets.js ADDED
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+
3
+ class SecretsAPI {
4
+ constructor(client) {
5
+ this._c = client;
6
+ }
7
+
8
+ /** Console JWT — list secret metadata. */
9
+ async list() {
10
+ this._c.requireConsole();
11
+ const data = await this._c.consoleRequest(
12
+ "GET",
13
+ `accounts/${this._c.accountId}/secrets`
14
+ );
15
+ return data.items || [];
16
+ }
17
+
18
+ /** Data plane — fetch secret payload by name (Access Key). */
19
+ async get(secretName) {
20
+ this._c.requireAccessKey();
21
+ const accountId = this._c.accountId;
22
+ const reqPath = `/${accountId}/secrets/${encodeURIComponent(secretName)}`;
23
+ return this._c.dataPlaneRequest("secrets", "GET", reqPath);
24
+ }
25
+ }
26
+
27
+ module.exports = { SecretsAPI };
package/src/signing.js ADDED
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+
5
+ function buildStringToSign({ method, path: reqPath, timestamp, accountId }) {
6
+ return `${String(method).toUpperCase()}\n${reqPath}\n${timestamp}\n${accountId}`;
7
+ }
8
+
9
+ function signRequestHeaders({
10
+ accessKeyId,
11
+ secret,
12
+ method,
13
+ path: reqPath,
14
+ accountId,
15
+ sessionToken,
16
+ }) {
17
+ const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
18
+ const stringToSign = buildStringToSign({
19
+ method,
20
+ path: reqPath,
21
+ timestamp,
22
+ accountId,
23
+ });
24
+ const signature = crypto.createHmac("sha256", secret).update(stringToSign).digest("hex");
25
+ const headers = {
26
+ "X-Homecloud-Access-Key-Id": accessKeyId,
27
+ "X-Homecloud-Date": timestamp,
28
+ "X-Homecloud-Signature": signature,
29
+ };
30
+ if (sessionToken) headers["X-Homecloud-Session-Token"] = sessionToken;
31
+ return headers;
32
+ }
33
+
34
+ function encodeObjectKeyPath(key) {
35
+ return key
36
+ .replace(/^\/+/, "")
37
+ .split("/")
38
+ .map((part) => encodeURIComponent(part))
39
+ .join("/");
40
+ }
41
+
42
+ function soObjectPaths(accountId, bucketName, objectKey) {
43
+ const key = String(objectKey || "").replace(/^\/+/, "");
44
+ const signPath = `/${accountId}/${bucketName}/objects/${key}`;
45
+ const urlPath = `/${accountId}/${bucketName}/objects/${encodeObjectKeyPath(key)}`;
46
+ return { signPath, urlPath };
47
+ }
48
+
49
+ module.exports = {
50
+ buildStringToSign,
51
+ signRequestHeaders,
52
+ encodeObjectKeyPath,
53
+ soObjectPaths,
54
+ };