@@ -17,12 +17,10 @@ import (
1717
1818 "github.com/kontext-security/kontext-cli/internal/diagnostic"
1919 "github.com/kontext-security/kontext-cli/internal/guard/store/sqlite"
20+ "github.com/kontext-security/kontext-cli/internal/ledger"
2021)
2122
2223const (
23- SchemaVersion = "authorization-ledger-v1"
24- DefaultEndpoint = "/api/v1/authorization-ledger/batches"
25-
2624 DefaultBatchLimit = 500
2725 DefaultInterval = 10 * time .Second
2826
@@ -45,27 +43,9 @@ type Options struct {
4543 Diagnostic diagnostic.Logger
4644}
4745
48- type Payload struct {
49- SchemaVersion string `json:"schema_version"`
50- OrganizationID string `json:"organization_id"`
51- InstallationID string `json:"installation_id"`
52- BatchID string `json:"batch_id"`
53- SentAt string `json:"sent_at"`
54- Device * Device `json:"device,omitempty"`
55- Sessions []sqlite.LedgerRecord `json:"agent_sessions"`
56- Actions []sqlite.LedgerRecord `json:"authorization_actions"`
57- Receipts []sqlite.LedgerRecord `json:"authorization_receipts"`
58- ReceiptChainAnchor * sqlite.LedgerReceiptChainAnchor `json:"receipt_chain_anchor,omitempty"`
59- }
60-
61- type Device struct {
62- Label string `json:"label,omitempty"`
63- DeploymentVersion string `json:"deployment_version,omitempty"`
64- }
65-
6646type State struct {
67- UpdatedAfter string `json:"updated_after,omitempty"`
68- ActionID string `json:"action_id,omitempty"`
47+ UpdatedAfter * time. Time
48+ ActionID string
6949}
7050
7151func Run (ctx context.Context , opts Options ) error {
@@ -114,21 +94,12 @@ func Flush(ctx context.Context, opts Options) error {
11494 return err
11595 }
11696
117- var updatedAfter * time.Time
118- if state .UpdatedAfter != "" {
119- parsed , err := time .Parse (time .RFC3339Nano , state .UpdatedAfter )
120- if err != nil {
121- return fmt .Errorf ("parse managed stream state: %w" , err )
122- }
123- updatedAfter = & parsed
124- }
125-
12697 limit := opts .BatchLimit
12798 if limit <= 0 {
12899 limit = DefaultBatchLimit
129100 }
130101 batch , err := store .LedgerBatch (ctx , sqlite.LedgerExportOptions {
131- UpdatedAfter : updatedAfter ,
102+ UpdatedAfter : state . UpdatedAfter ,
132103 UpdatedAfterID : state .ActionID ,
133104 Limit : limit ,
134105 })
@@ -139,8 +110,8 @@ func Flush(ctx context.Context, opts Options) error {
139110 return nil
140111 }
141112
142- payload := Payload {
143- SchemaVersion : SchemaVersion ,
113+ payload := ledger. Payload {
114+ SchemaVersion : ledger . SchemaVersion ,
144115 OrganizationID : opts .OrganizationID ,
145116 InstallationID : opts .InstallationID ,
146117 BatchID : "batch_" + uuid .NewString (),
@@ -158,22 +129,23 @@ func Flush(ctx context.Context, opts Options) error {
158129 deploymentVersion = strings .TrimSpace (opts .DeploymentVersion ())
159130 }
160131 if label != "" || deploymentVersion != "" {
161- payload .Device = & Device {Label : label , DeploymentVersion : deploymentVersion }
132+ payload .Device = & ledger. Device {Label : label , DeploymentVersion : deploymentVersion }
162133 }
163134 if err := post (ctx , opts , payload ); err != nil {
164135 return err
165136 }
166137
167138 if batch .Cursor != nil {
139+ updatedAfter := batch .Cursor .UpdatedAt .UTC ()
168140 return SaveState (statePath , State {
169- UpdatedAfter : batch . Cursor . UpdatedAt . UTC (). Format ( time . RFC3339Nano ) ,
141+ UpdatedAfter : & updatedAfter ,
170142 ActionID : batch .Cursor .ActionID ,
171143 })
172144 }
173145 return nil
174146}
175147
176- func post (ctx context.Context , opts Options , payload Payload ) error {
148+ func post (ctx context.Context , opts Options , payload ledger. Payload ) error {
177149 body , err := json .Marshal (payload )
178150 if err != nil {
179151 return err
@@ -209,7 +181,7 @@ func endpointURL(cloudURL string) (string, error) {
209181 if err != nil {
210182 return "" , err
211183 }
212- parsed .Path = DefaultEndpoint
184+ parsed .Path = ledger . DefaultEndpoint
213185 parsed .RawQuery = ""
214186 parsed .Fragment = ""
215187 return parsed .String (), nil
@@ -252,20 +224,53 @@ func LoadState(path string) (State, error) {
252224 }
253225 return State {}, err
254226 }
255- var state State
227+
228+ type diskState struct {
229+ UpdatedAfter string `json:"updated_after,omitempty"`
230+ ActionID string `json:"action_id,omitempty"`
231+ }
232+
233+ var state diskState
256234 if err := json .Unmarshal (data , & state ); err != nil {
257235 return State {}, err
258236 }
259- state .UpdatedAfter = strings .TrimSpace (state .UpdatedAfter )
260- state .ActionID = strings .TrimSpace (state .ActionID )
261- return state , nil
237+
238+ updatedAfter := strings .TrimSpace (state .UpdatedAfter )
239+ actionID := strings .TrimSpace (state .ActionID )
240+
241+ var parsedUpdatedAfter * time.Time
242+ if updatedAfter != "" {
243+ parsed , err := time .Parse (time .RFC3339Nano , updatedAfter )
244+ if err != nil {
245+ return State {}, fmt .Errorf ("parse managed stream state updated_after: %w" , err )
246+ }
247+ parsedUpdatedAfter = & parsed
248+ }
249+
250+ return State {
251+ UpdatedAfter : parsedUpdatedAfter ,
252+ ActionID : actionID ,
253+ }, nil
262254}
263255
264- func SaveState (path string , state State ) error {
256+ func SaveState (path string , state State ) ( err error ) {
265257 if err := os .MkdirAll (filepath .Dir (path ), 0o700 ); err != nil {
266258 return err
267259 }
268- data , err := json .MarshalIndent (state , "" , " " )
260+
261+ type diskState struct {
262+ UpdatedAfter string `json:"updated_after,omitempty"`
263+ ActionID string `json:"action_id,omitempty"`
264+ }
265+
266+ updatedAfter := ""
267+ if state .UpdatedAfter != nil {
268+ updatedAfter = state .UpdatedAfter .UTC ().Format (time .RFC3339Nano )
269+ }
270+ data , err := json .MarshalIndent (diskState {
271+ UpdatedAfter : updatedAfter ,
272+ ActionID : strings .TrimSpace (state .ActionID ),
273+ }, "" , " " )
269274 if err != nil {
270275 return err
271276 }
@@ -275,22 +280,35 @@ func SaveState(path string, state State) error {
275280 return err
276281 }
277282 tempPath := temp .Name ()
283+ closed := false
278284 cleanup := true
279285 defer func () {
280286 if cleanup {
281- _ = os .Remove (tempPath )
287+ var cleanupErr error
288+ if ! closed {
289+ cleanupErr = errors .Join (cleanupErr , temp .Close ())
290+ }
291+ cleanupErr = errors .Join (cleanupErr , os .Remove (tempPath ))
292+ if cleanupErr == nil {
293+ return
294+ }
295+ if err == nil {
296+ err = cleanupErr
297+ return
298+ }
299+ err = errors .Join (err , cleanupErr )
282300 }
283301 }()
284302 if err := temp .Chmod (0o600 ); err != nil {
285- _ = temp .Close ()
286303 return err
287304 }
288305 if _ , err := temp .Write (data ); err != nil {
289- _ = temp .Close ()
290306 return err
291307 }
292- if err := temp .Close (); err != nil {
293- return err
308+ closeErr := temp .Close ()
309+ closed = true
310+ if closeErr != nil {
311+ return closeErr
294312 }
295313 if err := os .Rename (tempPath , path ); err != nil {
296314 return err
0 commit comments