@asaidimu/hestia 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/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # @asaidimu/hestia
2
+
3
+ TypeScript client SDK for the [Hestia](https://github.com/asaidimu/hestia) platform — a lightweight, modular backend framework.
4
+
5
+ ## Features
6
+
7
+ - **Auth** — login, register, refresh, logout, session management with auto-refresh on 401
8
+ - **Collections** — generic CRUD over any named collection via `HestiaCollection<T>`
9
+ - **API Keys** — create, list, get, update, rotate, delete API keys
10
+ - **Policies** — manage policy operations, rules, validation, and reload
11
+ - **Audit Logs** — query and filter audit entries
12
+ - **Blobs** — upload, download, list, and manage blob storage namespaces
13
+ - **Capabilities** — query available capabilities
14
+ - **Reactive Paging** — observable paginated data with resize/navigate/subscribe
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @asaidimu/hestia
20
+ # or
21
+ bun add @asaidimu/hestia
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```ts
27
+ import { HestiaClient } from "@asaidimu/hestia"
28
+
29
+ const api = new HestiaClient({ baseUrl: "http://localhost:8070" })
30
+
31
+ // Login as admin
32
+ const { token, user } = await api.auth.login("admin@test.local", "password123")
33
+ console.log("Logged in as", user.email)
34
+
35
+ // List users
36
+ const { data: users } = await api.users.find()
37
+ console.log(`Found ${users.length} users`)
38
+
39
+ // Create an API key
40
+ const key = await api.keys.create({ name: "CI/CD Deploy Key" })
41
+ console.log("API key:", key.key)
42
+
43
+ // Reactive pager
44
+ const pager = api.users.page()
45
+ pager.subscribe((page) => console.log("Page:", page.page.number))
46
+ await pager.resize(25, 1)
47
+ ```
48
+
49
+ ## API
50
+
51
+ ### `HestiaClient`
52
+
53
+ The main entry point. Construct with a `baseUrl` pointing to a Hestia server.
54
+
55
+ | Property | Type | Description |
56
+ |---|---|---|
57
+ | `auth` | `HestiaAuth` | Authentication (login, register, refresh, logout) |
58
+ | `users` | `HestiaUsers` | User management |
59
+ | `keys` | `HestiaKeyStore` | API key management |
60
+ | `policies` | `HestiaPolicies` | Policy operations and rules |
61
+ | `logs` | `HestiaLogs` | Audit log queries |
62
+ | `collections` | `HestiaCollections` | Generic collection metadata |
63
+ | `blobs` | `HestiaBlobClient` | Blob (file) storage |
64
+ | `capabilities` | `HestiaCapabilities` | Available capabilities |
65
+ | `client` | `HestiaNetworkClient` | Low-level HTTP client |
66
+
67
+ ### Auth
68
+
69
+ ```ts
70
+ // Login — stores tokens and identity in the reactive store
71
+ const result = await api.auth.login(email, password)
72
+
73
+ // Register a new user (requires admin session)
74
+ const user = await api.auth.register(email, password, name)
75
+
76
+ // Refresh tokens
77
+ const pair = await api.auth.refresh(refreshToken)
78
+
79
+ // Logout — clears stored tokens
80
+ await api.auth.logout()
81
+
82
+ // Health check (public endpoint, no auth required)
83
+ const health = await api.auth.health()
84
+ ```
85
+
86
+ ### Collections
87
+
88
+ ```ts
89
+ // Get a typed collection handle
90
+ const docs = api.collection<MyType>("my_collection")
91
+
92
+ // CRUD operations
93
+ const list = await docs.find(query?)
94
+ const item = await docs.read(id)
95
+ const created = await docs.create(document)
96
+ const updated = await docs.update(id, partial)
97
+ await docs.delete(id)
98
+ ```
99
+
100
+ ### Reactive Paging
101
+
102
+ ```ts
103
+ const pager = api.users.page()
104
+
105
+ // Subscribe to page changes
106
+ const unsub = pager.subscribe((page) => render(page.data))
107
+
108
+ // Navigate, resize, sort, filter
109
+ await pager.navigate(2)
110
+ await pager.resize(50, 1)
111
+ await pager.sort({ field: "email", order: "asc" })
112
+ await pager.filter({ email: { $like: "%@example.com" } })
113
+
114
+ // Manual refresh
115
+ await pager.refresh()
116
+
117
+ // Cleanup
118
+ unsub()
119
+ ```
120
+
121
+ ### Auto-Refresh
122
+
123
+ The client transparently refreshes expired JWT access tokens on 401 responses. Concurrent requests that receive 401 are deduplicated — only one refresh call is made. API-key requests (with `X-API-Key` header) are excluded from auto-refresh.
124
+
125
+
126
+ ## Development
127
+
128
+ ```bash
129
+ # Install dependencies
130
+ bun install
131
+
132
+ # Run tests (requires a running test-server on :8070)
133
+ ./test-server &
134
+ bun test
135
+ ```
136
+
137
+ ## License
138
+
139
+ MIT
package/auth/store.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { HestiaNetworkClient, IdentityProvider } from "../core/client";
2
+ import type { LoginResult, ServerHealth, TokenPair } from "./types";
3
+
4
+ export class HestiaAuth {
5
+ constructor(
6
+ private client: HestiaNetworkClient,
7
+ private provider: IdentityProvider,
8
+ ) {}
9
+
10
+ async health(): Promise<ServerHealth> {
11
+ const res = await this.client.get<{ data: ServerHealth }>("/system/core/health");
12
+ return res.data!.data;
13
+ }
14
+
15
+ async login(email: string, password: string): Promise<LoginResult> {
16
+ const res = await this.client.post<{ data: LoginResult }>(
17
+ "/system/auth/session",
18
+ { email, password },
19
+ );
20
+ const result = res.data!.data;
21
+ this.provider.setIdentity(result.user);
22
+ this.client.storeTokens(result.token.access, result.token.refresh);
23
+ return result;
24
+ }
25
+
26
+ async register(
27
+ email: string,
28
+ password: string,
29
+ name: string,
30
+ ): Promise<{ _id_: string; email: string; name: string }> {
31
+ const res = await this.client.post<{
32
+ data: { _id_: string; email: string; name: string, scopes:string };
33
+ }>("/system/auth/user", { email, password, name });
34
+ return res.data!.data;
35
+ }
36
+
37
+ async refresh(refreshToken?: string): Promise<TokenPair> {
38
+ const body = refreshToken ? { refresh_token: refreshToken } : {};
39
+ const res = await this.client.patch<{ data: { token: TokenPair } }>(
40
+ "/system/auth/session",
41
+ body,
42
+ );
43
+ return res.data!.data.token;
44
+ }
45
+
46
+ async logout(): Promise<void> {
47
+ const refresh = this.provider.token("refresh");
48
+ const body = refresh ? { refresh_token: refresh } : {};
49
+ await this.client.delete("/system/auth/session", body);
50
+ await this.provider.clear();
51
+ }
52
+
53
+ async requestPasswordReset(email: string): Promise<void> {
54
+ await this.client.post("/system/auth/password", { email });
55
+ }
56
+
57
+ async confirmPasswordReset(
58
+ resetToken: string,
59
+ password: string,
60
+ ): Promise<void> {
61
+ await this.client.patch(
62
+ "/system/auth/password",
63
+ { password, token:resetToken },
64
+ { headers: { Authorization: `Bearer ${resetToken}` } },
65
+ );
66
+ }
67
+
68
+ async bootstrap(key: string, password: string, email: string): Promise<void> {
69
+ await this.client.patch(
70
+ "/system/auth/bootstrap",
71
+ { password, email },
72
+ { headers: { "X-API-Key": key } },
73
+ );
74
+ }
75
+ }
package/auth/types.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { UserIdentity } from "../system/identity/types"
2
+
3
+ export interface TokenPair {
4
+ access: string
5
+ refresh: string
6
+ type: string
7
+ validity: number
8
+ }
9
+
10
+ export interface LoginResult {
11
+ token: TokenPair
12
+ user: UserIdentity
13
+ }
14
+
15
+ export interface ServerHealth {
16
+ bootstrapped: boolean
17
+ ok: boolean
18
+ }
19
+
20
+ export interface LoginRequest {
21
+ email: string
22
+ password: string
23
+ }
24
+
25
+ export interface RegisterRequest {
26
+ email: string
27
+ password: string
28
+ name: string
29
+ }
30
+
31
+ export interface RefreshRequest {
32
+ refresh_token: string
33
+ }
34
+
35
+ export interface BootstrapPasswordRequest {
36
+ password: string
37
+ email: string
38
+ }
package/blobs/store.ts ADDED
@@ -0,0 +1,208 @@
1
+ import type { QueryDSL } from "@asaidimu/query";
2
+ import { ReactiveDataStore } from "@asaidimu/utils-store";
3
+ import { HestiaNetworkClient } from "../core/client";
4
+ import { createPagedController } from "../core/pager";
5
+ import type { Page, PagedData } from "../core/types";
6
+ import type {
7
+ BlobDocument,
8
+ BlobMeta,
9
+ CreateNamespaceRequest,
10
+ ListBlobsRequest,
11
+ NamespaceInfo,
12
+ } from "./types";
13
+
14
+ function asDoc(b: BlobMeta): BlobDocument {
15
+ return {
16
+ _id_: b.key,
17
+ _metadata_: {
18
+ checksum: "",
19
+ created: b.created_at,
20
+ updated: b.updated_at ?? b.created_at,
21
+ version: 1,
22
+ },
23
+ ...b,
24
+ };
25
+ }
26
+
27
+ function pageMeta<T extends Record<string,any>>(items: T[]): Page<T>["page"] {
28
+ return {
29
+ number: 1,
30
+ size: items.length,
31
+ count: items.length,
32
+ total: items.length,
33
+ pages: 1,
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Namespace-scoped blob facade shaped like HestiaCollection.
39
+ * Wraps blob CRUD behind a DocumentStore-compatible interface
40
+ * so you can use DataTable + PagedData.
41
+ */
42
+ export class BlobNamespace {
43
+ private pagerOptions = {};
44
+ private pager: PagedData<BlobMeta>;
45
+ private prefixFilter = "";
46
+
47
+ constructor(
48
+ private client: HestiaNetworkClient,
49
+ private ns: string,
50
+ ) {
51
+ this.pager = createPagedController<BlobMeta>(
52
+ `blobs_${ns}`,
53
+ new ReactiveDataStore<any>({}),
54
+ this.pagerOptions,
55
+ (query) => this.find(query),
56
+ );
57
+ }
58
+
59
+ name() {
60
+ return this.ns;
61
+ }
62
+
63
+ setPrefix(prefix: string) {
64
+ this.prefixFilter = prefix;
65
+ }
66
+
67
+ private basePath() {
68
+ return `/system/blobs/blob/${encodeURIComponent(this.ns)}`;
69
+ }
70
+
71
+ async find(query?: QueryDSL<BlobMeta>): Promise<Page<BlobMeta>> {
72
+ const prefix = this.prefixFilter || (query as any)?.prefix || "";
73
+ const limit =
74
+ (query as any)?.limit ?? query?.pagination?.limit ?? 0;
75
+
76
+ const req: ListBlobsRequest = {};
77
+ if (prefix) req.prefix = prefix;
78
+ if (limit) req.limit = limit;
79
+
80
+ const res = await this.client.post<{
81
+ data: { blobs: BlobMeta[] };
82
+ }>(`${this.basePath()}/query`, req);
83
+
84
+ const items = res.data?.data?.blobs ?? [];
85
+ return { data: items.map(asDoc), loading: false, page: pageMeta(items), error: undefined };
86
+ }
87
+
88
+ async head(key: string): Promise<BlobDocument | undefined> {
89
+ try {
90
+ const res = await this.client.get<{ data: BlobMeta }>(
91
+ `${this.basePath()}/${encodeURIComponent(key)}`,
92
+ );
93
+ if (!res.data?.data) return undefined;
94
+ return asDoc(res.data.data);
95
+ } catch (err: any) {
96
+ if (
97
+ err?.code === "SYNC-001-NF" ||
98
+ (err?.code === "INTERNAL_ERROR" &&
99
+ typeof err?.message === "string" &&
100
+ err.message.includes("not found"))
101
+ )
102
+ return undefined;
103
+ throw err;
104
+ }
105
+ }
106
+
107
+ async upload(
108
+ key: string,
109
+ data: Blob,
110
+ contentType?: string,
111
+ ): Promise<BlobDocument> {
112
+ const headers: Record<string, string> = {};
113
+ const ct = contentType || data.type;
114
+ if (ct) headers["Content-Type"] = ct;
115
+
116
+ const res = await this.client.post<{ data: BlobMeta }>(
117
+ `${this.basePath()}/${encodeURIComponent(key)}`,
118
+ data,
119
+ { headers, bodyType: "blob" },
120
+ );
121
+ return asDoc(res.data!.data);
122
+ }
123
+
124
+ async download(
125
+ key: string,
126
+ ): Promise<{ data: Blob; contentType: string }> {
127
+ const res = await this.client.get<Blob>(
128
+ `${this.basePath()}/${encodeURIComponent(key)}`,
129
+ { responseType: "blob" },
130
+ );
131
+ const blob = res.data!;
132
+ return { data: blob, contentType: blob.type };
133
+ }
134
+
135
+ async updateMetadata(key: string, custom: Record<string, any>): Promise<BlobMeta> {
136
+ const res = await this.client.patch<{ data: BlobMeta }>(
137
+ `${this.basePath()}/${encodeURIComponent(key)}`,
138
+ { custom },
139
+ );
140
+ return res.data!.data;
141
+ }
142
+
143
+ async delete(key: string): Promise<void> {
144
+ await this.client.delete(
145
+ `${this.basePath()}/${encodeURIComponent(key)}`,
146
+ );
147
+ }
148
+
149
+ async list(): Promise<BlobMeta[]> {
150
+ const res = await this.client.post<{
151
+ data: { blobs: BlobMeta[] };
152
+ }>(`${this.basePath()}/query`, {});
153
+ return res.data?.data?.blobs ?? [];
154
+ }
155
+
156
+ page(): PagedData<BlobMeta> {
157
+ return this.pager;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Top-level blob client. Entry point for all blob operations.
163
+ *
164
+ * Usage:
165
+ * client.blobs.listNamespaces()
166
+ * const ns = client.blobs.namespace("my-bucket")
167
+ * ns.find({ prefix: "images/" })
168
+ * ns.upload("logo.png", file)
169
+ * const { data } = await ns.download("logo.png")
170
+ */
171
+ export class HestiaBlobClient {
172
+ constructor(private client: HestiaNetworkClient) {}
173
+
174
+ private nsBase = "/system/blobs";
175
+
176
+ // ── Namespace operations ──────────────────────────────────────────────
177
+
178
+ async namespaces(): Promise<NamespaceInfo[]> {
179
+ const res = await this.client.post<{
180
+ data: { namespaces: NamespaceInfo[] };
181
+ }>(`${this.nsBase}/namespace/query`);
182
+ return res.data?.data?.namespaces ?? [];
183
+ }
184
+
185
+ async createNamespace(data: CreateNamespaceRequest): Promise<NamespaceInfo> {
186
+ const res = await this.client.post<{ data: NamespaceInfo }>(
187
+ `${this.nsBase}/namespace`,
188
+ data,
189
+ );
190
+ return res.data!.data;
191
+ }
192
+
193
+ async deleteNamespace(ns: string): Promise<void> {
194
+ await this.client.delete(
195
+ `${this.nsBase}/namespace/${encodeURIComponent(ns)}`,
196
+ );
197
+ }
198
+
199
+
200
+ blob(namespace: string, key:string) {
201
+ return `${this.client.base()}${this.nsBase}/blob/${encodeURIComponent(namespace)}/${encodeURIComponent(key)}`
202
+ }
203
+ // ── Namespace-scoped facade ───────────────────────────────────────────
204
+
205
+ namespace(ns: string): BlobNamespace {
206
+ return new BlobNamespace(this.client, ns);
207
+ }
208
+ }
package/blobs/types.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { Document } from "../core/types"
2
+
3
+ export interface NamespaceInfo {
4
+ id: string
5
+ display_name: string
6
+ }
7
+
8
+ export interface BlobMeta {
9
+ key: string
10
+ namespace_id: string
11
+ content_type: string
12
+ size: number
13
+ created_at: string
14
+ updated_at?: string
15
+ custom?: Record<string, any>
16
+ }
17
+
18
+ export type BlobDocument = Document<BlobMeta>
19
+
20
+ export interface ListBlobsRequest {
21
+ prefix?: string
22
+ limit?: number
23
+ }
24
+
25
+ export interface CreateNamespaceRequest {
26
+ display_name?: string
27
+ ns?: string
28
+ }
@@ -0,0 +1,59 @@
1
+ import { HestiaNetworkClient } from "../core/client"
2
+ import { HestiaCollection } from "../core/collection"
3
+ import type { Document } from "../core/types"
4
+ import type { CollectionMeta } from "./types"
5
+
6
+ export class HestiaCollections {
7
+ constructor(private client: HestiaNetworkClient) {}
8
+
9
+ async list(): Promise<{ collections: CollectionMeta[]; total: number }> {
10
+ const res = await this.client.get<{
11
+ data: { name: string; schema: any; created: string; updated: string }[]
12
+ }>("/system/collections/collection")
13
+ const items = res.data?.data ?? []
14
+ return {
15
+ collections: items.map((i) => ({
16
+ name: i.name,
17
+ schema: i.schema,
18
+ created: i.created,
19
+ updated: i.updated,
20
+ })),
21
+ total: items.length,
22
+ }
23
+ }
24
+
25
+ async read(name: string): Promise<CollectionMeta | undefined> {
26
+ try {
27
+ const res = await this.client.get<{ data: { name: string; schema: any; created: string; updated: string } }>(
28
+ `/system/collections/collection/${encodeURIComponent(name)}`,
29
+ )
30
+ if (!res.data) return undefined
31
+ return res.data.data
32
+ } catch (err: any) {
33
+ if (err?.code === "SYNC-001-NF") return undefined
34
+ throw err
35
+ }
36
+ }
37
+
38
+ async create(schema: any): Promise<Document<{ schema: any }>> {
39
+ const res = await this.client.post<{ data: Document<{ schema: any }> }>("/system/collections/collection", schema)
40
+ return res.data!.data
41
+ }
42
+
43
+ async update(name: string, schema: any): Promise<Document<{ schema: any }>> {
44
+ throw new Error("Method not implemented")
45
+ // const res = await this.client.patch<{ data: Document<{ schema: any }> }>(
46
+ // `/api/admin/collections/${encodeURIComponent(name)}`,
47
+ // schema,
48
+ // )
49
+ // return res.data!.data
50
+ }
51
+
52
+ async delete(name: string): Promise<void> {
53
+ await this.client.delete(`/system/collections/collection/${encodeURIComponent(name)}`)
54
+ }
55
+
56
+ documents<T extends Record<string, any>>(collectionName: string): HestiaCollection<T> {
57
+ return new HestiaCollection<T>(this.client,collectionName)
58
+ }
59
+ }
@@ -0,0 +1,11 @@
1
+ import { SchemaDefinition } from "@asaidimu/utils-schema"
2
+ import type { Document } from "../core/types"
3
+
4
+ export interface CollectionMeta {
5
+ name: string
6
+ schema: SchemaDefinition
7
+ created: string
8
+ updated: string
9
+ }
10
+
11
+ export type CollectionDocument = Document<{ schema: any }>
package/container.ts ADDED
@@ -0,0 +1,95 @@
1
+ import { ArtifactContainer } from "@asaidimu/utils-artifacts";
2
+ import type { SimplePersistence } from "@asaidimu/utils-persistence";
3
+ import { ReactiveDataStore } from "@asaidimu/utils-store";
4
+ import { HestiaAuth } from "./auth/store";
5
+ import { HestiaCollections } from "./collections/store";
6
+ import {
7
+ HestiaNetworkClient,
8
+ type IdentityProvider,
9
+ } from "./core/client";
10
+ import { HestiaKeyStore } from "./system/api-keys/store";
11
+ import { HestiaUsers } from "./system/identity/store";
12
+ import { UserIdentity } from "./system/identity/types";
13
+ import { HestiaLogs } from "./system/logs/store";
14
+ import { HestiaPolicies } from "./system/policies/store";
15
+ import { HestiaBlobClient } from "./blobs/store";
16
+ import { HestiaCapabilities } from "./system/capabilities/store";
17
+
18
+ export interface HestiaConfig {
19
+ baseUrl: string;
20
+ persistence?: SimplePersistence<AuthState>;
21
+ }
22
+
23
+ interface AuthState {
24
+ access: string | null;
25
+ refresh: string | null;
26
+ identity: UserIdentity | null;
27
+ }
28
+
29
+ type Registry = Record<string, any>;
30
+
31
+ export class HestiaClient {
32
+ readonly store: ReactiveDataStore<AuthState>;
33
+ readonly container: ArtifactContainer<Registry, any>;
34
+ readonly client: HestiaNetworkClient;
35
+ readonly auth: HestiaAuth;
36
+ readonly users: HestiaUsers;
37
+ readonly keys: HestiaKeyStore;
38
+ readonly policies: HestiaPolicies;
39
+ readonly logs: HestiaLogs;
40
+ readonly collections: HestiaCollections;
41
+ readonly blobs: HestiaBlobClient;
42
+ readonly capabilities: HestiaCapabilities
43
+ private tokenProvider: IdentityProvider;
44
+
45
+ private onAuthStateChanged?: () => void;
46
+
47
+ constructor(config: HestiaConfig) {
48
+ this.store = new ReactiveDataStore<AuthState>(
49
+ { access: null, refresh: null, identity: null },
50
+ config.persistence,
51
+ );
52
+
53
+ this.container = new ArtifactContainer<Registry, any>(this.store);
54
+
55
+ const tokenProvider: IdentityProvider = {
56
+ identity: () => this.store.get().identity,
57
+ token: (key: "access" | "refresh") => this.store.get()[key],
58
+ setTokens: async (access: string, refresh: string) =>
59
+ void (await this.store.set({ access, refresh })),
60
+ setIdentity: async (identity) =>
61
+ void (await this.store.set({ identity })),
62
+ clear: async () =>
63
+ void (await this.store.set({ access: null, refresh: null })),
64
+ };
65
+
66
+ this.tokenProvider = tokenProvider;
67
+ this.client = new HestiaNetworkClient(config.baseUrl, tokenProvider, () =>
68
+ this.onAuthStateChanged?.(),
69
+ );
70
+
71
+ this.auth = new HestiaAuth(this.client, tokenProvider);
72
+ this.users = new HestiaUsers(this.client);
73
+ this.keys = new HestiaKeyStore(this.client,);
74
+ this.policies = new HestiaPolicies(this.client);
75
+ this.logs = new HestiaLogs(
76
+ this.client,
77
+ config.baseUrl,
78
+ );
79
+ this.collections = new HestiaCollections(this.client);
80
+ this.blobs = new HestiaBlobClient(this.client);
81
+ this.capabilities = new HestiaCapabilities(this.client)
82
+ }
83
+
84
+ onAuthStateChange(callback: () => void) {
85
+ this.onAuthStateChanged = callback;
86
+ }
87
+
88
+ authenticated(): boolean {
89
+ return this.tokenProvider.token("access") !== null;
90
+ }
91
+
92
+ collection<T extends Record<string, any>>(name: string) {
93
+ return this.collections.documents<T>(name);
94
+ }
95
+ }