@feltdb/core 0.4.20 → 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/README.md +169 -0
- package/dist/cli/index.js +1 -1
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +12 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +2 -0
- package/dist/create/server-source/crates/feltdb-server/src/request_telemetry.rs +381 -0
- package/dist/create/server-source/crates/feltdb-server/src/transaction_idempotency.rs +280 -0
- package/dist/error-codes.d.ts +53 -0
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +46 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/revision-recovery.d.ts +162 -0
- package/dist/revision-recovery.d.ts.map +1 -0
- package/dist/revision-recovery.js +69 -0
- package/dist/state-contract.d.ts +2 -0
- package/dist/state-contract.d.ts.map +1 -1
- package/dist/state-contract.js +17 -7
- package/dist/studio-app/assets/{index-CGQV6zGa.js → index-3cvTQ0Mv.js} +4 -4
- package/dist/studio-app/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
use std::sync::{Arc, Mutex};
|
|
3
|
+
use serde::{Deserialize, Serialize};
|
|
4
|
+
use sha2::{Digest, Sha256};
|
|
5
|
+
|
|
6
|
+
/// Durable transaction idempotency: prevents re-execution of already-committed transactions.
|
|
7
|
+
/// Every committed transaction is stored with its result. If the client retries with the
|
|
8
|
+
/// same transaction_id + payload hash, the server returns the original result without re-executing.
|
|
9
|
+
|
|
10
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
11
|
+
pub struct CommittedTransaction {
|
|
12
|
+
pub transaction_id: String,
|
|
13
|
+
pub transaction_hash: String,
|
|
14
|
+
pub status: TransactionStatus,
|
|
15
|
+
pub committed_result: serde_json::Value,
|
|
16
|
+
pub commit_metadata: CommitMetadata,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
20
|
+
pub enum TransactionStatus {
|
|
21
|
+
#[serde(rename = "COMMITTED")]
|
|
22
|
+
Committed,
|
|
23
|
+
#[serde(rename = "CONFLICT")]
|
|
24
|
+
Conflict,
|
|
25
|
+
#[serde(rename = "VALIDATION_FAILED")]
|
|
26
|
+
ValidationFailed,
|
|
27
|
+
#[serde(rename = "INTERNAL_ERROR")]
|
|
28
|
+
InternalError,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
32
|
+
pub struct CommitMetadata {
|
|
33
|
+
pub timestamp_ms: u64,
|
|
34
|
+
pub revision_id: String,
|
|
35
|
+
pub application_id: String,
|
|
36
|
+
pub state_before: u64,
|
|
37
|
+
pub state_after: u64,
|
|
38
|
+
pub retry_count: usize,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pub struct TransactionIdempotencyStore {
|
|
42
|
+
// Map: transaction_id -> list of committed transactions (to detect hash mismatches)
|
|
43
|
+
store: Arc<Mutex<HashMap<String, Vec<CommittedTransaction>>>>,
|
|
44
|
+
max_per_transaction: usize,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
impl TransactionIdempotencyStore {
|
|
48
|
+
pub fn new(max_per_transaction: usize) -> Self {
|
|
49
|
+
Self {
|
|
50
|
+
store: Arc::new(Mutex::new(HashMap::new())),
|
|
51
|
+
max_per_transaction,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Record a committed transaction for idempotency.
|
|
56
|
+
/// If the same transaction_id is seen again with the same hash, return the original result.
|
|
57
|
+
/// If the same transaction_id is seen with a different hash, return CONFLICT.
|
|
58
|
+
pub fn record_committed(
|
|
59
|
+
&self,
|
|
60
|
+
transaction_id: String,
|
|
61
|
+
transaction_hash: String,
|
|
62
|
+
status: TransactionStatus,
|
|
63
|
+
result: serde_json::Value,
|
|
64
|
+
metadata: CommitMetadata,
|
|
65
|
+
) {
|
|
66
|
+
if let Ok(mut store) = self.store.lock() {
|
|
67
|
+
let tx_id_clone = transaction_id.clone();
|
|
68
|
+
let committed = CommittedTransaction {
|
|
69
|
+
transaction_id,
|
|
70
|
+
transaction_hash,
|
|
71
|
+
status,
|
|
72
|
+
committed_result: result,
|
|
73
|
+
commit_metadata: metadata,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
store
|
|
77
|
+
.entry(tx_id_clone.clone())
|
|
78
|
+
.or_insert_with(Vec::new)
|
|
79
|
+
.push(committed);
|
|
80
|
+
|
|
81
|
+
// Keep only the most recent attempts
|
|
82
|
+
if let Some(entries) = store.get_mut(&tx_id_clone) {
|
|
83
|
+
if entries.len() > self.max_per_transaction {
|
|
84
|
+
let to_remove = entries.len() - self.max_per_transaction;
|
|
85
|
+
entries.drain(0..to_remove);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Check if a transaction has been seen before.
|
|
92
|
+
/// Returns:
|
|
93
|
+
/// - Some(Ok(result)) if same transaction_id + same hash (idempotent replay)
|
|
94
|
+
/// - Some(Err("CONFLICT")) if same transaction_id + different hash (reuse with different payload)
|
|
95
|
+
/// - None if this is a new transaction_id
|
|
96
|
+
pub fn check_idempotent(
|
|
97
|
+
&self,
|
|
98
|
+
transaction_id: &str,
|
|
99
|
+
transaction_hash: &str,
|
|
100
|
+
) -> Option<Result<serde_json::Value, String>> {
|
|
101
|
+
self.store
|
|
102
|
+
.lock()
|
|
103
|
+
.ok()
|
|
104
|
+
.and_then(|store| store.get(transaction_id).map(|entries| entries.clone()))
|
|
105
|
+
.and_then(|entries| {
|
|
106
|
+
if entries.is_empty() {
|
|
107
|
+
return None;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let first_entry = &entries[0];
|
|
111
|
+
|
|
112
|
+
// If hashes match, return the original result
|
|
113
|
+
if first_entry.transaction_hash == transaction_hash {
|
|
114
|
+
return Some(Ok(first_entry.committed_result.clone()));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// If hashes differ, it's a conflict (transaction ID reused with different payload)
|
|
118
|
+
Some(Err(format!(
|
|
119
|
+
"Transaction ID {} reused with different payload",
|
|
120
|
+
transaction_id
|
|
121
|
+
)))
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Get all committed transactions for a transaction_id
|
|
126
|
+
pub fn get_history(&self, transaction_id: &str) -> Vec<CommittedTransaction> {
|
|
127
|
+
self.store
|
|
128
|
+
.lock()
|
|
129
|
+
.ok()
|
|
130
|
+
.and_then(|store| store.get(transaction_id).cloned())
|
|
131
|
+
.unwrap_or_default()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Clear all idempotency records (for testing)
|
|
135
|
+
pub fn clear(&self) {
|
|
136
|
+
if let Ok(mut store) = self.store.lock() {
|
|
137
|
+
store.clear();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Get statistics about idempotency store
|
|
142
|
+
pub fn statistics(&self) -> IdempotencyStatistics {
|
|
143
|
+
self.store
|
|
144
|
+
.lock()
|
|
145
|
+
.ok()
|
|
146
|
+
.map(|store| {
|
|
147
|
+
let total_transactions = store.len();
|
|
148
|
+
let total_entries = store.values().map(|v| v.len()).sum();
|
|
149
|
+
let replay_protected = store
|
|
150
|
+
.values()
|
|
151
|
+
.filter(|entries| !entries.is_empty() && entries[0].status == TransactionStatus::Committed)
|
|
152
|
+
.count();
|
|
153
|
+
|
|
154
|
+
IdempotencyStatistics {
|
|
155
|
+
total_transaction_ids: total_transactions,
|
|
156
|
+
total_committed_entries: total_entries,
|
|
157
|
+
replay_protected_count: replay_protected,
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
.unwrap_or_default()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#[derive(Debug, Clone, Serialize, Default)]
|
|
165
|
+
pub struct IdempotencyStatistics {
|
|
166
|
+
pub total_transaction_ids: usize,
|
|
167
|
+
pub total_committed_entries: usize,
|
|
168
|
+
pub replay_protected_count: usize,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Compute a hash of a transaction request for idempotency detection.
|
|
172
|
+
pub fn hash_transaction_request(request: &serde_json::Value) -> String {
|
|
173
|
+
let mut hasher = Sha256::new();
|
|
174
|
+
let json_str = serde_json::to_string(request).unwrap_or_default();
|
|
175
|
+
hasher.update(json_str.as_bytes());
|
|
176
|
+
format!("{:x}", hasher.finalize())
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#[cfg(test)]
|
|
180
|
+
mod tests {
|
|
181
|
+
use super::*;
|
|
182
|
+
|
|
183
|
+
#[test]
|
|
184
|
+
fn test_idempotency_store_same_hash() {
|
|
185
|
+
let store = TransactionIdempotencyStore::new(10);
|
|
186
|
+
let tx_id = "tx-123".to_string();
|
|
187
|
+
let tx_hash = "hash-abc".to_string();
|
|
188
|
+
let result = serde_json::json!({ "status": "committed" });
|
|
189
|
+
|
|
190
|
+
store.record_committed(
|
|
191
|
+
tx_id.clone(),
|
|
192
|
+
tx_hash.clone(),
|
|
193
|
+
TransactionStatus::Committed,
|
|
194
|
+
result.clone(),
|
|
195
|
+
CommitMetadata {
|
|
196
|
+
timestamp_ms: 1000,
|
|
197
|
+
revision_id: "rev-1".to_string(),
|
|
198
|
+
application_id: "app-1".to_string(),
|
|
199
|
+
state_before: 1,
|
|
200
|
+
state_after: 2,
|
|
201
|
+
retry_count: 0,
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// First replay should return the same result
|
|
206
|
+
let replay = store.check_idempotent(&tx_id, &tx_hash);
|
|
207
|
+
assert!(replay.is_some());
|
|
208
|
+
assert!(replay.unwrap().is_ok());
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#[test]
|
|
212
|
+
fn test_idempotency_store_different_hash() {
|
|
213
|
+
let store = TransactionIdempotencyStore::new(10);
|
|
214
|
+
let tx_id = "tx-456".to_string();
|
|
215
|
+
let tx_hash_1 = "hash-1".to_string();
|
|
216
|
+
let tx_hash_2 = "hash-2".to_string();
|
|
217
|
+
let result = serde_json::json!({ "status": "committed" });
|
|
218
|
+
|
|
219
|
+
store.record_committed(
|
|
220
|
+
tx_id.clone(),
|
|
221
|
+
tx_hash_1.clone(),
|
|
222
|
+
TransactionStatus::Committed,
|
|
223
|
+
result,
|
|
224
|
+
CommitMetadata {
|
|
225
|
+
timestamp_ms: 1000,
|
|
226
|
+
revision_id: "rev-1".to_string(),
|
|
227
|
+
application_id: "app-1".to_string(),
|
|
228
|
+
state_before: 1,
|
|
229
|
+
state_after: 2,
|
|
230
|
+
retry_count: 0,
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Replay with different hash should fail
|
|
235
|
+
let replay = store.check_idempotent(&tx_id, &tx_hash_2);
|
|
236
|
+
assert!(replay.is_some());
|
|
237
|
+
assert!(replay.unwrap().is_err());
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[test]
|
|
241
|
+
fn test_hash_transaction_request() {
|
|
242
|
+
let request1 = serde_json::json!({ "id": "test", "value": 123 });
|
|
243
|
+
let request2 = serde_json::json!({ "id": "test", "value": 123 });
|
|
244
|
+
let request3 = serde_json::json!({ "id": "test", "value": 456 });
|
|
245
|
+
|
|
246
|
+
let hash1 = hash_transaction_request(&request1);
|
|
247
|
+
let hash2 = hash_transaction_request(&request2);
|
|
248
|
+
let hash3 = hash_transaction_request(&request3);
|
|
249
|
+
|
|
250
|
+
assert_eq!(hash1, hash2); // Same content = same hash
|
|
251
|
+
assert_ne!(hash1, hash3); // Different content = different hash
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#[test]
|
|
255
|
+
fn test_idempotency_statistics() {
|
|
256
|
+
let store = TransactionIdempotencyStore::new(10);
|
|
257
|
+
|
|
258
|
+
for i in 0..5 {
|
|
259
|
+
store.record_committed(
|
|
260
|
+
format!("tx-{}", i),
|
|
261
|
+
"hash-abc".to_string(),
|
|
262
|
+
TransactionStatus::Committed,
|
|
263
|
+
serde_json::json!({}),
|
|
264
|
+
CommitMetadata {
|
|
265
|
+
timestamp_ms: 1000,
|
|
266
|
+
revision_id: "rev-1".to_string(),
|
|
267
|
+
application_id: "app-1".to_string(),
|
|
268
|
+
state_before: 1,
|
|
269
|
+
state_after: 2,
|
|
270
|
+
retry_count: 0,
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let stats = store.statistics();
|
|
276
|
+
assert_eq!(stats.total_transaction_ids, 5);
|
|
277
|
+
assert_eq!(stats.total_committed_entries, 5);
|
|
278
|
+
assert_eq!(stats.replay_protected_count, 5);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeltDB Deterministic Error Codes
|
|
3
|
+
*
|
|
4
|
+
* All FeltDB API responses use these semantic error codes.
|
|
5
|
+
* Never returns empty {} or untyped errors.
|
|
6
|
+
*
|
|
7
|
+
* Clients must handle each code appropriately:
|
|
8
|
+
* - CONFLICT: Retry with backoff (CAS mismatch)
|
|
9
|
+
* - PRECONDITION_FAILED: Do not retry (validation error)
|
|
10
|
+
* - TOO_BUSY: Retry with exponential backoff (queue full)
|
|
11
|
+
* - INTERNAL_ERROR: Log and escalate (server error)
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Semantic error codes matching HTTP status codes
|
|
15
|
+
*/
|
|
16
|
+
export declare enum FeltDBErrorCode {
|
|
17
|
+
/** 200: Request succeeded */
|
|
18
|
+
OK = "OK",
|
|
19
|
+
/** 409: Version conflict; concurrent writer won; retry intelligently */
|
|
20
|
+
CONFLICT = "CONFLICT",
|
|
21
|
+
/** 422: Validation or precondition failed; do not retry */
|
|
22
|
+
PRECONDITION_FAILED = "PRECONDITION_FAILED",
|
|
23
|
+
/** 429: Queue depth exceeded; retry with exponential backoff */
|
|
24
|
+
TOO_BUSY = "TOO_BUSY",
|
|
25
|
+
/** 500: Unrecoverable server error; audit trail available */
|
|
26
|
+
INTERNAL_ERROR = "INTERNAL_ERROR"
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Error response structure that all FeltDB APIs must return
|
|
30
|
+
*/
|
|
31
|
+
export interface FeltDBErrorResponse {
|
|
32
|
+
/** Semantic error code (never empty) */
|
|
33
|
+
code: FeltDBErrorCode | string;
|
|
34
|
+
/** Human-readable message explaining what happened */
|
|
35
|
+
message: string;
|
|
36
|
+
/** Unique request ID for debugging/auditing */
|
|
37
|
+
request_id: string;
|
|
38
|
+
/** Transaction ID if applicable */
|
|
39
|
+
transaction_id?: string;
|
|
40
|
+
/** HTTP status code for routing */
|
|
41
|
+
http_status: number;
|
|
42
|
+
/** Recovery hint for client ("retry", "fail", "queue", etc.) */
|
|
43
|
+
recovery_hint?: 'retry_backoff' | 'dont_retry' | 'check_queue_depth' | 'contact_support';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Determines if a FeltDB error is retryable
|
|
47
|
+
*/
|
|
48
|
+
export declare function isRetryableError(code: FeltDBErrorCode | string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Determines retry strategy based on error code
|
|
51
|
+
*/
|
|
52
|
+
export declare function getRetryStrategy(code: FeltDBErrorCode | string): 'exponential_backoff' | 'linear_backoff' | 'dont_retry';
|
|
53
|
+
//# sourceMappingURL=error-codes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-codes.d.ts","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;GAEG;AACH,oBAAY,eAAe;IACzB,6BAA6B;IAC7B,EAAE,OAAO;IAET,wEAAwE;IACxE,QAAQ,aAAa;IAErB,2DAA2D;IAC3D,mBAAmB,wBAAwB;IAE3C,gEAAgE;IAChE,QAAQ,aAAa;IAErB,6DAA6D;IAC7D,cAAc,mBAAmB;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,wCAAwC;IACxC,IAAI,EAAE,eAAe,GAAG,MAAM,CAAC;IAE/B,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAEhB,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IAEnB,mCAAmC;IACnC,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,mCAAmC;IACnC,WAAW,EAAE,MAAM,CAAC;IAEpB,gEAAgE;IAChE,aAAa,CAAC,EAAE,eAAe,GAAG,YAAY,GAAG,mBAAmB,GAAG,iBAAiB,CAAC;CAC1F;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,GAAG,OAAO,CAExE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,GAAG,qBAAqB,GAAG,gBAAgB,GAAG,YAAY,CAQxH"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeltDB Deterministic Error Codes
|
|
3
|
+
*
|
|
4
|
+
* All FeltDB API responses use these semantic error codes.
|
|
5
|
+
* Never returns empty {} or untyped errors.
|
|
6
|
+
*
|
|
7
|
+
* Clients must handle each code appropriately:
|
|
8
|
+
* - CONFLICT: Retry with backoff (CAS mismatch)
|
|
9
|
+
* - PRECONDITION_FAILED: Do not retry (validation error)
|
|
10
|
+
* - TOO_BUSY: Retry with exponential backoff (queue full)
|
|
11
|
+
* - INTERNAL_ERROR: Log and escalate (server error)
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Semantic error codes matching HTTP status codes
|
|
15
|
+
*/
|
|
16
|
+
export var FeltDBErrorCode;
|
|
17
|
+
(function (FeltDBErrorCode) {
|
|
18
|
+
/** 200: Request succeeded */
|
|
19
|
+
FeltDBErrorCode["OK"] = "OK";
|
|
20
|
+
/** 409: Version conflict; concurrent writer won; retry intelligently */
|
|
21
|
+
FeltDBErrorCode["CONFLICT"] = "CONFLICT";
|
|
22
|
+
/** 422: Validation or precondition failed; do not retry */
|
|
23
|
+
FeltDBErrorCode["PRECONDITION_FAILED"] = "PRECONDITION_FAILED";
|
|
24
|
+
/** 429: Queue depth exceeded; retry with exponential backoff */
|
|
25
|
+
FeltDBErrorCode["TOO_BUSY"] = "TOO_BUSY";
|
|
26
|
+
/** 500: Unrecoverable server error; audit trail available */
|
|
27
|
+
FeltDBErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
|
|
28
|
+
})(FeltDBErrorCode || (FeltDBErrorCode = {}));
|
|
29
|
+
/**
|
|
30
|
+
* Determines if a FeltDB error is retryable
|
|
31
|
+
*/
|
|
32
|
+
export function isRetryableError(code) {
|
|
33
|
+
return code === FeltDBErrorCode.CONFLICT || code === FeltDBErrorCode.TOO_BUSY;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Determines retry strategy based on error code
|
|
37
|
+
*/
|
|
38
|
+
export function getRetryStrategy(code) {
|
|
39
|
+
if (code === FeltDBErrorCode.CONFLICT) {
|
|
40
|
+
return 'exponential_backoff';
|
|
41
|
+
}
|
|
42
|
+
if (code === FeltDBErrorCode.TOO_BUSY) {
|
|
43
|
+
return 'exponential_backoff';
|
|
44
|
+
}
|
|
45
|
+
return 'dont_retry';
|
|
46
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,9 @@ export * from './file-db.js';
|
|
|
31
31
|
export * from './flowspec.js';
|
|
32
32
|
export * from './application-manifest.js';
|
|
33
33
|
export * from './state-contract.js';
|
|
34
|
+
export * from './revision-recovery.js';
|
|
35
|
+
export * from './operation-admission.js';
|
|
36
|
+
export * from './error-codes.js';
|
|
34
37
|
export * from './authorization.js';
|
|
35
38
|
export * from './sync-contract.js';
|
|
36
39
|
export * from './workload.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC;AAC1C,OAAO,EACL,eAAe,EACf,KAAK,OAAO,EACZ,KAAK,SAAS,IAAI,iBAAiB,EACnC,KAAK,QAAQ,IAAI,gBAAgB,GAClC,MAAM,eAAe,CAAC;AAEvB;;GAEG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AAEnC;;GAEG;AACH,YAAY,EACV,eAAe,EACf,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC;AAC1C,OAAO,EACL,eAAe,EACf,KAAK,OAAO,EACZ,KAAK,SAAS,IAAI,iBAAiB,EACnC,KAAK,QAAQ,IAAI,gBAAgB,GAClC,MAAM,eAAe,CAAC;AAEvB;;GAEG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AAEnC;;GAEG;AACH,YAAY,EACV,eAAe,EACf,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,9 @@ export * from './file-db.js';
|
|
|
31
31
|
export * from './flowspec.js';
|
|
32
32
|
export * from './application-manifest.js';
|
|
33
33
|
export * from './state-contract.js';
|
|
34
|
+
export * from './revision-recovery.js';
|
|
35
|
+
export * from './operation-admission.js';
|
|
36
|
+
export * from './error-codes.js';
|
|
34
37
|
export * from './authorization.js';
|
|
35
38
|
export * from './sync-contract.js';
|
|
36
39
|
export * from './workload.js';
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revision Recovery: Audited Recovery from Invalid Historical Revisions
|
|
3
|
+
*
|
|
4
|
+
* Provides mechanisms to safely recover from corrupt/invalid historical revisions
|
|
5
|
+
* under strict safety constraints and comprehensive audit trail requirements.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Authorization level required for revision recovery operations
|
|
9
|
+
*/
|
|
10
|
+
export type RecoveryAuthorization = 'ELEVATED' | 'ADMIN' | 'EMERGENCY';
|
|
11
|
+
/**
|
|
12
|
+
* Compatibility classification for manifest changes
|
|
13
|
+
*/
|
|
14
|
+
export type CompatibilityClassification = 'COMPATIBLE' | 'WARNING' | 'INCOMPATIBLE';
|
|
15
|
+
/**
|
|
16
|
+
* Recovery mode indicating what was approved
|
|
17
|
+
*/
|
|
18
|
+
export type RecoveryMode = 'SAFE_COMPATIBLE' | 'WARNING_APPROVED' | 'DESTRUCTIVE_APPROVED';
|
|
19
|
+
/**
|
|
20
|
+
* Input for recovering an application revision
|
|
21
|
+
*/
|
|
22
|
+
export interface RevisionRecoveryInput {
|
|
23
|
+
/** Application ID being recovered */
|
|
24
|
+
applicationId: string;
|
|
25
|
+
/** Current revision that is invalid/corrupt (must match actual current) */
|
|
26
|
+
expectedCurrentRevision: string;
|
|
27
|
+
/** Target revision to promote to (must be valid and integrity-checked) */
|
|
28
|
+
targetRevision: string;
|
|
29
|
+
/** Elevated authorization level required */
|
|
30
|
+
authorization: RecoveryAuthorization;
|
|
31
|
+
/** Actor (user/service) approving recovery */
|
|
32
|
+
actor: string;
|
|
33
|
+
/** Explicit reason for recovery */
|
|
34
|
+
reason: string;
|
|
35
|
+
/** Environment being recovered */
|
|
36
|
+
environment?: string;
|
|
37
|
+
/** Allow destructive schema changes if compatibility check requires it */
|
|
38
|
+
allowDestructiveChange?: boolean;
|
|
39
|
+
/** Explicit reason for approving destructive changes (required if allowDestructiveChange=true) */
|
|
40
|
+
destructiveChangeReason?: string;
|
|
41
|
+
/** Idempotency key for recovery operation */
|
|
42
|
+
recoveryId?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Audit record for a completed revision recovery
|
|
46
|
+
*/
|
|
47
|
+
export interface RevisionRecoveryAudit {
|
|
48
|
+
/** Unique audit record ID */
|
|
49
|
+
auditId: string;
|
|
50
|
+
/** Application being recovered */
|
|
51
|
+
applicationId: string;
|
|
52
|
+
/** Source (corrupt) revision being abandoned */
|
|
53
|
+
sourceRevision: string;
|
|
54
|
+
/** Recomputed hash of source revision (for verification) */
|
|
55
|
+
sourceRevisionHash: string;
|
|
56
|
+
/** Target revision being promoted to */
|
|
57
|
+
targetRevision: string;
|
|
58
|
+
/** Hash of target revision */
|
|
59
|
+
targetRevisionHash: string;
|
|
60
|
+
/** Hash of previous audit record in chain (for immutability) */
|
|
61
|
+
previousAuditHash?: string;
|
|
62
|
+
/** Hash of this audit record */
|
|
63
|
+
auditHash: string;
|
|
64
|
+
/** Actor who approved recovery */
|
|
65
|
+
approvedBy: string;
|
|
66
|
+
/** Recovery reason provided */
|
|
67
|
+
reason: string;
|
|
68
|
+
/** Authorization level that was required */
|
|
69
|
+
authorizationLevel: RecoveryAuthorization;
|
|
70
|
+
/** Mode of recovery (what was approved) */
|
|
71
|
+
recoveryMode: RecoveryMode;
|
|
72
|
+
/** Compatibility classification from diff analysis */
|
|
73
|
+
compatibilityClassification: CompatibilityClassification;
|
|
74
|
+
/** Detailed compatibility issues found (if any) */
|
|
75
|
+
compatibilityIssues?: string[];
|
|
76
|
+
/** Destructive change approval reason (if applicable) */
|
|
77
|
+
destructiveChangeReason?: string;
|
|
78
|
+
/** Timestamp of recovery */
|
|
79
|
+
recoveredAt: number;
|
|
80
|
+
/** Idempotency key for the recovery operation */
|
|
81
|
+
recoveryId: string;
|
|
82
|
+
/** Status of recovery operation */
|
|
83
|
+
status: 'COMPLETED' | 'PARTIAL' | 'ROLLED_BACK';
|
|
84
|
+
/** Marker indicating source revision must never be moved back to */
|
|
85
|
+
markedUntrustedUntilRevision: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Result of a revision recovery attempt
|
|
89
|
+
*/
|
|
90
|
+
export interface RevisionRecoveryResult {
|
|
91
|
+
/** Whether recovery succeeded */
|
|
92
|
+
success: boolean;
|
|
93
|
+
/** Error code if recovery failed */
|
|
94
|
+
errorCode?: 'PERMISSION_DENIED' | 'CONFLICT' | 'INCOMPATIBLE_SCHEMA' | 'CORRUPT_REVISION_UNMOVABLE' | 'STORAGE_FAILURE' | 'VALIDATION_FAILED';
|
|
95
|
+
/** Human-readable error message */
|
|
96
|
+
errorMessage?: string;
|
|
97
|
+
/** Audit record created (present if recovery succeeds or partially completes) */
|
|
98
|
+
audit?: RevisionRecoveryAudit;
|
|
99
|
+
/** Current revision pointer after recovery attempt */
|
|
100
|
+
currentRevision: string;
|
|
101
|
+
/** Whether audit record was durable persisted */
|
|
102
|
+
auditDurable: boolean;
|
|
103
|
+
/** Pointer was successfully moved to target */
|
|
104
|
+
pointerMoved: boolean;
|
|
105
|
+
/** Source revision was marked untrusted */
|
|
106
|
+
sourceMarkedUntrusted: boolean;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Revision state tracking for corrupt/untrusted revisions
|
|
110
|
+
*/
|
|
111
|
+
export interface RevisionState {
|
|
112
|
+
/** Revision ID */
|
|
113
|
+
revisionId: string;
|
|
114
|
+
/** Whether revision is valid/trusted */
|
|
115
|
+
trusted: boolean;
|
|
116
|
+
/** Marker: any revision equal to or before this cannot be promoted to */
|
|
117
|
+
untrustedUntilRevision?: string;
|
|
118
|
+
/** Reason revision was marked untrusted (if applicable) */
|
|
119
|
+
untrustedReason?: string;
|
|
120
|
+
/** Timestamp when marked untrusted */
|
|
121
|
+
markedUntrustedAt?: number;
|
|
122
|
+
/** Reference to recovery audit that marked it untrusted */
|
|
123
|
+
recoveryAuditId?: string;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Compatibility check result
|
|
127
|
+
*/
|
|
128
|
+
export interface CompatibilityCheckResult {
|
|
129
|
+
/** Overall compatibility classification */
|
|
130
|
+
classification: CompatibilityClassification;
|
|
131
|
+
/** List of specific issues or warnings found */
|
|
132
|
+
issues: Array<{
|
|
133
|
+
category: 'SCHEMA_CHANGE' | 'DATA_LOSS' | 'REFERENCE_BREAKING' | 'INDEX_REMOVAL';
|
|
134
|
+
severity: 'WARNING' | 'INCOMPATIBLE';
|
|
135
|
+
detail: string;
|
|
136
|
+
}>;
|
|
137
|
+
/** Whether any data loss would occur */
|
|
138
|
+
causesDataLoss: boolean;
|
|
139
|
+
/** Whether this is a breaking change */
|
|
140
|
+
isBreakingChange: boolean;
|
|
141
|
+
/** Recommended action */
|
|
142
|
+
recommendation: string;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Validates revision recovery input
|
|
146
|
+
*/
|
|
147
|
+
export declare function validateRevisionRecoveryInput(input: RevisionRecoveryInput): {
|
|
148
|
+
valid: boolean;
|
|
149
|
+
error?: string;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Generates cryptographic hash for audit record chain
|
|
153
|
+
*/
|
|
154
|
+
export declare function generateAuditHash(data: Record<string, unknown>, previousHash?: string): string;
|
|
155
|
+
/**
|
|
156
|
+
* Checks if a revision pointer transition would violate untrust markers
|
|
157
|
+
*/
|
|
158
|
+
export declare function wouldViolateUntrustworthiness(fromRevision: string, toRevision: string, revisionStates: Map<string, RevisionState>): {
|
|
159
|
+
violates: boolean;
|
|
160
|
+
reason?: string;
|
|
161
|
+
};
|
|
162
|
+
//# sourceMappingURL=revision-recovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revision-recovery.d.ts","sourceRoot":"","sources":["../src/revision-recovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revision Recovery: Audited Recovery from Invalid Historical Revisions
|
|
3
|
+
*
|
|
4
|
+
* Provides mechanisms to safely recover from corrupt/invalid historical revisions
|
|
5
|
+
* under strict safety constraints and comprehensive audit trail requirements.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Validates revision recovery input
|
|
9
|
+
*/
|
|
10
|
+
export function validateRevisionRecoveryInput(input) {
|
|
11
|
+
if (!input.applicationId || typeof input.applicationId !== 'string') {
|
|
12
|
+
return { valid: false, error: 'applicationId must be a non-empty string' };
|
|
13
|
+
}
|
|
14
|
+
if (!input.expectedCurrentRevision || typeof input.expectedCurrentRevision !== 'string') {
|
|
15
|
+
return { valid: false, error: 'expectedCurrentRevision must be a non-empty string' };
|
|
16
|
+
}
|
|
17
|
+
if (!input.targetRevision || typeof input.targetRevision !== 'string') {
|
|
18
|
+
return { valid: false, error: 'targetRevision must be a non-empty string' };
|
|
19
|
+
}
|
|
20
|
+
if (!input.authorization || !['ELEVATED', 'ADMIN', 'EMERGENCY'].includes(input.authorization)) {
|
|
21
|
+
return { valid: false, error: 'authorization must be ELEVATED, ADMIN, or EMERGENCY' };
|
|
22
|
+
}
|
|
23
|
+
if (!input.actor || typeof input.actor !== 'string') {
|
|
24
|
+
return { valid: false, error: 'actor must be a non-empty string' };
|
|
25
|
+
}
|
|
26
|
+
if (!input.reason || typeof input.reason !== 'string' || input.reason.length < 10) {
|
|
27
|
+
return { valid: false, error: 'reason must be a string with at least 10 characters' };
|
|
28
|
+
}
|
|
29
|
+
if (input.expectedCurrentRevision === input.targetRevision) {
|
|
30
|
+
return { valid: false, error: 'expectedCurrentRevision and targetRevision must be different' };
|
|
31
|
+
}
|
|
32
|
+
if (input.allowDestructiveChange === true) {
|
|
33
|
+
if (!input.destructiveChangeReason || typeof input.destructiveChangeReason !== 'string' || input.destructiveChangeReason.length < 10) {
|
|
34
|
+
return { valid: false, error: 'destructiveChangeReason must be provided and at least 10 characters when allowDestructiveChange=true' };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { valid: true };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Generates cryptographic hash for audit record chain
|
|
41
|
+
*/
|
|
42
|
+
export function generateAuditHash(data, previousHash) {
|
|
43
|
+
// In production, use SHA-256 or similar
|
|
44
|
+
// For now, use a simple hash based on JSON serialization
|
|
45
|
+
const payload = JSON.stringify({ ...data, previousHash });
|
|
46
|
+
// Use Buffer for Node.js, or btoa for browser environments
|
|
47
|
+
const encoded = typeof btoa !== 'undefined'
|
|
48
|
+
? btoa(payload)
|
|
49
|
+
: Buffer.from(payload).toString('base64');
|
|
50
|
+
return encoded.substring(0, 64);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Checks if a revision pointer transition would violate untrust markers
|
|
54
|
+
*/
|
|
55
|
+
export function wouldViolateUntrustworthiness(fromRevision, toRevision, revisionStates) {
|
|
56
|
+
const state = revisionStates.get(fromRevision);
|
|
57
|
+
if (!state || !state.untrustedUntilRevision) {
|
|
58
|
+
return { violates: false };
|
|
59
|
+
}
|
|
60
|
+
// Check if trying to move back to or through an untrusted revision
|
|
61
|
+
// This is a simplified check; production would need proper revision ordering
|
|
62
|
+
if (toRevision === state.untrustedUntilRevision || state.untrustedUntilRevision === fromRevision) {
|
|
63
|
+
return {
|
|
64
|
+
violates: true,
|
|
65
|
+
reason: `Revision ${fromRevision} is marked untrusted and cannot be moved back to`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { violates: false };
|
|
69
|
+
}
|
package/dist/state-contract.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RevisionRecoveryInput, RevisionRecoveryResult } from './revision-recovery';
|
|
1
2
|
/** Transport types for the Rust-owned FeltDB state contract. */
|
|
2
3
|
export type PrimitiveType = 'string' | 'integer' | 'number' | 'boolean' | 'timestamp' | 'date' | 'time' | 'json' | 'uuid' | 'decimal' | 'money' | 'bigint' | 'email' | 'url' | 'phone' | 'binary' | 'file' | 'geo_point' | 'object';
|
|
3
4
|
export type FieldType = {
|
|
@@ -174,6 +175,7 @@ export declare class StateContractClient {
|
|
|
174
175
|
transactionId?: string;
|
|
175
176
|
causalParent?: number;
|
|
176
177
|
}): Promise<TransactionResult>;
|
|
178
|
+
recoverApplicationRevision(input: RevisionRecoveryInput): Promise<RevisionRecoveryResult>;
|
|
177
179
|
}
|
|
178
180
|
export {};
|
|
179
181
|
//# sourceMappingURL=state-contract.d.ts.map
|