@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/core/types.ts ADDED
@@ -0,0 +1,279 @@
1
+ import { QueryFilter, SortConfiguration } from "@asaidimu/query";
2
+
3
+ // Internal metadata managed by Anansi
4
+ interface DocumentMetadata {
5
+ checksum: string; // Integrity hash of the document
6
+ created: string; // Timestamp (often as a numeric string / nanoseconds)
7
+ updated: string; // Timestamp (often as a numeric string / nanoseconds)
8
+ version: number; // Optimistic locking version (increments on each write)
9
+ }
10
+
11
+ // The base envelope applied to every single document in the system
12
+ interface BaseDocument {
13
+ _id_: string;
14
+ _metadata_: DocumentMetadata;
15
+ }
16
+
17
+ // Generic Document type: base envelope + your custom properties T
18
+ export type Document<T extends Record<string, any>> = BaseDocument & T;
19
+
20
+ /**
21
+ * Provides comprehensive pagination information for a collection of records.
22
+ */
23
+ export type PaginationInfo = {
24
+ /** The current page number (1-based). */
25
+ number: number;
26
+
27
+ /** The maximum number of items requested per page. */
28
+ size: number;
29
+
30
+ /** The number of items in the current page */
31
+ count: number;
32
+
33
+ /** The total count of all items across all pages. */
34
+ total: number;
35
+
36
+ /** The total number of available pages. */
37
+ pages: number;
38
+ };
39
+
40
+ /**
41
+ * Represents a single paginated response containing a list of records and pagination details.
42
+ * @template T The type of the records in the page.
43
+ */
44
+ export interface Page<T extends Record<string, any>> {
45
+ /** The array of records for the current page. */
46
+ data: Document<T>[];
47
+
48
+ /** Indicates if the data query is currently loading. */
49
+ loading: boolean;
50
+
51
+ /** An error object if the query failed, otherwise `undefined`. */
52
+ error?: any | undefined;
53
+
54
+ /** Pagination metadata providing details about the current page and total collection. */
55
+ page: PaginationInfo;
56
+ }
57
+
58
+ export interface PagerRefreshOptions {
59
+ sort?: SortConfiguration | SortConfiguration[];
60
+ filter?: QueryFilter;
61
+ delay?: number;
62
+ size?: number;
63
+ page?: number;
64
+ }
65
+ /**
66
+ * Represents the complete state and control mechanisms for consuming paginated data.
67
+ * @template T The type of the records in the page (`TableRowData` or an extension).
68
+ */
69
+ export interface PagedData<T extends Record<string, any>> {
70
+ id: () => string
71
+
72
+ /** The current paginated data, or `undefined` if not found or still loading. */
73
+ page: () => Page<T>;
74
+
75
+ /** Function to fetch a specific page of data.
76
+ * @param page The 1-based page number to fetch.
77
+ */
78
+ navigate: (page: number) => Promise<void>;
79
+
80
+ sort: (sort: SortConfiguration<T> | SortConfiguration<T>[]) => Promise<void>;
81
+ filter: (filter?: QueryFilter<T>) => Promise<void>;
82
+
83
+ /**
84
+ * Function to change the page size of the data
85
+ */
86
+ resize: (size: number, page: number) => Promise<void>;
87
+
88
+ /**
89
+ * Function to force a refresh of the current page data, optionally with a delay.
90
+ * @param delay Optional delay in milliseconds before the refresh operation starts.
91
+ */
92
+ refresh: (opts?: PagerRefreshOptions) => Promise<void>;
93
+
94
+ /**
95
+ * Subscribes to page changes.
96
+ * The callback is invoked immediately with the current page, and then on every
97
+ * subsequent change (navigation, resize, SSE patch, refresh).
98
+ *
99
+ * @param listener A function that receives the current Page<T>.
100
+ * @returns An unsubscribe function.
101
+ */
102
+ subscribe: (listener: (page: Page<T>) => void) => () => void;
103
+
104
+ /**
105
+ * Invalidates the pagination controller, cleaning up any cached resources
106
+ * (e.g., store subscriptions, in‑flight requests) and removing internal state.
107
+ * After calling `invalidate()`, the controller should no longer be used.
108
+ *
109
+ * This is useful when the component using the pager is unmounted or when
110
+ * you need to reset the pagination state completely.
111
+ */
112
+ invalidate: () => void;
113
+ }
114
+
115
+ /**
116
+ * Represents an event emitted by the store, typically for notifications or state changes.
117
+ */
118
+ export interface StoreEvent {
119
+ /** The scope of the event, indicating its type and context. Custom scopes are allowed. */
120
+ scope: string;
121
+ /** Optional payload carrying data related to the event. */
122
+ payload?: any;
123
+ }
124
+
125
+ /**
126
+ * Defines the core interface for interacting with a remote data store.
127
+ * @template T The type of the records managed by the store, extending Record.
128
+ * @template TFindOptions Options for the find operation.
129
+ * @template TReadOptions Options for the read operation.
130
+ * @template TListOptions Options for the list operation.
131
+ * @template TPageOptions Options for the paging operation.
132
+ * @template TDeleteOptions Options for the delete operation.
133
+ * @template TUpdateOptions Options for the update operation.
134
+ * @template TCreateOptions Options for the create operation.
135
+ * @template TUploadOptions Options for the upload operation.
136
+ * @template TStreamOptions Options for the stream operation.
137
+ */
138
+ export interface DocumentStore<
139
+ T extends Record<string, any>,
140
+ TFindOptions = Record<string, unknown>,
141
+ TReadOptions = Record<string, unknown>,
142
+ TListOptions = Record<string, unknown>,
143
+ TPageOptions = Record<string, unknown>,
144
+ TDeleteOptions = Record<string, unknown>,
145
+ TUpdateOptions = Record<string, unknown>,
146
+ TCreateOptions = Record<string, unknown>,
147
+ TUploadOptions = Record<string, unknown>,
148
+ TStreamOptions = Record<string, unknown>,
149
+ > {
150
+ /**
151
+ * Finds records based on provided options, returning a paginated result.
152
+ * @param options The options for the find operation.
153
+ * @returns A promise that resolves to a Page of records.
154
+ */
155
+ find: (options: TFindOptions) => Promise<Page<T>>;
156
+
157
+ /**
158
+ * Reads a single record by its identifier or other read options.
159
+ * @param options The options for the read operation, typically including an ID.
160
+ * @returns A promise that resolves to the record or undefined if not found.
161
+ */
162
+ read: (options: TReadOptions) => Promise<Document<T> | undefined>;
163
+
164
+ /**
165
+ * Lists records based on provided options, returning a paginated result.
166
+ * @param options The options for the list operation.
167
+ * @returns A promise that resolves to a Page of records.
168
+ */
169
+ list: (options: TListOptions) => Promise<Page<T>>;
170
+
171
+ /**
172
+ * Deletes a record based on provided options, typically including an ID.
173
+ * @param options The options for the delete operation.
174
+ * @returns A promise that resolves when the deletion is complete.
175
+ */
176
+ delete: (options: TDeleteOptions) => Promise<void>;
177
+
178
+ /**
179
+ * Updates an existing record.
180
+ * @param props An object containing the ID of the record to update, the partial data, and optional update options.
181
+ * @returns A promise that resolves to the updated record or undefined if not found.
182
+ */
183
+ update: (props: {
184
+ data: Partial<T>;
185
+ options?: TUpdateOptions;
186
+ }) => Promise<Document<T> | undefined>;
187
+
188
+ /**
189
+ * Creates a new record.
190
+ * @param props An object containing the data for the new record and optional create options.
191
+ * @returns A promise that resolves to the newly created record or undefined if creation failed.
192
+ */
193
+ create: (props: {
194
+ data: Partial<T>;
195
+ options?: TCreateOptions;
196
+ }) => Promise<Document<T> | undefined>;
197
+
198
+ /**
199
+ * Uploads a file associated with a record. Optionally updates the record with upload details.
200
+ * @param props An object containing the file to upload and optional upload options.
201
+ * @returns A promise that resolves to the updated record after upload or undefined if upload failed.
202
+ */
203
+ upload: (props: {
204
+ file: File;
205
+ options?: TUploadOptions;
206
+ }) => Promise<Document<T> | undefined>;
207
+
208
+ /**
209
+ * Subscribes to store events for a given scope.
210
+ * @param scope The event scope to subscribe to (e.g., 'todos:created:success', '*').
211
+ * @param callback The function to call when an event matching the scope is received.
212
+ * @returns A promise that resolves to an unsubscribe function. Call this function to stop receiving events.
213
+ */
214
+ subscribe(
215
+ scope: string,
216
+ callback: (event: StoreEvent) => void,
217
+ ): Promise<() => void>;
218
+ /**
219
+ * Notifies the store of an event, which can then be broadcast to subscribers.
220
+ * @param event The StoreEvent to notify.
221
+ * @returns A promise that resolves when the notification has been processed.
222
+ */
223
+ notify: (event: StoreEvent) => Promise<void>;
224
+
225
+ /**
226
+ * Establishes a stream of records based on provided options.
227
+ * @param options Options for configuring the stream (e.g., batch size, delay, filters).
228
+ * @param onStreamChange A callback function that is called when the stream's status changes.
229
+ * @returns An object containing the async iterable stream, a cancel function, and a status getter.
230
+ */
231
+ stream: (
232
+ options: TStreamOptions,
233
+ onStreamChange: () => void,
234
+ ) => {
235
+ /** An async iterable that yields records as they become available in the stream. */
236
+ stream: () => AsyncIterable<Document<T>>;
237
+ /** A function to call to cancel the ongoing stream. */
238
+ cancel: () => void;
239
+ /** A getter function that returns the current status of the stream ('active', 'cancelled', or 'completed'). */
240
+ status: () => "active" | "cancelled" | "completed";
241
+ };
242
+
243
+ /**
244
+ * Creates a paginated data controller for this store.
245
+ *
246
+ * @param options - The pagination options, whose type is defined by the
247
+ * store's `TPageOptions` generic parameter. This lets you
248
+ * pass any configuration (e.g., filters, sorting, custom
249
+ * pagination metadata) that your store implementation needs.
250
+ * @returns A `PagedData<T>` controller that manages loading, navigation,
251
+ * resizing, and refresh.
252
+ *
253
+ * @example
254
+ * // Define your store with a custom TPageOptions type
255
+ * interface MyTodoStore extends DocumentStore<
256
+ * Todo,
257
+ * FindOptions,
258
+ * ReadOptions,
259
+ * ListOptions,
260
+ * { page: number; size: number; sort?: string; filter?: string }
261
+ * > {}
262
+ *
263
+ * // Use it
264
+ * const todosPaged = myTodoStore.page({
265
+ * page: 1,
266
+ * size: 10,
267
+ * sort: 'createdAt',
268
+ * filter: 'done:false'
269
+ * });
270
+ *
271
+ * // Get current page state
272
+ * const { data, loading, page } = todosPaged.page();
273
+ * console.log(`Page ${page.number} of ${page.pages}`);
274
+ *
275
+ * // Navigate
276
+ * await todosPaged.navigate(2);
277
+ */
278
+ page(options: TPageOptions): Promise<PagedData<T>>;
279
+ }
package/index.ts ADDED
@@ -0,0 +1,62 @@
1
+ // Core
2
+ export { HestiaNetworkClient, HestiaResponse } from "./core/client"
3
+ export type { IdentityProvider } from "./core/client"
4
+ export { HestiaCollection } from "./core/collection"
5
+ export { toSystemError } from "./core/errors"
6
+
7
+ // Generic types
8
+ export type { Document, Page, PagedData, PaginationInfo, StoreEvent } from "./core/types"
9
+
10
+ export type { PageOptions } from "./core/pager"
11
+
12
+ // Auth
13
+ export { HestiaAuth } from "./auth/store"
14
+ export type { TokenPair, LoginResult, ServerHealth, LoginRequest, RegisterRequest } from "./auth/types"
15
+
16
+ // System: identity
17
+ export { HestiaUsers } from "./system/identity/store"
18
+ export type {
19
+ UserData,
20
+ UserIdentity,
21
+ CreateUserRequest,
22
+ UpdateUserRequest,
23
+ } from "./system/identity/types"
24
+
25
+ // System: api-keys
26
+ export { HestiaKeyStore } from "./system/api-keys/store"
27
+ export type { APIKey, APIKeyWithSecret, CreateKeyRequest, UpdateKeyRequest } from "./system/api-keys/types"
28
+
29
+ // System: policies
30
+ export { HestiaPolicies } from "./system/policies/store"
31
+ export type {
32
+ PolicyOperation,
33
+ PolicyRule,
34
+ UpsertOperationRequest,
35
+ UpsertRuleRequest,
36
+ ValidateRuleRequest,
37
+ ValidateRuleResult,
38
+ ReloadResult,
39
+ } from "./system/policies/types"
40
+
41
+ // System: logs
42
+ export { HestiaLogs } from "./system/logs/store"
43
+ export type { AuditEntry, LogFilter } from "./system/logs/types"
44
+
45
+ // Blobs
46
+ export { HestiaBlobClient, BlobNamespace } from "./blobs/store"
47
+ export type {
48
+ NamespaceInfo,
49
+ BlobMeta,
50
+ BlobDocument as BlobDoc,
51
+ ListBlobsRequest,
52
+ CreateNamespaceRequest,
53
+ } from "./blobs/types"
54
+
55
+ // Collections
56
+ export { HestiaCollections } from "./collections/store"
57
+ export type { CollectionMeta } from "./collections/types"
58
+
59
+ // Container
60
+ export { HestiaClient } from "./container"
61
+
62
+ export * from "./utils"
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@asaidimu/hestia",
3
+ "version": "1.0.0",
4
+ "description": "TypeScript client SDK for the Hestia platform — auth, collections, API keys, policies, audit logs, blobs, and capabilities",
5
+ "type": "module",
6
+ "module": "index.ts",
7
+ "main": "index.ts",
8
+ "types": "index.ts",
9
+ "scripts": {
10
+ "test": "vitest run",
11
+ "test:watch": "vitest",
12
+ "test:browser": "vitest --browser --run"
13
+ },
14
+ "files": [
15
+ "*.ts",
16
+ "*/**/*.ts",
17
+ "!*.test.ts",
18
+ "!*/**/*.test.ts"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/asaidimu/hestia.git"
23
+ },
24
+ "license": "MIT",
25
+ "author": "Asaidimu <dev@asaidimu.com>",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "devDependencies": {
30
+ "@types/bun": "latest",
31
+ "vitest": "^4.1.10"
32
+ },
33
+ "peerDependencies": {
34
+ "typescript": "^5"
35
+ },
36
+ "dependencies": {
37
+ "@asaidimu/network-client": "^2.0.0",
38
+ "@asaidimu/utils-artifacts": "^8.2.25",
39
+ "@asaidimu/utils-error": "^1.1.10",
40
+ "@asaidimu/utils-store": "^10.2.18",
41
+ "uuid": "^14.0.1"
42
+ }
43
+ }
@@ -0,0 +1,70 @@
1
+ import type { QueryDSL } from "@asaidimu/query"
2
+ import { HestiaNetworkClient } from "../../core/client"
3
+ import type { Document, Page, PagedData } from "../../core/types"
4
+ import type { APIKey, APIKeyWithSecret, CreateKeyRequest, UpdateKeyRequest } from "./types"
5
+
6
+ export class HestiaKeyStore {
7
+ constructor(private client: HestiaNetworkClient) {}
8
+
9
+ private basePath = "/system/apikeys/key"
10
+
11
+ async find(_query?: QueryDSL<APIKey>): Promise<Page<APIKey>> {
12
+ const res = await this.client.get<{
13
+ data: Document<APIKey>[];
14
+ metadata?: { page?: any };
15
+ }>(this.basePath)
16
+ const items = res.data?.data ?? []
17
+ const pageMeta = res.data?.metadata?.page ?? {
18
+ number: 1,
19
+ size: items.length,
20
+ count: items.length,
21
+ total: items.length,
22
+ pages: 1,
23
+ }
24
+ return { data: items, loading: false, page: pageMeta }
25
+ }
26
+
27
+ async list(): Promise<Page<APIKey>> {
28
+ return this.find()
29
+ }
30
+
31
+ async read(id: string): Promise<Document<APIKey> | undefined> {
32
+ try {
33
+ const res = await this.client.get<{ data: Document<APIKey> }>(
34
+ `${this.basePath}/${encodeURIComponent(id)}`,
35
+ )
36
+ return res.data?.data
37
+ } catch (err: any) {
38
+ if (err?.code === "SYNC-001-NF") return undefined
39
+ if (err?.code === "INTERNAL_ERROR" && typeof err?.message === "string" && err.message.includes("not found")) return undefined
40
+ throw err
41
+ }
42
+ }
43
+
44
+ async create(data: CreateKeyRequest): Promise<Document<APIKeyWithSecret>> {
45
+ const res = await this.client.post<{ data: Document<APIKeyWithSecret> }>(
46
+ this.basePath,
47
+ data,
48
+ )
49
+ return res.data!.data
50
+ }
51
+
52
+ async update(id: string, data: UpdateKeyRequest): Promise<Document<APIKey>> {
53
+ const res = await this.client.patch<{ data: Document<APIKey> }>(
54
+ `${this.basePath}/${encodeURIComponent(id)}`,
55
+ data as any,
56
+ )
57
+ return res.data!.data
58
+ }
59
+
60
+ async delete(id: string): Promise<void> {
61
+ await this.client.delete(`${this.basePath}/${encodeURIComponent(id)}`)
62
+ }
63
+
64
+ async rotate(id: string): Promise<Document<APIKeyWithSecret>> {
65
+ const res = await this.client.post<{ data: Document<APIKeyWithSecret> }>(
66
+ `${this.basePath}/${encodeURIComponent(id)}`,
67
+ )
68
+ return res.data!.data
69
+ }
70
+ }
@@ -0,0 +1,29 @@
1
+ export interface APIKey {
2
+ _id_: string
3
+ name: string
4
+ prefix: string
5
+ scopes: string[]
6
+ status: string
7
+ expiry?: string | null
8
+ environment?: string
9
+ _metadata_: Record<string, unknown>
10
+ }
11
+
12
+ export interface APIKeyWithSecret extends APIKey {
13
+ key: string
14
+ }
15
+
16
+ export interface CreateKeyRequest {
17
+ name: string
18
+ environment?: string
19
+ scopes?: string[]
20
+ expiry?: string
21
+ }
22
+
23
+ export interface UpdateKeyRequest {
24
+ name?: string
25
+ environment?: string
26
+ scopes?: string[]
27
+ status?: "active" | "revoked"
28
+ expiry?: string
29
+ }
@@ -0,0 +1,23 @@
1
+ import { HestiaNetworkClient } from "../../core/client";
2
+ import type { Document } from "../../core/types";
3
+
4
+ export class HestiaCapabilities {
5
+ constructor(
6
+ private client: HestiaNetworkClient,
7
+ ) {
8
+
9
+ }
10
+
11
+ async fetch(): Promise<Array<Document<any>> | undefined> {
12
+ try {
13
+ const res = await this.client.get<{ data: Array<Document<any>> }>(
14
+ `/system/core/docs`,
15
+ );
16
+ return res.data?.data;
17
+ } catch (err: any) {
18
+ if (err?.code === "SYNC-001-NF") return undefined;
19
+ throw err;
20
+ }
21
+ }
22
+
23
+ }
@@ -0,0 +1 @@
1
+ // Capabilities types — currently empty
@@ -0,0 +1,90 @@
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 { Document, Page, PagedData } from "../../core/types";
6
+ import type { UpdateUserRequest, UserData } from "./types";
7
+
8
+ export class HestiaUsers {
9
+ private pagerOptions: any = {};
10
+ private pager: PagedData<UserData>;
11
+
12
+ constructor(
13
+ private client: HestiaNetworkClient,
14
+ ) {
15
+ this.pager = createPagedController<UserData>(
16
+ "users",
17
+ new ReactiveDataStore<any>({}),
18
+ this.pagerOptions,
19
+ (query) => this.find(query),
20
+ );
21
+ }
22
+
23
+ name() {
24
+ return "users";
25
+ }
26
+
27
+ async find(query?: QueryDSL<UserData>): Promise<Page<UserData>> {
28
+ const res = await this.client.post<{
29
+ data: Document<UserData>[];
30
+ metadata?: { page?: any };
31
+ }>("/system/users/user/query", query ?? {});
32
+ const data = res.data?.data ?? [];
33
+ const pageMeta = res.data?.metadata?.page ?? {
34
+ number: 1,
35
+ size: data.length,
36
+ count: data.length,
37
+ total: data.length,
38
+ pages: 1,
39
+ };
40
+ return { data, loading: false, page: pageMeta };
41
+ }
42
+
43
+ async list(options?: QueryDSL<UserData>): Promise<Page<UserData>> {
44
+ return this.find(
45
+ options ?? { pagination: { type: "offset", offset: 0, limit: 50 } },
46
+ );
47
+ }
48
+
49
+ async read(id: string): Promise<Document<UserData> | undefined> {
50
+ try {
51
+ const res = await this.client.get<{ data: Document<UserData> }>(
52
+ `/system/users/user/${encodeURIComponent(id)}`,
53
+ );
54
+ return res.data?.data;
55
+ } catch (err: any) {
56
+ if (err?.code === "SYNC-001-NF") return undefined;
57
+ throw err;
58
+ }
59
+ }
60
+
61
+ async update(
62
+ id: string,
63
+ data: UpdateUserRequest,
64
+ ): Promise<Document<UserData>> {
65
+ const res = await this.client.patch<{ data: Document<UserData> }>(
66
+ `/system/users/user/${encodeURIComponent(id)}`,
67
+ data as any,
68
+ );
69
+ return res.data!.data;
70
+ }
71
+
72
+ async delete(id: string): Promise<void> {
73
+ await this.client.delete(`/system/users/user/${encodeURIComponent(id)}`);
74
+ }
75
+
76
+ async changePassword(
77
+ userId: string,
78
+ current: string,
79
+ newPassword: string,
80
+ ): Promise<void> {
81
+ await this.client.patch(`/system/users/password/${encodeURIComponent(userId)}`, {
82
+ current,
83
+ new: newPassword,
84
+ });
85
+ }
86
+
87
+ page(): PagedData<UserData> {
88
+ return this.pager;
89
+ }
90
+ }
@@ -0,0 +1,31 @@
1
+ import { Document } from "../../core/types"
2
+
3
+ export interface UserData {
4
+ email: string
5
+ name: string
6
+ verified: boolean
7
+ scopes: string[]
8
+ deleted_at?: string | null
9
+ }
10
+
11
+ export type UserIdentity = Document<UserData>
12
+
13
+ export interface CreateUserRequest {
14
+ email: string
15
+ password: string
16
+ name: string
17
+ scopes?: string[]
18
+ verified?: boolean
19
+ }
20
+
21
+ export interface UpdateUserRequest {
22
+ name?: string
23
+ email?: string
24
+ scopes?: string[]
25
+ verified?: boolean
26
+ }
27
+
28
+ export interface ChangePasswordRequest {
29
+ current: string
30
+ new: string
31
+ }