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
- The Hardware ID Override option was introduced in .NET 0.9.0, Python 0.8.0, and Node.js 0.8.0. Older SDK versions cannot use this prompt — upgrade first.
- The server treats the hardware ID as an opaque string of up to 128 characters. A standard 36-char UUID fits comfortably and is the recommended choice.
- Trial state is bound per-hardware-id. Use a single fixed "admin" pseudo-hardware ID for the container-wide trial gate; do not let each user start their own trial.
- The corporate license must have MaxSeats configured in the ExisOne dashboard. Each user activation consumes one seat; deactivation releases it.
- For SDK API details see the .NET, Python, and Node.js SDK pages.
- For the standard (single-machine) integration prompt, see the per-language pages: .NET · Python · Node.js.