@asaidimu/hestia 1.0.0 → 1.0.1
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/auth/store.ts +2 -2
- package/auth/types.ts +1 -1
- package/blobs/store.ts +58 -57
- package/collections/store.ts +53 -24
- package/collections/types.ts +1 -1
- package/container.ts +7 -9
- package/core/client.ts +170 -9
- package/core/collection.ts +33 -9
- package/core/errors.ts +1 -1
- package/core/pager.ts +3 -0
- package/core/types.ts +2 -2
- package/index.ts +18 -41
- package/package.json +6 -2
- package/system/api-keys/store.ts +47 -10
- package/system/api-keys/types.ts +5 -3
- package/system/capabilities/store.ts +57 -13
- package/system/identity/store.ts +35 -12
- package/system/identity/types.ts +5 -5
- package/system/policies/store.ts +138 -35
- package/system/policies/types.ts +2 -2
- package/utils/pager.ts +8 -4
- package/system/logs/store.ts +0 -147
- package/system/logs/types.ts +0 -38
package/system/policies/store.ts
CHANGED
|
@@ -1,74 +1,177 @@
|
|
|
1
1
|
import type { QueryDSL } from "@asaidimu/query"
|
|
2
2
|
import { HestiaNetworkClient } from "../../core/client"
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import { ReactiveDataStore } from "@asaidimu/utils-store"
|
|
4
|
+
import { createPagedController } from "../../core/pager"
|
|
5
|
+
import type { Document, Page, PagedData, PaginationInfo, StoreEvent } from "../../core/types"
|
|
6
|
+
import type { DocumentStore } from "../../core/types"
|
|
5
7
|
import type {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
OperationPolicy,
|
|
9
|
+
IamRule,
|
|
8
10
|
UpsertOperationRequest,
|
|
9
11
|
UpsertRuleRequest,
|
|
12
|
+
ReloadResult,
|
|
10
13
|
} from "./types"
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
const OPERATIONS_COLLECTION = "_operation_policy_"
|
|
16
|
+
const RULES_COLLECTION = "_iam_rule_"
|
|
17
|
+
|
|
18
|
+
export class HestiaPolicies implements DocumentStore<OperationPolicy, QueryDSL<OperationPolicy>, string, QueryDSL<OperationPolicy>, Record<string, unknown>, string, string, Record<string, unknown>> {
|
|
19
|
+
private pager: PagedData<OperationPolicy>
|
|
20
|
+
private operationsPath = "/system/policies/operation"
|
|
21
|
+
private rulesPath = "/system/policies/rule"
|
|
15
22
|
|
|
16
23
|
constructor(private client: HestiaNetworkClient) {
|
|
17
|
-
this.
|
|
18
|
-
|
|
24
|
+
this.pager = createPagedController<OperationPolicy>(
|
|
25
|
+
"policies",
|
|
26
|
+
new ReactiveDataStore<any>({}),
|
|
27
|
+
{},
|
|
28
|
+
(query) => this.find(query),
|
|
29
|
+
)
|
|
19
30
|
}
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
private async collectionQuery<T extends Record<string, any>>(
|
|
33
|
+
collection: string,
|
|
34
|
+
query?: Record<string, unknown>,
|
|
35
|
+
): Promise<Page<T>> {
|
|
36
|
+
const res = await this.client.post<{
|
|
37
|
+
data: Document<T>[];
|
|
38
|
+
metadata?: { page?: PaginationInfo };
|
|
39
|
+
}>(`/system/collections/document/${encodeURIComponent(collection)}/query`, query ?? {})
|
|
40
|
+
const items = res.data?.data ?? []
|
|
41
|
+
const pageMeta = res.data?.metadata?.page ?? {
|
|
42
|
+
number: 1,
|
|
43
|
+
size: items.length,
|
|
44
|
+
count: items.length,
|
|
45
|
+
total: items.length,
|
|
46
|
+
pages: 1,
|
|
47
|
+
}
|
|
48
|
+
return { data: items, loading: false, page: pageMeta, error: null }
|
|
49
|
+
}
|
|
22
50
|
|
|
23
|
-
async
|
|
24
|
-
return this.
|
|
51
|
+
async find(query?: QueryDSL<OperationPolicy>): Promise<Page<OperationPolicy>> {
|
|
52
|
+
return this.collectionQuery<OperationPolicy>(OPERATIONS_COLLECTION, query as Record<string, unknown> | undefined)
|
|
25
53
|
}
|
|
26
54
|
|
|
27
|
-
async
|
|
28
|
-
return this.
|
|
55
|
+
async list(options?: QueryDSL<OperationPolicy>): Promise<Page<OperationPolicy>> {
|
|
56
|
+
return this.find(options ?? { pagination: { type: "offset", offset: 0, limit: 50 } })
|
|
29
57
|
}
|
|
30
58
|
|
|
31
|
-
async
|
|
32
|
-
|
|
59
|
+
async read(id: string): Promise<Document<OperationPolicy> | undefined> {
|
|
60
|
+
try {
|
|
61
|
+
const res = await this.client.get<{ data: Document<OperationPolicy> }>(
|
|
62
|
+
`${this.operationsPath}/${encodeURIComponent(id)}`,
|
|
63
|
+
)
|
|
64
|
+
return res.data?.data
|
|
65
|
+
} catch (err: any) {
|
|
66
|
+
if (err?.code === "SYNC-001-NF" || err?.code === "NOT_FOUND" || err?.code === "INTERNAL_ERROR" && typeof err?.message === "string" && err.message.includes("not found")) return undefined
|
|
67
|
+
throw err
|
|
68
|
+
}
|
|
33
69
|
}
|
|
34
70
|
|
|
35
|
-
async
|
|
36
|
-
|
|
71
|
+
async create(props: { data: Partial<OperationPolicy> }): Promise<Document<OperationPolicy> | undefined> {
|
|
72
|
+
const name = props.data.name
|
|
73
|
+
if (!name) throw new Error("Operation name is required for create")
|
|
74
|
+
const res = await this.client.patch<{ data: Document<OperationPolicy> }>(
|
|
75
|
+
`${this.operationsPath}/${encodeURIComponent(name)}`,
|
|
76
|
+
props.data as UpsertOperationRequest,
|
|
77
|
+
)
|
|
78
|
+
return res.data!.data
|
|
37
79
|
}
|
|
38
80
|
|
|
39
|
-
async
|
|
40
|
-
|
|
81
|
+
async update(props: { data: Partial<OperationPolicy>; options?: string }): Promise<Document<OperationPolicy> | undefined> {
|
|
82
|
+
const name = props.options!
|
|
83
|
+
if (!name) throw new Error("Operation name is required for update")
|
|
84
|
+
const res = await this.client.patch<{ data: Document<OperationPolicy> }>(
|
|
85
|
+
`${this.operationsPath}/${encodeURIComponent(name)}`,
|
|
86
|
+
props.data as UpsertOperationRequest,
|
|
87
|
+
)
|
|
88
|
+
return res.data!.data
|
|
41
89
|
}
|
|
42
90
|
|
|
43
|
-
async
|
|
44
|
-
|
|
91
|
+
async delete(id: string): Promise<void> {
|
|
92
|
+
await this.client.delete(`${this.operationsPath}/${encodeURIComponent(id)}`)
|
|
45
93
|
}
|
|
46
94
|
|
|
47
|
-
|
|
95
|
+
async upsertOperation(
|
|
96
|
+
name: string,
|
|
97
|
+
data: UpsertOperationRequest,
|
|
98
|
+
): Promise<Document<OperationPolicy>> {
|
|
99
|
+
const res = await this.client.patch<{ data: Document<OperationPolicy> }>(
|
|
100
|
+
`${this.operationsPath}/${encodeURIComponent(name)}`,
|
|
101
|
+
data,
|
|
102
|
+
)
|
|
103
|
+
return res.data!.data
|
|
104
|
+
}
|
|
48
105
|
|
|
49
|
-
async
|
|
50
|
-
|
|
106
|
+
async getRule(name: string): Promise<Document<IamRule> | undefined> {
|
|
107
|
+
try {
|
|
108
|
+
const res = await this.client.get<{ data: Document<IamRule> }>(
|
|
109
|
+
`${this.rulesPath}/${encodeURIComponent(name)}`,
|
|
110
|
+
)
|
|
111
|
+
return res.data?.data
|
|
112
|
+
} catch (err: any) {
|
|
113
|
+
if (err?.code === "SYNC-001-NF" || err?.code === "NOT_FOUND" || err?.code === "INTERNAL_ERROR" && typeof err?.message === "string" && err.message.includes("not found")) return undefined
|
|
114
|
+
throw err
|
|
115
|
+
}
|
|
51
116
|
}
|
|
52
117
|
|
|
53
|
-
async
|
|
54
|
-
|
|
118
|
+
async upsertRule(
|
|
119
|
+
name: string,
|
|
120
|
+
data: UpsertRuleRequest,
|
|
121
|
+
): Promise<Document<IamRule>> {
|
|
122
|
+
const res = await this.client.patch<{ data: Document<IamRule> }>(
|
|
123
|
+
`${this.rulesPath}/${encodeURIComponent(name)}`,
|
|
124
|
+
data,
|
|
125
|
+
)
|
|
126
|
+
return res.data!.data
|
|
55
127
|
}
|
|
56
128
|
|
|
57
|
-
async
|
|
58
|
-
|
|
129
|
+
async deleteRule(name: string): Promise<void> {
|
|
130
|
+
await this.client.delete(`${this.rulesPath}/${encodeURIComponent(name)}`)
|
|
59
131
|
}
|
|
60
132
|
|
|
61
|
-
async
|
|
62
|
-
return this.
|
|
133
|
+
async listRules(options?: Record<string, unknown>): Promise<Page<IamRule>> {
|
|
134
|
+
return this.collectionQuery<IamRule>(RULES_COLLECTION, options)
|
|
63
135
|
}
|
|
64
136
|
|
|
65
|
-
async
|
|
66
|
-
|
|
137
|
+
async validateRule(
|
|
138
|
+
expression: string,
|
|
139
|
+
): Promise<boolean> {
|
|
140
|
+
const res = await this.client.post<{ data: { valid: boolean } }>(
|
|
141
|
+
`${this.rulesPath}/validate`,
|
|
142
|
+
{ expression },
|
|
143
|
+
)
|
|
144
|
+
return res.data?.data?.valid ?? false
|
|
67
145
|
}
|
|
68
146
|
|
|
69
|
-
async
|
|
70
|
-
|
|
147
|
+
async reload(): Promise<ReloadResult> {
|
|
148
|
+
const res = await this.client.get<{ data: ReloadResult }>(
|
|
149
|
+
`${this.rulesPath}/reload`,
|
|
150
|
+
)
|
|
151
|
+
return res.data!.data
|
|
71
152
|
}
|
|
72
153
|
|
|
154
|
+
async upload(_props: { file: File }): Promise<Document<OperationPolicy> | undefined> {
|
|
155
|
+
throw new Error("Upload not supported for policies")
|
|
156
|
+
}
|
|
73
157
|
|
|
158
|
+
async subscribe(_scope: string, _callback: (event: StoreEvent) => void): Promise<() => void> {
|
|
159
|
+
throw new Error("Subscription not supported for policies")
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async notify(_event: StoreEvent): Promise<void> {
|
|
163
|
+
throw new Error("Notify not supported for policies")
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
stream(_options: Record<string, unknown>, _onStreamChange: () => void): {
|
|
167
|
+
stream: () => AsyncIterable<Document<OperationPolicy>>;
|
|
168
|
+
cancel: () => void;
|
|
169
|
+
status: () => "active" | "cancelled" | "completed";
|
|
170
|
+
} {
|
|
171
|
+
throw new Error("Stream not supported for policies")
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
page(_options?: Record<string, unknown>): PagedData<OperationPolicy> {
|
|
175
|
+
return this.pager
|
|
176
|
+
}
|
|
74
177
|
}
|
package/system/policies/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface OperationPolicy {
|
|
2
2
|
name: string
|
|
3
3
|
ruleKey: string
|
|
4
4
|
description?: string
|
|
@@ -11,7 +11,7 @@ export interface UpsertOperationRequest {
|
|
|
11
11
|
intentType?: "COMMAND" | "QUERY"
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export interface
|
|
14
|
+
export interface IamRule {
|
|
15
15
|
name: string
|
|
16
16
|
ruleType?: string
|
|
17
17
|
expression?: string
|
package/utils/pager.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createMatcher, createSorter } from '@asaidimu/query'; //
|
|
2
|
+
import type { QueryDSL, QueryFilter, SortConfiguration } from '@asaidimu/query'; //
|
|
2
3
|
import { DELETE_SYMBOL, ReactiveDataStore } from "@asaidimu/utils-store"; //
|
|
3
|
-
import { Page, PagedData } from '../core/types';
|
|
4
|
+
import type { Page, PagedData, PagerRefreshOptions } from '../core/types';
|
|
4
5
|
|
|
5
6
|
export interface FetchResult<T extends Record<string, any>> {
|
|
6
7
|
data: T[];
|
|
@@ -168,6 +169,7 @@ export function createArrayPager<T extends Record<string, any>, F = any>(
|
|
|
168
169
|
executeDataPipeline(true);
|
|
169
170
|
|
|
170
171
|
return {
|
|
172
|
+
id: () => CACHE_KEY,
|
|
171
173
|
page: () => selector.get() ?? sentinel, //
|
|
172
174
|
|
|
173
175
|
navigate: async (page: number) => {
|
|
@@ -195,15 +197,17 @@ export function createArrayPager<T extends Record<string, any>, F = any>(
|
|
|
195
197
|
await executeDataPipeline(true);
|
|
196
198
|
},
|
|
197
199
|
|
|
198
|
-
refresh: async (
|
|
200
|
+
refresh: async (opts?: PagerRefreshOptions) => { //
|
|
199
201
|
const previousState = selector.get() ?? sentinel;
|
|
200
202
|
await store.set({
|
|
201
203
|
[CACHE_KEY]: { ...previousState, loading: true } //
|
|
202
204
|
});
|
|
203
205
|
|
|
206
|
+
const delay = opts?.delay ?? 600;
|
|
207
|
+
|
|
204
208
|
await Promise.all([
|
|
205
209
|
new Promise((resolve) => setTimeout(resolve, delay)), //
|
|
206
|
-
executeDataPipeline(false) // Handle full resolution pipeline passing down false to
|
|
210
|
+
executeDataPipeline(false) // Handle full resolution pipeline passing down false to avoid flashing double mutations
|
|
207
211
|
]);
|
|
208
212
|
|
|
209
213
|
const finishedState = selector.get() ?? sentinel;
|
package/system/logs/store.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import type { QueryDSL } from "@asaidimu/query"
|
|
2
|
-
import { HestiaNetworkClient } from "../../core/client"
|
|
3
|
-
import type { Page, PaginationInfo } from "../../core/types"
|
|
4
|
-
import type { AuditEntry } from "./types"
|
|
5
|
-
|
|
6
|
-
interface FindEnvelope {
|
|
7
|
-
data: AuditEntry[]
|
|
8
|
-
metadata?: { page?: PaginationInfo }
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface StreamDoc {
|
|
12
|
-
data: AuditEntry
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class HestiaLogs {
|
|
16
|
-
constructor(
|
|
17
|
-
private client: HestiaNetworkClient,
|
|
18
|
-
private baseUrl: string,
|
|
19
|
-
) {}
|
|
20
|
-
|
|
21
|
-
async find(query?: QueryDSL<AuditEntry>): Promise<Page<AuditEntry>> {
|
|
22
|
-
const res = await this.client.post<FindEnvelope>(
|
|
23
|
-
"/system/audit/log/query",
|
|
24
|
-
{ query: query ?? {} },
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
const items = res.data?.data ?? []
|
|
28
|
-
const pageMeta = res.data?.metadata?.page ?? {
|
|
29
|
-
number: 1,
|
|
30
|
-
size: items.length,
|
|
31
|
-
count: items.length,
|
|
32
|
-
total: items.length,
|
|
33
|
-
pages: 1,
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return { data: items, loading: false, page: pageMeta, error: null }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async list(): Promise<Page<AuditEntry>> {
|
|
40
|
-
return this.find({
|
|
41
|
-
pagination: { type: "offset", offset: 0, limit: 20 },
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
getStreamUrl(): string {
|
|
46
|
-
return `${this.baseUrl}/system/audit/log/stream`
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
subscribe(onEntry: (entry: AuditEntry) => void): () => void {
|
|
50
|
-
const url = this.getStreamUrl()
|
|
51
|
-
const eventSource = new EventSource(url, { withCredentials: true })
|
|
52
|
-
|
|
53
|
-
eventSource.onmessage = (event) => {
|
|
54
|
-
try {
|
|
55
|
-
const parsed = JSON.parse(event.data) as StreamDoc
|
|
56
|
-
if (parsed?.data) {
|
|
57
|
-
onEntry(parsed.data)
|
|
58
|
-
}
|
|
59
|
-
} catch {
|
|
60
|
-
// skip non-JSON lines
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
eventSource.onerror = () => {
|
|
65
|
-
eventSource.close()
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return () => {
|
|
69
|
-
eventSource.close()
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
stream(): {
|
|
74
|
-
stream: () => AsyncIterable<AuditEntry>
|
|
75
|
-
cancel: () => void
|
|
76
|
-
status: () => "active" | "cancelled" | "completed"
|
|
77
|
-
} {
|
|
78
|
-
const url = this.getStreamUrl()
|
|
79
|
-
let eventSource: EventSource | null = null
|
|
80
|
-
let currentStatus: "active" | "cancelled" | "completed" = "active"
|
|
81
|
-
let pendingResolve: (() => void) | null = null
|
|
82
|
-
|
|
83
|
-
const asyncStream = async function* () {
|
|
84
|
-
const pending: AuditEntry[] = []
|
|
85
|
-
|
|
86
|
-
eventSource = new EventSource(url, { withCredentials: true })
|
|
87
|
-
|
|
88
|
-
eventSource.onmessage = (event) => {
|
|
89
|
-
for (const line of event.data.split("\n")) {
|
|
90
|
-
const trimmed = line.trim()
|
|
91
|
-
if (!trimmed) continue
|
|
92
|
-
try {
|
|
93
|
-
const parsed = JSON.parse(trimmed) as StreamDoc
|
|
94
|
-
if (parsed?.data) pending.push(parsed.data)
|
|
95
|
-
} catch {
|
|
96
|
-
// skip non-JSON lines
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (pending.length > 0 && pendingResolve) {
|
|
100
|
-
pendingResolve()
|
|
101
|
-
pendingResolve = null
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
eventSource.onerror = () => {
|
|
106
|
-
if (currentStatus === "active") currentStatus = "completed"
|
|
107
|
-
if (pendingResolve) {
|
|
108
|
-
pendingResolve()
|
|
109
|
-
pendingResolve = null
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
while (currentStatus === "active") {
|
|
115
|
-
if (pending.length > 0) {
|
|
116
|
-
yield pending.shift()!
|
|
117
|
-
} else {
|
|
118
|
-
await new Promise<void>((resolve) => {
|
|
119
|
-
pendingResolve = resolve
|
|
120
|
-
if (pending.length > 0) {
|
|
121
|
-
resolve()
|
|
122
|
-
pendingResolve = null
|
|
123
|
-
}
|
|
124
|
-
})
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
} finally {
|
|
128
|
-
eventSource?.close()
|
|
129
|
-
if (pendingResolve) {
|
|
130
|
-
pendingResolve()
|
|
131
|
-
pendingResolve = null
|
|
132
|
-
}
|
|
133
|
-
if (currentStatus === "active") currentStatus = "completed"
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
stream: () => asyncStream(),
|
|
139
|
-
cancel: () => {
|
|
140
|
-
if (currentStatus !== "active") return
|
|
141
|
-
currentStatus = "cancelled"
|
|
142
|
-
eventSource?.close()
|
|
143
|
-
},
|
|
144
|
-
status: () => currentStatus,
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
package/system/logs/types.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export interface AuditEntry {
|
|
2
|
-
event_id: string
|
|
3
|
-
occurred_at: string
|
|
4
|
-
recorded_at: string
|
|
5
|
-
trace_id?: string
|
|
6
|
-
request_id?: string
|
|
7
|
-
actor_id: string
|
|
8
|
-
actor_type: "user" | "service" | "system" | "anonymous"
|
|
9
|
-
on_behalf_of_id?: string
|
|
10
|
-
auth_method?: "password" | "oauth" | "api_key" | "mtls" | "sso" | "service_account" | "none"
|
|
11
|
-
session_id?: string
|
|
12
|
-
operation: "create" | "read" | "update" | "delete" | "login" | "logout" | "grant" | "revoke" | "execute" | "other"
|
|
13
|
-
resource_type: string
|
|
14
|
-
resource_id?: string
|
|
15
|
-
event_name: string
|
|
16
|
-
status: "success" | "failure" | "denied" | "error"
|
|
17
|
-
severity?: "info" | "warning" | "critical"
|
|
18
|
-
error_code?: string
|
|
19
|
-
error_message?: string
|
|
20
|
-
latency_ms: number
|
|
21
|
-
source_ip?: string
|
|
22
|
-
user_agent?: string
|
|
23
|
-
service_name: string
|
|
24
|
-
region?: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export type RequestLogEntry = AuditEntry
|
|
28
|
-
|
|
29
|
-
export interface LogFilter {
|
|
30
|
-
actor_id?: string
|
|
31
|
-
actor_type?: string
|
|
32
|
-
operation?: string
|
|
33
|
-
status?: string
|
|
34
|
-
resource_type?: string
|
|
35
|
-
trace_id?: string
|
|
36
|
-
start?: string
|
|
37
|
-
end?: string
|
|
38
|
-
}
|