Docker SDK AI Prompt

Copy/paste this prompt into your AI assistant (e.g., GitHub Copilot Chat, Cursor, ChatGPT) to integrate the ExisOne Client SDK into a multi-user app running inside a Docker container.

Use this when: You ship a Dockerized app with multiple users (web app, internal tool, SaaS), you have a single Corporate (multi-seat) license, and you want each user inside the container to consume one seat. Requires SDK version .NET 0.9.0+, Python 0.8.0+, or Node.js 0.8.0+ (the versions that introduced the hardware ID override).

Why this is different from the standard SDK prompt

Inside a Docker container, the standard GenerateHardwareId() result is unreliable: WMI is unavailable on Linux, /etc/machine-id is identical for every user in the container, and the value can change across container restarts. The Hardware ID Override option (added to the SDK) lets your application supply a stable, per-user identifier instead — typically a GUID stored on the user record. The server treats this string opaquely, so the activation, validation, and seat-tracking flows all work unchanged.

Prompt

You're assisting me in integrating the ExisOne Client SDK into a Dockerized
multi-user application that uses a Corporate (multi-seat) license. Each
user inside the container should consume one seat on the same license key.

GOAL
- On container startup, ping the server to start/check a TRIAL bound to the
  "admin" identity. The trial gates the entire container: when it expires,
  no users may use the app.
- For each new user that signs into the app, generate a stable per-user
  pseudo hardware ID (UUID), persist it on the user row, and ACTIVATE the
  corporate license against that ID. This consumes one seat.
- On every user request (or periodic re-check), VALIDATE the per-user
  pseudo hardware ID. If the admin trial check has failed, block ALL users.
- When a user is removed (or admin frees a seat), DEACTIVATE that user's
  pseudo hardware ID to release the seat.
- Cleanly handle the corporate seat-cap: when the license is full, surface
  a clear error to whoever is provisioning the new user.

WHY THE OVERRIDE
- The SDK's default GenerateHardwareId() reads CPU/MAC/machine-id from the
  host. Inside Docker that value is either identical for every user
  (machine-id is shared) or unstable across container restarts. Neither
  works for corporate seat tracking.
- The new hardwareIdOverride option (.NET 0.9.0 / Python 0.8.0 / Node 0.8.0)
  makes GenerateHardwareId() return whatever string you supply, and the
  server treats hardware IDs as opaque. So a GUID per user is perfect.

CONTEXT
- SDK packages (pick one):
  * .NET:    ExisOne.Client >= 0.9.0   (TFM net8.0 or net9.0)
  * Python:  exisone-client >= 0.8.0   (Python 3.8+)
  * Node.js: exisone-client >= 0.8.0   (Node 18+)
- I have:
  * An ExisOne API base URL (https only)
  * An access token (format: exo_at_<public>_<secret>) with the
    activate / verify scopes
  * A Corporate license activation key with N seats configured
  * A Product configured in the ExisOne dashboard
- The Docker app has:
  * A "default admin" identity that owns the trial (one fixed UUID)
  * A user table where I can store one extra column:
    license_hardware_id (string, ~36 chars, unique per user)

REQUIREMENTS

1) ADD THE PACKAGE
   .NET:    dotnet add package ExisOne.Client --version 0.9.0
   Python:  pip install exisone-client>=0.8.0
   Node.js: npm install exisone-client@^0.8.0

2) DATABASE / PERSISTENCE
   - Add a stable per-user hardware ID column to the user table.
     Generate it ONCE when the user is created and never change it.
     (Regenerating it on every request will burn through seats.)
   - Store one fixed admin pseudo-hardware ID (e.g. in app config or a
     dedicated row) for the trial check. Never change it across container
     restarts — back it with a Docker volume or persistent config.
   - Optionally cache the last successful admin trial check result with a
     short TTL (e.g. 5 minutes) so you don't hit the server on every
     request. The trial expires when the cached result says it's expired
     OR when a fresh check fails.

3) CONTAINER STARTUP — ADMIN TRIAL CHECK
   Create one client instance with the admin pseudo-hardware ID and the
   corporate activation key omitted (or null/empty). Calling validate
   without an activation key triggers the trial flow on the server, which
   tracks per-hardware-id trial state.

   .NET example:
       var adminClient = new ExisOneClient(new ExisOneClientOptions {
           BaseUrl = "https://your-api-host",
           AccessToken = "exo_at_PUBLIC_SECRET",
           HardwareIdOverride = adminPseudoHwid, // fixed, persistent
       });
       var hwid = adminClient.GenerateHardwareId(); // returns the override
       var (isValid, status, exp, _, _, _) =
           await adminClient.ValidateAsync(hwid, "MyProduct", activationKey: null);
       // status will be "trial", "trial_unavailable", or "expired"
       if (status == "expired") {
           // Block the entire app from serving requests.
       }

   Python example:
       admin_client = ExisOneClient(ExisOneClientOptions(
           base_url="https://your-api-host",
           access_token="exo_at_PUBLIC_SECRET",
           hardware_id_override=admin_pseudo_hwid,
       ))
       hwid = admin_client.generate_hardware_id()
       result = admin_client.validate(
           activation_key="",   # empty = trial check
           hardware_id=hwid,
           product_name="MyProduct",
       )
       if result.status == "expired":
           # Block the entire app from serving requests.

   Node.js example:
       const adminClient = new ExisOneClient({
           baseUrl: 'https://your-api-host',
           accessToken: 'exo_at_PUBLIC_SECRET',
           hardwareIdOverride: adminPseudoHwid,
       });
       const hwid = adminClient.generateHardwareId();
       const result = await adminClient.validate({
           activationKey: '',         // empty = trial check
           hardwareId: hwid,
           productName: 'MyProduct',
       });
       if (result.status === 'expired') {
           // Block the entire app from serving requests.
       }

4) PER-USER ACTIVATION (NEW USER SIGNUP)
   When a new user is created in your app:
   a) Generate a UUID and store it on the user row as license_hardware_id.
   b) Construct a SECOND client instance with hardwareIdOverride set to
      that UUID, and call activate() with the corporate license key.
   c) On success, the user has consumed one seat on the corporate license.
   d) If the server returns a "no seats available" error, SURFACE THAT
      ERROR to whoever provisioned the user. Do not silently swallow it.

   .NET:
       var userClient = new ExisOneClient(new ExisOneClientOptions {
           BaseUrl = "...", AccessToken = "...",
           HardwareIdOverride = newUser.LicenseHardwareId,
       });
       var result = await userClient.ActivateAsync(
           corporateKey, newUser.Email,
           userClient.GenerateHardwareId(),
           "MyProduct");
       if (!result.Success) throw new SeatAllocationException(result.ErrorMessage);

5) PER-USER VALIDATION (ON LOGIN OR PER-REQUEST)
   On each login (or periodically while the user is active), validate the
   user's seat AND check the admin trial state.

   if (!AdminTrialCacheStillValid()) RecheckAdminTrial();
   if (AdminTrialExpired()) DenyAccess();

   var userClient = new ExisOneClient(new ExisOneClientOptions {
       BaseUrl = "...", AccessToken = "...",
       HardwareIdOverride = currentUser.LicenseHardwareId,
   });
   var hwid = userClient.GenerateHardwareId();
   var (isValid, status, _, _, _, _) =
       await userClient.ValidateAsync(hwid, "MyProduct", corporateKey);
   if (!isValid) DenyAccess();

6) PER-USER DEACTIVATION (USER REMOVED)
   When a user is deleted or an admin frees a seat:

   await userClient.DeactivateAsync(corporateKey, hwid, "MyProduct");

   The seat is released and can be consumed by the next new user.

7) GATE THE APP ON THE ADMIN TRIAL
   The trial is bound to the admin pseudo-hardware ID, NOT to individual
   users. Once it expires, the gate must affect EVERY user request:
   - Wrap your auth/middleware so every request checks the cached admin
     trial state and denies access (or shows an "upgrade required" page)
     when expired.
   - When the customer purchases the corporate license and you switch
     from trial mode to licensed mode, the per-user activate() calls in
     step 4 take over and the admin trial check becomes a no-op.

8) OPERATIONAL CONCERNS
   - Persist the admin pseudo-hardware ID and all user license_hardware_id
     values in a Docker VOLUME or external database, not the container's
     ephemeral filesystem. Otherwise restarting the container will mint
     fresh IDs and burn seats.
   - Generate the per-user UUID exactly ONCE per user. Never regenerate.
   - Cache validation calls (e.g. 5-minute TTL per user) to avoid hammering
     the server on every HTTP request. Re-check on login and periodically.
   - Surface clear error messages to admins when the seat cap is reached.
   - Periodically prune deactivated users so old DeviceLicenseRecord rows
     don't accumulate server-side.

9) SECURITY
   - Treat the access token like a database password. Pass it via env var
     or secret store, never bake it into the image.
   - Use https only for the BaseUrl. The SDK enforces this.
   - The corporate activation key is also a secret — same rules.

DELIVERABLES
- Configuration / DI registration code for the SDK client(s)
- A startup hook that performs the admin trial check
- A user-creation flow that generates+persists the per-user pseudo HWID
  and activates the corporate license
- An auth middleware (or request interceptor) that gates the entire
  application on (a) the admin trial state and (b) per-user validation
- A user-deletion flow that calls deactivate() to release the seat
- Clear error surfaces for "seat cap reached" and "trial expired"
- A Dockerfile snippet showing where the admin pseudo-hardware ID and
  user database are persisted (volume mount)

Please produce idiomatic code for my stack (specify .NET / Python / Node)
with concise methods and no extraneous commentary.

Notes