NAME

Concierge - Service layer orchestrator for authentication, sessions, and user data

VERSION

v0.10.0

SYNOPSIS

use Concierge;

# Open an existing desk (created by Concierge::Desk::Setup)
my $desk = Concierge->open_desk('./desk');
my $concierge = $desk->{concierge};

# Register a user
$concierge->add_user({
    user_id  => 'alice',
    moniker  => 'Alice',
    email    => 'alice@example.com',
    password => 'secret123',
});

# Log in -- returns a Concierge::Desk::User object
my $login = $concierge->login_user({
    user_id  => 'alice',
    password => 'secret123',
});
my $user = $login->{user};

# User object provides direct access
say $user->moniker;         # "Alice"
say $user->session_id;      # random hex string
say $user->is_logged_in;    # 1

# Restore user from a cookie on next request
my $restore = $concierge->restore_user($user->user_key);
my $same_user = $restore->{user};

# Log out
$concierge->logout_user($user->session_id);

DESCRIPTION

Concierge coordinates three component modules behind a single API:

  • Concierge::Auth -- password authentication (Argon2)

  • Concierge::Sessions -- session management (SQLite or file backends)

  • Concierge::Users -- user data storage (SQLite, YAML, or CSV/TSV backends)

Applications interact only with Concierge and the Concierge::Desk::User objects it returns. The component modules are never exposed directly.

What the Suite Provides

Concierge handles orchestration -- coordinating components, managing the user_key mapping, and returning consistent structured results. The capabilities of the suite live in the three components:

Authentication (Concierge::Auth): Argon2id password hashing and verification; no plaintext credentials are ever written to disk. Also provides random token, UUID, word-passphrase, and hex-ID generators. The component is substitutable: any replacement implementing the same method contract (authenticate, enroll, change_credentials, etc. -- see Concierge::Auth::Base) can replace it for LDAP, OAuth, or any other scheme.

Sessions (Concierge::Sessions): Full session lifecycle -- creation, retrieval, expiry, and cleanup -- with SQLite, file, or in-memory backends. Sessions carry arbitrary key/value data. A single-session-per-user policy is enforced: creating a new session automatically removes any prior session for that user. Expired sessions are cleaned up each time a desk is opened.

User Records (Concierge::Users): User data store with a configurable field schema. Standard fields (moniker, email, phone, access_level, user_status, term_ends, and others) are built in and can be selectively overridden. Applications add their own fields via app_fields at setup time. Supports SQLite, YAML, and CSV/TSV backends, with filtering and listing operations.

For the full API of any component, see its own documentation.

Desks

A desk is a storage directory containing the configuration and data files for all three components. Use Concierge::Desk::Setup to create a desk, then open_desk() to load it at runtime.

User Participation Levels

Concierge provides three graduated levels of user participation, each returning a Concierge::Desk::User object:

Visitor -- admit_visitor()

Assigned a unique identifier only. No session, no stored data. Suitable for anonymous tracking (e.g., cookies).

Guest -- checkin_guest()

Assigned an identifier and a session. Can store temporary data (e.g., a shopping cart). No authentication or persistent user record.

Logged-in user -- login_user()

Authenticated with credentials. Has a session, persistent user data, and full access to the User object's data methods.

A guest can be converted to a logged-in user with login_guest(), transferring any session data accumulated during the guest session.

User Keys

Each active user (guest or logged-in) is tracked by a user_key -- a random token stored in the concierge's user_keys mapping alongside the user's user_id and session_id. This mapping is persisted to user_keys.json in the desk directory and synchronized against active sessions when the desk is opened.

Return Values

All methods return a hashref with at least success (0 or 1) and message:

# Success
{ success => 1, message => '...', ... }

# Failure
{ success => 0, message => 'error description' }

Success responses include additional fields relevant to the operation:

  • User lifecycle methods (login_user(), restore_user(), checkin_guest(), admit_visitor(), login_guest()) return user, a Concierge::Desk::User object. Guest and visitor results also set is_guest or is_visitor to 1.

  • open_desk() returns concierge, the ready-to-use Concierge object.

  • User management methods return user_id. remove_user() also returns deleted_from (arrayref of component names) and, if any deletion failed, warnings (arrayref).

  • verify_user() returns verified (0 or 1), exists_in_auth, and exists_in_users.

  • list_users() returns user_ids (arrayref) and count. With include_data => 1, also returns users (hashref keyed by user_id).

See the individual method descriptions below for the complete field list.

Methods never croak during normal operation. The one exception is open_desk(), which croaks if the desk directory does not exist.

Architecture

Concierge ships with three identity core components:

Concierge::Auth -- credential storage and verification
Concierge::Sessions -- session lifecycle and persistence
Concierge::Users -- user records with configurable field schemas

These three are tightly orchestrated: a single login_user() call authenticates via Auth, retrieves a record from Users, and creates a session through Sessions. This coordination is the purpose of Concierge -- applications interact with the Concierge API and the Concierge::Desk::User objects it returns, not with the components directly.

The identity core is designed to be sufficient on its own, but the component pattern it follows -- backend abstraction, setup-time configuration, and Concierge-level orchestration -- is intentionally replicable. Each identity core component can also be substituted with a conforming replacement, and additional components (Organizations, Assets, etc.) can be added by following the same conventions. See "EXTENSIBILITY" for details.

METHODS

Desk Management

open_desk

my $result = Concierge->open_desk($desk_location);
my $concierge = $result->{concierge};

Opens an existing desk directory created by Concierge::Desk::Setup. Reads the configuration file, instantiates all component modules, loads the user_keys mapping, and runs session cleanup.

Croaks if $desk_location is not an existing directory.

Returns { success => 1, concierge => $obj } on success.

new_concierge

my $concierge = Concierge->new_concierge();

Low-level constructor: returns a bare, uninitialized Concierge object with no components attached. Used internally by open_desk(). Application code should use open_desk() (or Concierge::Desk::Setup::build_desk() for first-time setup) rather than calling this directly.

save_user_keys

my $result = $concierge->save_user_keys();

Writes the in-memory user_keys mapping (user_key => session/user_id lookup data, used by restore_user()) to persistent storage as JSON. Called automatically after any operation that changes the mapping (login, logout, guest promotion, user removal, etc.); applications do not normally need to call it directly.

Returns { success => 1 } on success, or { success => 0, message => '...' } if the file cannot be written.

Component Accessors

auth, sessions, users

my $auth_obj     = $concierge->auth;
my $sessions_obj = $concierge->sessions;
my $users_obj    = $concierge->users;

Read-only accessors returning the live, already-instantiated component object ( Concierge::Auth, Concierge::Sessions, or Concierge::Users, or a substitute -- see "EXTENSIBILITY" ) for direct use when an operation isn't exposed as a Concierge-level convenience method. This is the same "bare accessor" escape hatch described in "Component Substitution" and Concierge::Desk::Component's promote mechanism -- it remains available regardless of what a component chooses to promote onto $concierge directly.

User Lifecycle

admit_visitor

my $result = $concierge->admit_visitor();
my $user = $result->{user};    # Concierge::Desk::User (visitor)

Creates a visitor with a generated identifier. No session is created and no data is stored.

checkin_guest

my $result = $concierge->checkin_guest(\%session_opts);
my $user = $result->{user};    # Concierge::Desk::User (guest)

Creates a guest with a generated identifier and a session. The optional %session_opts hashref may include timeout (in seconds; defaults to 1800).

login_user

my $result = $concierge->login_user(\%credentials, \%session_opts);
my $user = $result->{user};    # Concierge::Desk::User (logged-in)

Authenticates user_id and password from %credentials, retrieves the user's data record, creates a session, and returns a fully-equipped User object. If the user already has an active session, the previous session is replaced.

restore_user

my $result = $concierge->restore_user($user_key);
my $user = $result->{user};    # Concierge::Desk::User (guest or logged-in)

Reconstructs a User object from a user_key (typically stored in a cookie or URL token). Looks up the key in the concierge mapping, validates the session, and determines whether the user is a guest or logged-in user.

Logged-in users are restored with their full user data snapshot and backend closures. Guests are restored with their session only.

If the session has expired, the stale mapping entry is cleaned up and the method returns failure. The application can then redirect to login or create a new guest as appropriate.

Returns { success => 1, user => $user } on success. Guest restores also include is_guest => 1.

login_guest

my $result = $concierge->login_guest(\%credentials, $guest_user_key);
my $user = $result->{user};    # Concierge::Desk::User (logged-in)

Converts a guest to a logged-in user. Authenticates with %credentials, transfers any data from the guest's session to the new session, then deletes the guest session and removes the guest's user_key mapping.

logout_user

my $result = $concierge->logout_user($session_id);

Deletes the session and removes the user_key mapping entry.

Admin Operations

add_user

my $result = $concierge->add_user(\%user_input);

Registers a new user. %user_input must include user_id, moniker, and password. Any additional fields (email, phone, application- defined fields, etc.) are stored in the Users component. The password is stored separately in the Auth component and never reaches the user data store.

If password validation fails, the Users record is rolled back.

remove_user

my $result = $concierge->remove_user($user_id);

Removes the user from all components: Users, Auth, Sessions, and the user_keys mapping. Attempts all deletions; the response includes deleted_from (arrayref) and warnings (arrayref, if any component deletion failed).

verify_user

my $result = $concierge->verify_user($user_id);

Checks whether $user_id exists in both Auth and Users components. Returns verified => 1 only if present in both. Includes exists_in_auth and exists_in_users flags, and a warning if the user exists in one component but not the other.

list_users

# IDs only
my $result = $concierge->list_users($filter, \%options);
my @ids = @{ $result->{user_ids} };

# With full data
my $result = $concierge->list_users('', { include_data => 1 });
my %users = %{ $result->{users} };

Returns user IDs from the Users component. $filter is a string passed through to Concierge::Users. With include_data => 1, fetches each user's full record into a users hash keyed by user_id. With fields => [...], returns only the specified fields per user.

get_user_data

my $result = $concierge->get_user_data($user_id, @fields);
my $data = $result->{user};

Retrieves user data from the Users component. If @fields is provided, returns only those fields; otherwise returns all fields.

update_user_data

my $result = $concierge->update_user_data($user_id, \%updates);

Updates the user's record in the Users component. The user_id and password fields are filtered out and cannot be changed through this method.

Password Operations

Initial password registration is handled by add_user(), which sets the password atomically with user creation. The methods here operate on passwords for existing users.

verify_password

my $result = $concierge->verify_password($user_id, $password);

Checks whether $password is correct for $user_id. Returns success => 1 if the password matches.

reset_password

my $result = $concierge->reset_password($user_id, $new_password);

Sets a new password for an existing user. The application is responsible for verifying the user's identity before calling this method.

PARAMETER FILTERS

Concierge uses Params::Filter to enforce data segregation at method boundaries:

$auth_data_filter -- extracts only user_id and password
$user_data_filter -- extracts everything except password
$session_data_filter -- extracts user_id plus non-credential fields
$user_update_filter -- excludes user_id and password from updates

These ensure that credentials never leak into user data stores and that identity fields cannot be changed via update operations.

EXTENSIBILITY

Component Substitution

Each identity core component can be replaced with a drop-in alternative as long as the replacement implements the methods Concierge calls on it.

Auth -- Concierge calls:

$auth->authenticate($user_id, $credential)
$auth->is_id_known($user_id)
$auth->enroll($user_id, $credential, \%opts?)
$auth->change_credentials($user_id, $new_credential)
$auth->revoke($user_id)

A substitute backend must implement this contract -- see Concierge::Auth::Base -- and must also provide the Concierge::Auth::Generators methods (used for visitor/guest identifiers, independent of authentication). Register it in Concierge::Desk::Setup's backend catalog so auth => { backend => 'mybackend', ... } resolves to it at desk-build time, or bypass config entirely by constructing Concierge::Auth->new(backend => 'My::Fully::Qualified::Class', ...) directly and assigning it to $concierge->{auth} after open_desk.

Sessions -- Concierge calls:

Concierge::Sessions->new(%args) -- constructor; accepts storage_dir and backend
$sessions->new_session(%args) -- returns { success => 1, session => $obj }
$sessions->get_session($session_id) -- returns { success => 1, session => $obj }
$sessions->delete_session($session_id) -- returns { success => 1|0, ... }
$sessions->cleanup_sessions() -- returns { success => 1, deleted_count => N, active => [...] }

Users -- Concierge calls:

Concierge::Users->new($config_file) -- constructor
$users->register_user(\%data) -- returns { success => 1|0, message => '...' }
$users->get_user($user_id) -- returns { success => 1, user => \%data }
$users->update_user($user_id, \%updates) -- returns { success => 1|0, ... }
$users->delete_user($user_id) -- returns { success => 1|0, ... }
$users->list_users($filter) -- returns { success => 1, user_ids => [...] }

To substitute a component, supply an object that responds to these methods and assign it to the corresponding slot on the concierge object after open_desk():

my $result = Concierge->open_desk($desk_dir);
my $c = $result->{concierge};
$c->{auth} = My::LDAPAuth->new(...);   # drop-in replacement

Additional Components

Beyond the identity core, a desk can declare additional components -- Organizations, Assets, Catalog, etc. -- via a components block passed to Concierge::Desk::Setup::build_desk():

my $result = Concierge::Desk::Setup::build_desk({
    base_dir => './desk',
    auth     => { backend => 'pwd' },
    sessions => { backend => 'database' },
    users    => { backend => 'database' },
    components => {
        organizations => {
            class    => 'Concierge::Organizations',
            optional => 0,
            dir      => 'organizations',   # optional; resolved like
                                            # other components' dirs
            # ...any other setup() args for this component...
        },
    },
});

Each entry is resolved once, at build time: build_desk() creates the component's storage directory, requires class, calls $class->new(), then calls $comp->setup(\%entry_config). The hashref setup() returns is persisted verbatim into concierge.conf as that component's payload -- this is never recomputed at open_desk()/runtime. A setup() failure always fails the entire build, regardless of the optional flag.

At open_desk() time, Concierge reads each component's persisted payload back out of concierge.conf, requires its class, and calls $class->new($payload):

  • If new() succeeds, the component is attached at $concierge->{$name}, and a same-named accessor is installed automatically (e.g., $concierge->organizations) if one doesn't already exist.

  • If new() dies and the component is not optional, the exception propagates uncaught out of open_desk() -- a desk must never open half-instantiated.

  • If new() dies and the component is optional, a Concierge::Desk::UnavailableComponent stand-in is attached instead. Any method called on it returns { success => 0, message => "Component '$name' unavailable: ..." }.

Access the component through the concierge object once the desk is open:

my $c = Concierge->open_desk($desk_dir)->{concierge};
my $r = $c->organizations->add_record('acme', \%data);
unless ($r->{success}) {
    # handles both a genuine failure and an UnavailableComponent
    # substitution identically
}

A component only needs to satisfy the duck-typed contract documented in Concierge::Desk::Component -- there is no isa check anywhere in this mechanism. A component whose job is per-record CRUD is responsible for its own CRUD methods; Concierge does not standardize or scaffold them.

Sessions, Auth, and Users are not wired through this generic mechanism -- they remain hardcoded core affordances, loaded unconditionally by open_desk() as described above. The components block is only for components genuinely additional to that identity core.

Component Method Promotion

A component's own bare accessor ($concierge->{name}->method(...) or $concierge->name->method(...)) always exposes its complete API -- this is the permanent escape hatch and needs no configuration. promote is an optional, purely cosmetic convenience on top of it: a curated allowlist of a component's methods forwarded directly onto $concierge, declared in the same components config block:

components => {
    reports => {
        class   => 'Concierge::Reports',
        promote => ['get_signal_report'],                   # same-name
        # or:
        promote => { get_signal_report => 'fetch_signal_report' },  # aliased
    },
},

my $c = Concierge->open_desk($desk_dir)->{concierge};
$c->get_signal_report(...);                # promoted sugar
$c->{reports}->get_signal_report(...);      # escape hatch -- always works,
                                             # promoted or not

promote is not access control -- it never restricts what's reachable through the escape hatch. All validation (shape, method-existence, and name-collision checks against core methods and other components) happens once, at build_desk() time; open_desk() trusts the persisted result completely. See "PROMOTION: EXPOSING COMPONENT METHODS ON $concierge" in Concierge::Desk::Component for the full contract.

Future Components

Concierge::Organizations is the first component built on this mechanism -- a simple JSON-file-backed records store for multi-tenancy (users belonging to organizations), installed and namespaced independently of this distribution, exactly like Auth/Sessions/Users. The following illustrate other kinds of components the Concierge:: namespace is suited for. These are not roadmap commitments -- they are examples of what the component pattern enables:

  • Concierge::Assets -- files, images, or other owned resources

  • Concierge::Guides -- role and permission records

  • Concierge::Catalog -- product or content records

  • Concierge::Calendar -- event and booking records

Contributing

If you build a component that might be useful to others, contributions to the Concierge:: namespace on CPAN are welcome. The conventions to follow are: satisfy the duck-typed contract in Concierge::Desk::Component, use the { success = 1|0, message => '...' }> return convention, accept a desk config block via setup(), and include comprehensive tests and POD. Open an issue or pull request at https://github.com/bwva/Concierge to discuss before publishing.

SEE ALSO

Concierge::Desk::Setup -- desk creation and configuration

Concierge::Desk::User -- user objects returned by lifecycle methods

Concierge::Desk::Component -- contract documentation for additional components

Concierge::Desk::UnavailableComponent -- stand-in for a failed optional component

Concierge::Auth, Concierge::Sessions, Concierge::Users -- identity core component modules

Concierge::Organizations -- example additional component (multi-tenancy records store)

AUTHOR

Bruce Van Allen <bva@cruzio.com>

LICENSE

This module is free software; you can redistribute it and/or modify it under the terms of the Artistic License 2.0.