kvdb-client 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 n0bisuke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ja.md ADDED
@@ -0,0 +1,147 @@
1
+ # kvdb-client
2
+
3
+ KVdb.io 用のシンプルな Node.js クライアントです。サードパーティーによる非公式 SDK です。
4
+
5
+ ## インストール
6
+
7
+ ```bash
8
+ npm install kvdb-client
9
+ ```
10
+
11
+ ## クイックスタート
12
+
13
+ ```js
14
+ import { KVdbClient } from "kvdb-client";
15
+
16
+ const kv = new KVdbClient({ bucket: "YOUR_BUCKET" });
17
+
18
+ await kv.set("myName", "n0bisuke");
19
+ const name = await kv.get("myName");
20
+ console.log(name);
21
+ ```
22
+
23
+ ## 認証
24
+
25
+ ```js
26
+ // Basic 認証(デフォルト)
27
+ const basic = new KVdbClient({ bucket: "YOUR_BUCKET", token: "supersecret" });
28
+
29
+ // Bearer トークン
30
+ const bearer = new KVdbClient({
31
+ bucket: "YOUR_BUCKET",
32
+ token: "ACCESS_TOKEN",
33
+ authType: "bearer"
34
+ });
35
+
36
+ // クエリ文字列でトークン指定
37
+ const query = new KVdbClient({
38
+ bucket: "YOUR_BUCKET",
39
+ token: "ACCESS_TOKEN",
40
+ authType: "query"
41
+ });
42
+ ```
43
+
44
+ ## サンプル
45
+
46
+ 実行可能なスクリプトは GitHub の `examples/` フォルダを参照してください。
47
+
48
+ ## 使い方
49
+
50
+ ### バケット作成
51
+
52
+ ```js
53
+ const bucket = await KVdbClient.createBucket("you@example.com");
54
+ ```
55
+
56
+ ### 値の保存・取得
57
+
58
+ ```js
59
+ await kv.set("myName", "n0bisuke");
60
+ const name = await kv.get("myName");
61
+ ```
62
+
63
+ ### JSON の保存・取得
64
+
65
+ ```js
66
+ await kv.set("myData", { name: "test", value: 123 }, { json: true });
67
+ const data = await kv.get("myData", { parseJson: true });
68
+ ```
69
+
70
+ ### キー一覧
71
+
72
+ ```js
73
+ const keys = await kv.list();
74
+ const items = await kv.list({ values: true, format: "json" });
75
+ ```
76
+
77
+ ### 更新(PATCH)
78
+
79
+ ```js
80
+ await kv.update("myName", "Sugawara");
81
+ ```
82
+
83
+ ### カウンター操作
84
+
85
+ ```js
86
+ await kv.increment("visits");
87
+ await kv.decrement("visits", 2);
88
+ ```
89
+
90
+ ### トランザクション
91
+
92
+ ```js
93
+ await kv.transaction([
94
+ { set: "users:email:new@example.com", value: "user 1" },
95
+ { delete: "users:email:old@example.com" }
96
+ ]);
97
+ ```
98
+
99
+ ### アクセストークン作成
100
+
101
+ ```js
102
+ const token = await kv.createAccessToken({
103
+ prefix: "user:123:",
104
+ permissions: "read,write",
105
+ ttl: 3600
106
+ });
107
+ ```
108
+
109
+ ### キー削除
110
+
111
+ ```js
112
+ await kv.delete("myName");
113
+ ```
114
+
115
+ ### バケット削除
116
+
117
+ ```js
118
+ await kv.deleteBucket();
119
+ ```
120
+
121
+ ## API
122
+
123
+ - `new KVdbClient({ bucket, token?, authType?, baseUrl? })`
124
+ - `KVdbClient.createBucket(email, { baseUrl?, secretKey?, writeKey?, readKey?, signingKey?, defaultTtl? })`
125
+ - `get(key, { parseJson? })`
126
+ - `set(key, value, { json?, contentType?, ttl? })`
127
+ - `update(key, value, { json?, contentType?, ttl? })`
128
+ - `increment(key, amount?, { ttl? })`
129
+ - `decrement(key, amount?, { ttl? })`
130
+ - `delete(key)`
131
+ - `list({ values?, format?, limit?, skip?, prefix?, reverse? })`
132
+ - `transaction(operations)`
133
+ - `createAccessToken({ prefix?, permissions?, ttl? })`
134
+ - `updateBucketPolicy({ secretKey?, writeKey?, readKey?, signingKey?, defaultTtl? })`
135
+ - `deleteBucket()`
136
+
137
+ ## 注意
138
+
139
+ - KVdb は公開される可能性があるため、重要な情報は保存しないでください。
140
+ - `token` は `curl -u 'token:'` と同じ形式で送信されます。
141
+ - KVdb に保存した情報がどのように扱われるかについて、SDK 開発者(n0bisuke)は一切把握していません。利用は自己責任とし、トラブル等について開発者は責任を負いません。
142
+
143
+ ## Language
144
+
145
+ - English: `README.md`
146
+ - Japanese (this file)
147
+ - English (GitHub): `https://github.com/n0bisuke/kvdb-client/blob/main/README.md`
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # kvdb-client
2
+
3
+ A simple Node.js client for KVdb.io. This is a third-party, unofficial SDK.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install kvdb-client
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```js
14
+ import { KVdbClient } from "kvdb-client";
15
+
16
+ const kv = new KVdbClient({ bucket: "YOUR_BUCKET" });
17
+
18
+ await kv.set("myName", "n0bisuke");
19
+ const name = await kv.get("myName");
20
+ console.log(name);
21
+ ```
22
+
23
+ ## Authentication
24
+
25
+ ```js
26
+ // Basic auth (default)
27
+ const basic = new KVdbClient({ bucket: "YOUR_BUCKET", token: "supersecret" });
28
+
29
+ // Bearer token
30
+ const bearer = new KVdbClient({
31
+ bucket: "YOUR_BUCKET",
32
+ token: "ACCESS_TOKEN",
33
+ authType: "bearer"
34
+ });
35
+
36
+ // Query string token
37
+ const query = new KVdbClient({
38
+ bucket: "YOUR_BUCKET",
39
+ token: "ACCESS_TOKEN",
40
+ authType: "query"
41
+ });
42
+ ```
43
+
44
+ ## Examples
45
+
46
+ See the `examples/` folder on GitHub for runnable scripts.
47
+
48
+ ## Usage
49
+
50
+ ### Create a bucket
51
+
52
+ ```js
53
+ const bucket = await KVdbClient.createBucket("you@example.com");
54
+ ```
55
+
56
+ ### Set / Get values
57
+
58
+ ```js
59
+ await kv.set("myName", "n0bisuke");
60
+ const name = await kv.get("myName");
61
+ ```
62
+
63
+ ### JSON values
64
+
65
+ ```js
66
+ await kv.set("myData", { name: "test", value: 123 }, { json: true });
67
+ const data = await kv.get("myData", { parseJson: true });
68
+ ```
69
+
70
+ ### List keys
71
+
72
+ ```js
73
+ const keys = await kv.list();
74
+ const items = await kv.list({ values: true, format: "json" });
75
+ ```
76
+
77
+ ### Update (PATCH)
78
+
79
+ ```js
80
+ await kv.update("myName", "Sugawara");
81
+ ```
82
+
83
+ ### Counters
84
+
85
+ ```js
86
+ await kv.increment("visits");
87
+ await kv.decrement("visits", 2);
88
+ ```
89
+
90
+ ### Transactions
91
+
92
+ ```js
93
+ await kv.transaction([
94
+ { set: "users:email:new@example.com", value: "user 1" },
95
+ { delete: "users:email:old@example.com" }
96
+ ]);
97
+ ```
98
+
99
+ ### Create access token
100
+
101
+ ```js
102
+ const token = await kv.createAccessToken({
103
+ prefix: "user:123:",
104
+ permissions: "read,write",
105
+ ttl: 3600
106
+ });
107
+ ```
108
+
109
+ ### Delete a key
110
+
111
+ ```js
112
+ await kv.delete("myName");
113
+ ```
114
+
115
+ ### Delete a bucket
116
+
117
+ ```js
118
+ await kv.deleteBucket();
119
+ ```
120
+
121
+ ## API
122
+
123
+ - `new KVdbClient({ bucket, token?, authType?, baseUrl? })`
124
+ - `KVdbClient.createBucket(email, { baseUrl?, secretKey?, writeKey?, readKey?, signingKey?, defaultTtl? })`
125
+ - `get(key, { parseJson? })`
126
+ - `set(key, value, { json?, contentType?, ttl? })`
127
+ - `update(key, value, { json?, contentType?, ttl? })`
128
+ - `increment(key, amount?, { ttl? })`
129
+ - `decrement(key, amount?, { ttl? })`
130
+ - `delete(key)`
131
+ - `list({ values?, format?, limit?, skip?, prefix?, reverse? })`
132
+ - `transaction(operations)`
133
+ - `createAccessToken({ prefix?, permissions?, ttl? })`
134
+ - `updateBucketPolicy({ secretKey?, writeKey?, readKey?, signingKey?, defaultTtl? })`
135
+ - `deleteBucket()`
136
+
137
+ ## Notes
138
+
139
+ - KVdb buckets can be public; avoid storing sensitive data.
140
+ - `token` for Basic auth is sent as `curl -u 'token:'`.
141
+ - The SDK author (n0bisuke) has no knowledge of how data stored in KVdb is handled; use at your own risk. The author is not responsible for any issues or damages.
142
+
143
+ ## Language
144
+
145
+ - English (this file)
146
+ - Japanese: `README.ja.md`
147
+ - Japanese (GitHub): `https://github.com/n0bisuke/kvdb-client/blob/main/README.ja.md`
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "kvdb-client",
3
+ "version": "0.1.0",
4
+ "description": "Simple Node.js client for kvdb.io",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "exports": {
9
+ ".": "./src/index.js"
10
+ },
11
+ "engines": {
12
+ "node": ">=18"
13
+ },
14
+ "keywords": [
15
+ "kvdb",
16
+ "kvdb.io",
17
+ "key-value",
18
+ "sdk",
19
+ "node"
20
+ ],
21
+ "license": "MIT"
22
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,70 @@
1
+ export type KVdbClientOptions = {
2
+ bucket: string;
3
+ token?: string;
4
+ authType?: "basic" | "bearer" | "query";
5
+ baseUrl?: string;
6
+ };
7
+
8
+ export type CreateBucketOptions = {
9
+ baseUrl?: string;
10
+ secretKey?: string;
11
+ writeKey?: string;
12
+ readKey?: string;
13
+ signingKey?: string;
14
+ defaultTtl?: number;
15
+ };
16
+
17
+ export type GetOptions = {
18
+ parseJson?: boolean;
19
+ };
20
+
21
+ export type SetOptions = {
22
+ json?: boolean;
23
+ contentType?: string;
24
+ ttl?: number;
25
+ };
26
+
27
+ export type ListOptions = {
28
+ values?: boolean;
29
+ format?: string;
30
+ limit?: number;
31
+ skip?: number;
32
+ prefix?: string;
33
+ reverse?: boolean;
34
+ };
35
+
36
+ export type TransactionOperation =
37
+ | { set: string; value: unknown; ttl?: number }
38
+ | { delete: string };
39
+
40
+ export type AccessTokenOptions = {
41
+ prefix?: string;
42
+ permissions?: string;
43
+ ttl?: number;
44
+ };
45
+
46
+ export type UpdateBucketPolicyOptions = {
47
+ secretKey?: string;
48
+ writeKey?: string;
49
+ readKey?: string;
50
+ signingKey?: string;
51
+ defaultTtl?: number;
52
+ };
53
+
54
+ export declare class KVdbClient {
55
+ constructor(options: KVdbClientOptions);
56
+ static createBucket(email: string, options?: CreateBucketOptions): Promise<string>;
57
+ get(key: string, options?: GetOptions): Promise<unknown>;
58
+ set(key: string, value: unknown, options?: SetOptions): Promise<unknown>;
59
+ update(key: string, value: unknown, options?: SetOptions): Promise<unknown>;
60
+ increment(key: string, amount?: number, options?: { ttl?: number }): Promise<unknown>;
61
+ decrement(key: string, amount?: number, options?: { ttl?: number }): Promise<unknown>;
62
+ delete(key: string): Promise<unknown>;
63
+ list(options?: ListOptions): Promise<unknown>;
64
+ transaction(operations: TransactionOperation[]): Promise<unknown>;
65
+ createAccessToken(options?: AccessTokenOptions): Promise<unknown>;
66
+ updateBucketPolicy(options?: UpdateBucketPolicyOptions): Promise<unknown>;
67
+ deleteBucket(): Promise<unknown>;
68
+ }
69
+
70
+ export default KVdbClient;
package/src/index.js ADDED
@@ -0,0 +1,271 @@
1
+ const DEFAULT_BASE_URL = "https://kvdb.io";
2
+
3
+ function normalizeBaseUrl(baseUrl) {
4
+ return baseUrl.replace(/\/$/, "");
5
+ }
6
+
7
+ function normalizeKey(key) {
8
+ if (typeof key !== "string" || key.trim() === "") {
9
+ throw new TypeError("key must be a non-empty string");
10
+ }
11
+ return key.replace(/^\/+/, "");
12
+ }
13
+
14
+ function buildAuthHeaders(authType, token) {
15
+ if (!token) return {};
16
+ if (authType === "bearer") {
17
+ return { Authorization: `Bearer ${token}` };
18
+ }
19
+ if (authType === "basic") {
20
+ const encoded = Buffer.from(`${token}:`, "utf8").toString("base64");
21
+ return { Authorization: `Basic ${encoded}` };
22
+ }
23
+ return {};
24
+ }
25
+
26
+ function appendAccessToken(url, authType, token) {
27
+ if (!token || authType !== "query") return url;
28
+ const hasQuery = url.includes("?");
29
+ const separator = hasQuery ? "&" : "?";
30
+ return `${url}${separator}access_token=${encodeURIComponent(token)}`;
31
+ }
32
+
33
+ async function readResponseBody(response) {
34
+ const contentType = response.headers.get("content-type") || "";
35
+ const text = await response.text();
36
+ if (contentType.includes("application/json")) {
37
+ try {
38
+ return JSON.parse(text);
39
+ } catch {
40
+ return text;
41
+ }
42
+ }
43
+ return text;
44
+ }
45
+
46
+ async function throwIfNotOk(response) {
47
+ if (response.ok) return response;
48
+ const body = await response.text();
49
+ const error = new Error(`KVdb request failed: ${response.status} ${response.statusText}`);
50
+ error.status = response.status;
51
+ error.body = body;
52
+ throw error;
53
+ }
54
+
55
+ export class KVdbClient {
56
+ constructor({ bucket, token, authType = "basic", baseUrl = DEFAULT_BASE_URL } = {}) {
57
+ if (!bucket) throw new TypeError("bucket is required");
58
+ this.bucket = bucket;
59
+ this.token = token;
60
+ this.authType = authType;
61
+ this.baseUrl = normalizeBaseUrl(baseUrl);
62
+ }
63
+
64
+ static async createBucket(email, {
65
+ baseUrl = DEFAULT_BASE_URL,
66
+ secretKey,
67
+ writeKey,
68
+ readKey,
69
+ signingKey,
70
+ defaultTtl
71
+ } = {}) {
72
+ if (!email) throw new TypeError("email is required");
73
+ const body = new URLSearchParams({ email });
74
+ if (secretKey) body.set("secret_key", secretKey);
75
+ if (writeKey) body.set("write_key", writeKey);
76
+ if (readKey) body.set("read_key", readKey);
77
+ if (signingKey) body.set("signing_key", signingKey);
78
+ if (typeof defaultTtl === "number") body.set("default_ttl", String(defaultTtl));
79
+ const response = await fetch(normalizeBaseUrl(baseUrl), {
80
+ method: "POST",
81
+ headers: { "content-type": "application/x-www-form-urlencoded" },
82
+ body
83
+ });
84
+ await throwIfNotOk(response);
85
+ return response.text();
86
+ }
87
+
88
+ _urlForKey(key, query = "") {
89
+ const safeKey = normalizeKey(key);
90
+ const suffix = query ? `?${query}` : "";
91
+ return `${this.baseUrl}/${this.bucket}/${safeKey}${suffix}`;
92
+ }
93
+
94
+ _urlForBucket(query = "") {
95
+ const suffix = query ? `?${query}` : "";
96
+ return `${this.baseUrl}/${this.bucket}${suffix}`;
97
+ }
98
+
99
+ _urlForBucketList(query = "") {
100
+ const suffix = query ? `/?${query}` : "/";
101
+ return `${this.baseUrl}/${this.bucket}${suffix}`;
102
+ }
103
+
104
+ _buildUrl(url) {
105
+ return appendAccessToken(url, this.authType, this.token);
106
+ }
107
+
108
+ _authHeaders() {
109
+ return buildAuthHeaders(this.authType, this.token);
110
+ }
111
+
112
+ async _request({ method, url, headers, body }) {
113
+ const response = await fetch(this._buildUrl(url), {
114
+ method,
115
+ headers: {
116
+ ...headers,
117
+ ...this._authHeaders()
118
+ },
119
+ body
120
+ });
121
+ await throwIfNotOk(response);
122
+ return readResponseBody(response);
123
+ }
124
+
125
+ async get(key, { parseJson } = {}) {
126
+ const body = await this._request({
127
+ method: "GET",
128
+ url: this._urlForKey(key)
129
+ });
130
+ if (parseJson && typeof body === "string") {
131
+ try {
132
+ return JSON.parse(body);
133
+ } catch {
134
+ return body;
135
+ }
136
+ }
137
+ return body;
138
+ }
139
+
140
+ async set(key, value, { json, contentType, ttl } = {}) {
141
+ const { body, headers } = this._serializeValue(value, { json, contentType });
142
+ const query = typeof ttl === "number" ? `ttl=${ttl}` : "";
143
+ return this._request({
144
+ method: "POST",
145
+ url: this._urlForKey(key, query),
146
+ headers,
147
+ body
148
+ });
149
+ }
150
+
151
+ async update(key, value, { json, contentType, ttl } = {}) {
152
+ const { body, headers } = this._serializeValue(value, { json, contentType });
153
+ const query = typeof ttl === "number" ? `ttl=${ttl}` : "";
154
+ return this._request({
155
+ method: "PATCH",
156
+ url: this._urlForKey(key, query),
157
+ headers,
158
+ body
159
+ });
160
+ }
161
+
162
+ async increment(key, amount = 1, { ttl } = {}) {
163
+ if (typeof amount !== "number") throw new TypeError("amount must be a number");
164
+ const sign = amount >= 0 ? "+" : "";
165
+ const query = typeof ttl === "number" ? `ttl=${ttl}` : "";
166
+ return this._request({
167
+ method: "PATCH",
168
+ url: this._urlForKey(key, query),
169
+ headers: { "content-type": "text/plain" },
170
+ body: `${sign}${amount}`
171
+ });
172
+ }
173
+
174
+ async decrement(key, amount = 1, { ttl } = {}) {
175
+ return this.increment(key, -Math.abs(amount), { ttl });
176
+ }
177
+
178
+ async delete(key) {
179
+ return this._request({
180
+ method: "DELETE",
181
+ url: this._urlForKey(key)
182
+ });
183
+ }
184
+
185
+ async list({
186
+ values = false,
187
+ format = "json",
188
+ limit,
189
+ skip,
190
+ prefix,
191
+ reverse
192
+ } = {}) {
193
+ const query = new URLSearchParams();
194
+ if (values) query.set("values", "true");
195
+ if (format) query.set("format", format);
196
+ if (typeof limit === "number") query.set("limit", String(limit));
197
+ if (typeof skip === "number") query.set("skip", String(skip));
198
+ if (prefix) query.set("prefix", prefix);
199
+ if (reverse) query.set("reverse", "true");
200
+ const body = await this._request({
201
+ method: "GET",
202
+ url: this._urlForBucketList(query.toString())
203
+ });
204
+ if (format === "json" && typeof body === "string") {
205
+ try {
206
+ return JSON.parse(body);
207
+ } catch {
208
+ return body;
209
+ }
210
+ }
211
+ return body;
212
+ }
213
+
214
+ async transaction(operations) {
215
+ if (!Array.isArray(operations)) throw new TypeError("operations must be an array");
216
+ const body = JSON.stringify({ txn: operations });
217
+ return this._request({
218
+ method: "POST",
219
+ url: this._urlForBucket(),
220
+ headers: { "content-type": "application/json" },
221
+ body
222
+ });
223
+ }
224
+
225
+ async createAccessToken({ prefix, permissions, ttl } = {}) {
226
+ const body = new URLSearchParams();
227
+ if (prefix) body.set("prefix", prefix);
228
+ if (permissions) body.set("permissions", permissions);
229
+ if (typeof ttl === "number") body.set("ttl", String(ttl));
230
+ return this._request({
231
+ method: "POST",
232
+ url: `${this.baseUrl}/${this.bucket}/tokens/`,
233
+ headers: { "content-type": "application/x-www-form-urlencoded" },
234
+ body
235
+ });
236
+ }
237
+
238
+ async updateBucketPolicy({ secretKey, writeKey, readKey, signingKey, defaultTtl } = {}) {
239
+ const body = new URLSearchParams();
240
+ if (secretKey) body.set("secret_key", secretKey);
241
+ if (writeKey) body.set("write_key", writeKey);
242
+ if (readKey) body.set("read_key", readKey);
243
+ if (signingKey) body.set("signing_key", signingKey);
244
+ if (typeof defaultTtl === "number") body.set("default_ttl", String(defaultTtl));
245
+ return this._request({
246
+ method: "PATCH",
247
+ url: this._urlForBucket(),
248
+ headers: { "content-type": "application/x-www-form-urlencoded" },
249
+ body
250
+ });
251
+ }
252
+
253
+ async deleteBucket() {
254
+ return this._request({
255
+ method: "DELETE",
256
+ url: `${this.baseUrl}/${this.bucket}/`
257
+ });
258
+ }
259
+
260
+ _serializeValue(value, { json, contentType } = {}) {
261
+ const headers = {};
262
+ if (contentType) headers["content-type"] = contentType;
263
+ if (json || value instanceof Object && !(value instanceof Buffer) && !(value instanceof ArrayBuffer) && !(value instanceof Uint8Array)) {
264
+ headers["content-type"] = headers["content-type"] || "application/json";
265
+ return { body: JSON.stringify(value), headers };
266
+ }
267
+ return { body: typeof value === "string" ? value : String(value), headers };
268
+ }
269
+ }
270
+
271
+ export default KVdbClient;