Skip to content

Stores are embedder-supplied persistence hooks. State stores hold durable provider state. Continuation stores hold in-flight resume artifacts produced by action-required backend result envelopes.

State Store Functions

GLN_API gln_status_t GLN_CALL gln_create_state_store(
    const gln_state_store_vtable_t* in_vtable,
    gln_state_store_t**             out_store,
    gln_error_t*                    out_error);

GLN_API gln_status_t GLN_CALL gln_create_file_state_store(
    const char*         in_state_path,
    const char*         in_key_path_or_null,
    gln_state_store_t** out_store,
    gln_error_t*        out_error);

GLN_API void GLN_CALL gln_destroy_state_store(gln_state_store_t* in_store);

gln_open_fints_backend, gln_open_revolut_backend, and gln_open_wise_backend consume state stores. FinTS retains the underlying state-store implementation during backend open, so the C store handle itself can be destroyed after a successful FinTS open. Revolut and Wise keep non-owning references to the C store handle; keep that handle alive until after closing every Revolut or Wise backend that uses it.

The file-backed constructor stores an encrypted blob at in_state_path. in_key_path_or_null selects the encryption-key sidecar path; pass NULL to use the implementation default. Destroy the returned handle with gln_destroy_state_store.

State Store Vtable

typedef struct {
    gln_status_t (GLN_CALL *load)(
        void*     in_user_data,
        uint8_t** out_payload,
        size_t*   out_len);
    gln_status_t (GLN_CALL *save)(
        void*          in_user_data,
        const uint8_t* in_payload,
        size_t         in_len);
    void (GLN_CALL *free_payload)(
        void*    in_user_data,
        uint8_t* in_payload);
    void* user_data;
} gln_state_store_vtable_t;

Field contract:

  • load returns the previously saved blob. On GLN_OK, write a fresh allocation to *out_payload and its length to *out_len. A zero-length blob may use NULL with *out_len == 0. Return GLN_ERR_NOT_FOUND when no state has been saved yet.
  • save persists in_payload[0..in_len). The buffer is library-owned and valid only during the callback. Copy or durably write it before returning.
  • free_payload releases a non-null buffer previously returned by load. It must use the same allocator family that load used.
  • user_data is passed back unchanged to every callback. The library does not inspect or copy it.

The direction of ownership is the practical rule: load produces a buffer that the library later returns to free_payload; save receives a borrowed library-owned buffer that the store must not retain.

Continuation Store Functions

GLN_API gln_status_t GLN_CALL gln_create_continuation_store(
    const gln_continuation_store_vtable_t* in_vtable,
    gln_secret_t*                          in_encryption_key,
    gln_continuation_store_t**             out_store,
    gln_error_t*                           out_error);

GLN_API gln_status_t GLN_CALL gln_create_file_continuation_store(
    const char*                in_directory_path,
    gln_secret_t*              in_encryption_key,
    gln_continuation_store_t** out_store,
    gln_error_t*               out_error);

GLN_API void GLN_CALL gln_destroy_continuation_store(gln_continuation_store_t* in_store);

gln_open_fints_backend accepts an optional continuation store. Pass NULL if resume artifacts do not need to survive process boundaries. When a backend result envelope contains an action-required continuation, take it with gln_take_backend_result_continuation, save it with gln_save_continuation, and destroy the in-memory handle when finished.

in_encryption_key is required for both constructors and must wrap exactly 32 bytes of AES-256-GCM key material. The library encrypts every continuation at the C-API boundary; the user-supplied vtable callbacks therefore see ciphertext (header || nonce || ciphertext || tag) rather than plaintext. The store retains its own owning copy of the key bytes, so the caller may destroy in_encryption_key after the constructor returns.

The file-backed continuation store writes encrypted resume blobs under in_directory_path. Where the 32 bytes come from (sidecar file, OS keyring, env var, password derivation) is the caller's responsibility.

Continuation Store Vtable

typedef struct {
    gln_status_t (GLN_CALL *load)(
        void*       in_user_data,
        const char* in_id,
        uint8_t**   out_payload,
        size_t*     out_len);
    gln_status_t (GLN_CALL *save)(
        void*          in_user_data,
        const char*    in_id,
        const uint8_t* in_payload,
        size_t         in_len);
    void (GLN_CALL *free_payload)(
        void*    in_user_data,
        uint8_t* in_payload);
    void* user_data;
} gln_continuation_store_vtable_t;

The ownership contract is the same as the state store. The extra in_id parameter is a library-owned string valid only during the callback. Copy it if the backing store needs to keep it after the callback returns.

The bytes the library hands save are ciphertext (header || nonce || ciphertext || tag) under the AES-256-GCM key supplied at store creation. The bytes returned from load must be the same ciphertext envelope; the library decrypts internally before any higher layer sees the plaintext. A custom store therefore never needs its own encryption layer.

Return GLN_ERR_NOT_FOUND when no artifact exists for in_id. Return any non-OK status to surface load failures from the backing storage. The library returns GLN_ERR_RESUME_ARTIFACT_INVALID itself if the loaded ciphertext fails to decrypt or to parse, so the store implementation does not need to classify malformed bytes.

Custom Store Skeleton

struct mem_state {
    uint8_t* bytes;
    size_t   len;
};

static gln_status_t mem_load(
    void* in_user_data,
    uint8_t** out_payload,
    size_t* out_len)
{
    struct mem_state* state = (struct mem_state*)in_user_data;
    if (state->bytes == NULL) {
        return GLN_ERR_NOT_FOUND;
    }

    uint8_t* copy = (uint8_t*)malloc(state->len);
    if (copy == NULL && state->len != 0) {
        return GLN_ERR_OUT_OF_MEMORY;
    }

    if (state->len != 0) {
        memcpy(copy, state->bytes, state->len);
    }
    *out_payload = copy;
    *out_len = state->len;
    return GLN_OK;
}

static gln_status_t mem_save(
    void* in_user_data,
    const uint8_t* in_payload,
    size_t in_len)
{
    struct mem_state* state = (struct mem_state*)in_user_data;
    uint8_t* copy = NULL;

    if (in_len != 0) {
        copy = (uint8_t*)malloc(in_len);
        if (copy == NULL) {
            return GLN_ERR_OUT_OF_MEMORY;
        }
        memcpy(copy, in_payload, in_len);
    }

    free(state->bytes);
    state->bytes = copy;
    state->len = in_len;
    return GLN_OK;
}

static void mem_free_payload(void* in_user_data, uint8_t* in_payload)
{
    (void)in_user_data;
    free(in_payload);
}

static struct mem_state state = { NULL, 0 };
static const gln_state_store_vtable_t vtable = {
    mem_load,
    mem_save,
    mem_free_payload,
    &state,
};

For continuation stores, add the in_id parameter to load and save and use it as the key in the backing store.

Practical Rules

  • Keep state stores profile-scoped. Sharing one state file across unrelated profiles will mix provider state.
  • Keep continuation IDs narrow and explicit. Include enough application context to avoid one user's resume artifact overwriting another's.
  • Complete durable writes before returning GLN_OK from save.
  • Never retain in_payload after save returns.
  • Never free a load buffer directly from library code or host code; implement free_payload and let the library call it.
  • Keep user_data alive until no live custom store handle or retained backend store implementation can invoke its callbacks.
  • For Revolut and Wise state stores, destroy the C store handle only after all backends using it have been closed.

See Also