@zdevzone/keyforge-client 0.0.0-stage → 0.1.1-beta.39008329343.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abraham Bekele
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,134 @@
1
- # Temporary Holding Version
1
+ # KeyForge TypeScript client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@zdevzone/keyforge-client` provides online onboarding, registration/activation, signed offline authorization, trial accounting, public support threads and in-app notifications. It supports the existing product-scoped server API, license v4 and trial v2. It uses Fetch, Web Crypto Ed25519, and Zod; it has no server, database, UI or desktop-framework dependency. Node 22.18+ is the reference runtime. Other desktop runtimes must support those Web APIs or run the client in their trusted Node/main process.
4
+
5
+ Install in an existing application:
6
+
7
+ ```sh
8
+ vp add @zdevzone/keyforge-client
9
+ ```
10
+
11
+ Or create a separate Node.js integration starter:
12
+
13
+ ```sh
14
+ vp create keyforge-client
15
+ ```
16
+
17
+ The [starter](../create-keyforge-client/README.md) includes explicit onboarding commands, public-key pinning and a secure-storage adapter boundary. Its memory demo supports local status only; implement secure atomic storage before real activation or offline restart testing.
18
+
19
+ For workspace development, use `"@zdevzone/keyforge-client": "workspace:*"`. To install a reviewed local build in another repository, run `vp -C packages/keyforge-client pm pack --pack-destination ../../.artifacts`, copy the archive into that app's `vendor/` folder, and run `vp add ./vendor/zdevzone-keyforge-client-0.1.0.tgz`. The prepack hook builds and verifies JavaScript/type exports automatically; do not disable lifecycle scripts. Commit the archive, dependency and lockfile so installs remain reproducible. Zod is the only runtime dependency.
20
+
21
+ The old name `@keyforge/client` belongs to another project. Maintainers should follow the [package release guide](../../docs/publishing.md).
22
+
23
+ ## Onboarding and authorization
24
+
25
+ ```ts
26
+ import { KeyForgeClient, type SecureStorage } from "@zdevzone/keyforge-client";
27
+
28
+ // Implement this using your framework's credential vault and an atomic state record.
29
+ declare const secureStorage: SecureStorage;
30
+ declare const pinnedPublicKeys: Record<string, string>;
31
+
32
+ const client = new KeyForgeClient({
33
+ serverUrl: "https://licenses.example.com",
34
+ productId: "your-product",
35
+ publicKeys: pinnedPublicKeys, // { "trusted-kid": "-----BEGIN PUBLIC KEY-----\n..." }
36
+ storage: secureStorage,
37
+ registration: { appVersion: "1.0.0", platform: "windows" },
38
+ });
39
+
40
+ // Choose one from customer onboarding; these require internet.
41
+ // await client.activate(keyEnteredByCustomer);
42
+ // await client.startTrial();
43
+
44
+ // Skipping onboarding leaves the app unlicensed. This call needs no network.
45
+ const authorization = await client.getAuthorization();
46
+ if (authorization.state === "licensed" && authorization.entitlements.export === true) {
47
+ // Permit this specific export operation.
48
+ }
49
+ ```
50
+
51
+ Customers enter their **product/license key**. The SDK generates a different, random **installation secret** behind the scenes. It authenticates subsequent device requests and must survive app restarts. There is no customer-facing credential entry field. Trial onboarding is offered only when provider policy permits it; `startTrial` obtains/evaluates the server's grant and cannot manufacture permission.
52
+
53
+ `register()` registers online without selecting a trial. `validate(key)` is online inspection only and consumes no seat; only activation returns a paid offline grant. `activate(key)` registers if necessary, binds a seat online, verifies and saves the grant, and returns current authorization. Product keys are never persisted by the SDK. `refresh()` contacts KeyForge for the current bound license; schedule it while online ahead of `deadline`, also after network recovery. There is no hidden background worker. Use a single refresh loop (for example on startup, connectivity recovery, then every few hours with jitter); surface failures and back off instead of polling in a tight loop.
54
+
55
+ `deactivate()` releases the seat online and clears local paid authorization after success. It does not silently enable a previously selected trial. `collectLicense(licenseId)` retrieves an installation-targeted key; call `activate(result.key)` separately. Collection never grants features by itself.
56
+
57
+ | State | App behavior |
58
+ | --- | --- |
59
+ | `unlicensed` | Offer key entry, allowed trial onboarding, or explicitly free features. |
60
+ | `trial` | Enforce provider/app trial features and the returned `remaining` allowance. |
61
+ | `licensed` | Enforce typed signed `entitlements` at every protected operation. |
62
+ | `reconnect_required` | Offer reconnection/activation recovery; do not enable protected features. Inspect `reason`. |
63
+ | `expired` | Offer renewal/key entry; no paid/trial authorization. |
64
+ | `revoked` | Stop protected use; contact the provider. |
65
+
66
+ New activation, validation, trial onboarding and refresh require internet. Offline usage checks the authenticity and remaining lifetime of a previously saved grant; it cannot validate a new product key. Paid authorization ends at the earlier of signed expiry and `offlineGraceUntil`. Expiry deadlines have no grace extension for clock tolerance. A signed revoked/expired refresh can arrive with HTTP 200 and is persisted before returning. Refresh failures do not erase a still-usable signed grant for transient network outages; explicit binding/identity denials prevent fallback to an older active grant.
67
+
68
+ Trials retain their distinct existing contract: `fresh`/`spent`, nullable expiry and nullable allowance. An allowance-only trial may have no time deadline. Call `consumeTrial(units)` at the app's billable trial operation; it serializes and saves cumulative usage separately from grants. Activation, refresh, registration replay and app restarts never reset this counter. Its return state describes authorization **after** consumption, so inspect the pre-operation allowance before starting work and retain the consumed result for the next operation. App-local accounting is mutable on a compromised device; authoritative usage belongs on a trusted backend. A trial grant does not contain paid plan entitlements.
69
+
70
+ ## Storage and trust boundaries
71
+
72
+ `SecureStorage` has `load(): Promise<ClientState | null>` and `save(state): Promise<void>`. Supply it explicitly. `load` returns an isolated snapshot; `save` must atomically replace a coherent record including identity, pending registration, token(s), counters, denial and clock checkpoint. Protect this record with the operating system's credential storage/encryption, restrict file permissions, and handle failures visibly. For a vault-plus-file design, encrypt state with a vault-held key and atomically replace the ciphertext; never split identity/token/counter updates across non-atomic writes.
73
+
74
+ Use one SDK owner per installation (typically the desktop main process) and a narrow IPC interface for UI requests. The SDK serializes operations within that instance; cross-process ownership/locking belongs to the host app. Scope storage to deployment URL, product and subject kind. Mismatches or malformed state fail closed rather than creating a new identity. Lost credentials need a new installation and operator-assisted seat recovery; do not silently erase state to work around an authorization failure. `MemoryStorage` is an explicitly ephemeral test/reference adapter, not production storage.
75
+
76
+ Ship trusted Ed25519 SPKI public PEM keys indexed by `kid` through your signed app/update distribution. The SDK never adds trust by fetching `/keys`. It verifies signatures over the decoded JSON bytes (not the encoded segment or reserialized JSON), strict payload shape, versions, issuer `default`, product, installation and subject, and plausible issuance. `verifySignedToken` authenticates even negative or expired tokens; its return value alone is **not permission**. Use `getAuthorization` for current access. Unknown keys/versions fail closed until an approved app/trust-store update.
77
+
78
+ Clock checkpoints persist observed local time and authenticated grant issuance times. A rollback greater than five minutes returns `clock_rollback`; correct the system clock and reconnect. Clock tolerance is configurable via `clockToleranceMs` and never extends expiry. This is basic rollback detection, not resistance to a modified binary, restored disk snapshot or compromised device. The SDK does not rely on unsigned `serverTime` to extend access. Offline revocation remains delayed until the signed deadline or a successful refresh.
79
+
80
+ HTTPS is required except loopback HTTP for local development. Redirects are rejected so credentials do not follow another server. Do not expose SDK state, product keys, tokens or installation secrets in renderer globals, analytics, screenshots, logs or error reports. Signing keys, administrator credentials and metering credentials never belong in the dependent app. Sales/payments and license issuance remain server/operator workflows.
81
+
82
+ ## Retries and communication
83
+
84
+ Registration identity, UUID and exact body are persisted **before** the first request and reused after interruption/restart. Changes to registration metadata take effect for a new identity, not by mutating a pending request. Activation repeats safely against the same binding; the SDK never creates sales or issues replacement licenses. Default transport uses three attempts, a ten-second per-attempt timeout and bounded exponential backoff with jitter for network failures and HTTP 429/502/503/504. `Retry-After` is honored; waits over 30 seconds return control to the caller instead of retrying early. `KeyForgeError` exposes safe `code`, `status`, `retryable`; `TokenError` identifies verification failures without including secrets.
85
+
86
+ Optional methods:
87
+
88
+ - `getNotifications()`, `setNotificationPreferences({messages, expirations})`, `markNotificationRead(eventId)`.
89
+ - `listSupportThreads()`, `getSupportThread(id)`.
90
+ - `createSupportThread({requestId, licenseId, category, urgency, subject, message, createdAt, reporter?, app?})`.
91
+ - `replyToSupportThread(id, {requestId, revision, message})`.
92
+
93
+ Register before invoking optional methods (getting notifications also registers automatically). Poll while online with a reasonable interval (for example 60 seconds while the inbox is visible), pause when offline/backgrounded, and back off after errors. Collections currently return all installation-visible records; paginate in your app. Private operator notes and assignments are excluded by the server. There is no email, attachment or realtime transport. Technical/feature/other cases need an active paid binding; licensing/billing can use an existing expired or released binding. Trials cannot create cases.
94
+
95
+ For support creation/replies, persist a durable outbox with the UUID **and exact body** before calling; SDK transport retries reuse these values, but it does not create an app-specific outbox. Case creation sets `Idempotency-Key` to `requestId`. Replies need the current thread revision; a conflict is surfaced for reconciliation rather than silently overwriting newer content. Do not generate a new UUID after an ambiguous timeout. A closed case needs a new case; resolved/waiting cases reopen on a customer reply.
96
+
97
+ The preinstallation public form/private status link continues to work independently. Request secrets and installation secrets are distinct. Use the issued key in desktop onboarding later. Trusted-backend metering uses separate service credentials and is documented in OpenAPI; it is intentionally absent from the desktop client API.
98
+
99
+ ## Example, skill and validation
100
+
101
+ The [Node reference](../../examples/keyforge-node.ts) runs with `vp exec tsx examples/keyforge-node.ts` from the repository root. Set `KEYFORGE_SERVER_URL`, `KEYFORGE_PRODUCT_ID`, and `KEYFORGE_PUBLIC_KEYS_FILE` (JSON object mapping trusted kids to public PEMs). Optionally set `KEYFORGE_LICENSE_KEY` or `KEYFORGE_TRIAL=1`; do not put a key in command-line arguments. This executable uses ephemeral storage, creates a new installation each run, and prints only the resulting state. Use a test product: repeated activation can exhaust seats. Its reusable `runReference` function accepts a real storage adapter.
102
+
103
+ The [AI integration skill](skill/keyforge-integrate/SKILL.md) can be copied with its entire folder into the dependent repository's `.agents/skills/keyforge-integrate/`. It is bundled in the package archive; no global agent configuration is changed by installing the SDK.
104
+
105
+ The [OpenAPI document](../../docs/client-openapi.json) covers public routes, including separately authenticated preinstallation requests and trusted-backend usage. Regenerate it with `vp run docs:client-api`; `vp run docs:client-api --check` detects drift. SDK tests send real HTTP requests to a temporary KeyForge server and validate requests/responses against this contract. Normal `vp run typecheck`, `vp run test`, `vp run check`, and `vp run build:all` include the package. The SDK does not change server database schemas, token formats, users, license issuance, financial records or signing behavior.
106
+
107
+ ## Optional desktop recognition
108
+
109
+ Use `createDeviceSignalsProvider()` from `@zdevzone/keyforge-client/device-signals` to select **Windows, Linux or macOS** automatically in a trusted Node process. Explicit `/windows`, `/linux` and `/macos` entry points are also exported. The core import stays framework-independent. This SDK remains JavaScript/TypeScript; other languages can use the HTTP API/OpenAPI contract directly and implement the documented signing, secure storage and retry rules. A generated HTTP client alone does not provide offline authorization.
110
+
111
+ Linux reports hashed `linux-machine-id`, accessible `smbios-uuid` and `root-volume-uuid`; macOS reports hashed `macos-platform-uuid` and `root-volume-uuid`. Linux reads fixed machine-id/DMI files and queries findmnt; macOS uses fixed ioreg/diskutil queries. No elevation is requested. Permission/tool/sandbox failures yield safe partial reports; browsers/mobile platforms retain installation-based operation. Reads and individual identifiers are bounded to 4 KiB, Apple tool output to 64 KiB, and queries to eight seconds. Available means the OS's complete set (two Mac signals, three Windows/Linux), not an attested physical device. See the repository's `docs/device-identity.md` for sources, normalization, cross-language hash vectors and recovery guidance. `KEYFORGE_DEVICE_SIGNALS=1` enables auto selection in the Node example.
112
+
113
+ ### Existing Windows setup
114
+
115
+ The core entry point remains framework-independent. In a trusted Node process, opt in with:
116
+
117
+ ```ts
118
+ import { createWindowsDeviceSignalsProvider } from "@zdevzone/keyforge-client/windows";
119
+ // Add to the existing KeyForgeClient options (keep the same secure storage):
120
+ const deviceSignalsProvider = createWindowsDeviceSignalsProvider();
121
+ const client = new KeyForgeClient({ serverUrl, productId, publicKeys, storage, deviceSignalsProvider });
122
+ await client.register(); // Online registration alone does not grant paid access.
123
+ const diagnostics = await client.getDeviceSignalsDiagnostics();
124
+ // Existing installations: recollect metadata without registering again.
125
+ await client.updateDeviceSignals();
126
+ ```
127
+
128
+ Fixed read-only, hidden PowerShell subprocesses collect Windows MachineGuid, SMBIOS UUID and system-volume serial, with no elevation, 8-second execution and 4 KiB output limits per query. CPU IDs, MAC/IP addresses and raw identifiers are never transmitted. Unsupported platforms, missing values, placeholders, timeout and permissions failures return safe per-kind diagnostics, permitting ordinary installation licensing. Diagnostics contain only kind/status codes. Treat hashes as pseudonymous data and explain optional collection to users. A volume serial identifies a filesystem volume, not a unique physical disk.
129
+
130
+ UUIDs normalize to trimmed lowercase unbraced hyphenated hex. Volume serials normalize to eight lowercase hex digits (optional middle hyphen removed). Zero, repeated and known OEM placeholders are rejected. Scheme v1 hashes each signal separately with SHA-256 over UTF-8 compact JSON: `["keyforge-device-signals",1,canonicalServerUrl,productId,kind,normalizedValue]`. Canonical URL is `new URL(serverUrl).toString()` with a final slash removed; credentials/query/fragment are prohibited. Test vector for `https://licenses.example.test`, `studio`, `machine-guid`, `a1234567-b123-c123-d123-e123456789ab`: `aa7f9a3b0eeee456daced7ae390c08795b46934418950b09e5a7aeb7a0d8127f`.
131
+
132
+ Initial signals and diagnostics are saved atomically with the registration UUID/body before networking. The saved request is replayed unchanged after interruption/restart; hardware changes never rewrite it. Manual `registration.machineSignals` still take precedence for initial registration. `updateDeviceSignals()` requires the provider and uses the installation-authenticated PUT; no trial consumption, seat, meter, grant or financial state changes. Empty reports clear current availability but retain historical group evidence. Retry the update after network recovery.
133
+
134
+ Legacy hashes and namespaced hashes do not automatically recognize one another. Authenticated backfill anchors new evidence to the existing installation's group without automatic group merges. A server URL/product change changes the namespace and storage context; coordinate a credential-preserving migration/backfill rather than deleting state. Lost credentials need operator-assisted recovery. Signals can be changed, cloned or forged; they are recognition hints, not physical-device proof. Activation limits still count installations, and matching hardware never automatically transfers/releases seats. Keep production credentials in your application-supplied secure storage adapter; never silently fall back to plaintext files.
@@ -0,0 +1,105 @@
1
+ import type { Authorization, ClientState, CustomerNotification, CustomerThread, DeviceSignalsProvider, DeviceSignalsReport, NotificationPreferences, RegistrationMetadata, SecureStorage, SupportRequest, ValidationResult } from "./types.js";
2
+ export declare class KeyForgeError extends Error {
3
+ readonly code: string;
4
+ readonly status: number;
5
+ readonly retryable: boolean;
6
+ constructor(code: string, status?: number, retryable?: boolean);
7
+ }
8
+ /** Ephemeral test/example storage. Not a production persistence implementation. */
9
+ export declare class MemoryStorage implements SecureStorage {
10
+ private state;
11
+ load(): Promise<ClientState | null>;
12
+ save(state: ClientState): Promise<void>;
13
+ }
14
+ export interface ClientOptions {
15
+ serverUrl: string;
16
+ productId: string;
17
+ publicKeys: Readonly<Record<string, string>>;
18
+ storage: SecureStorage;
19
+ subjectKind?: "installation" | "service";
20
+ registration?: RegistrationMetadata;
21
+ /** Opt-in collection. Raw identifiers must never leave the provider. */
22
+ deviceSignalsProvider?: DeviceSignalsProvider;
23
+ fetch?: typeof globalThis.fetch;
24
+ now?: () => number;
25
+ clockToleranceMs?: number;
26
+ maxAttempts?: number;
27
+ timeoutMs?: number;
28
+ /** Injectable to make retry tests deterministic. */
29
+ sleep?: (ms: number) => Promise<void>;
30
+ }
31
+ export declare class KeyForgeClient {
32
+ private readonly options;
33
+ private readonly url;
34
+ private queue;
35
+ constructor(options: ClientOptions);
36
+ private now;
37
+ private serial;
38
+ private load;
39
+ private collectDeviceSignals;
40
+ /** Recollect recognition hints for this authenticated installation; never re-registers it. */
41
+ updateDeviceSignals(): Promise<DeviceSignalsReport>;
42
+ getDeviceSignalsDiagnostics(): Promise<DeviceSignalsReport["diagnostics"]>;
43
+ private context;
44
+ private productPath;
45
+ private installationPath;
46
+ private request;
47
+ private ensureRegistered;
48
+ private observe;
49
+ private evaluate;
50
+ /** Registers online but does not select a trial or unlock features. */
51
+ register(): Promise<{
52
+ installationId: string;
53
+ trialAvailable: boolean;
54
+ }>;
55
+ startTrial(): Promise<Authorization>;
56
+ getAuthorization(): Promise<Authorization>;
57
+ /** Saves usage independently of grants. Refresh/activation never resets this counter. */
58
+ consumeTrial(units?: number): Promise<Authorization>;
59
+ validate(key: string): Promise<ValidationResult>;
60
+ activate(key: string): Promise<Authorization>;
61
+ private saveLicense;
62
+ refresh(): Promise<Authorization>;
63
+ deactivate(): Promise<void>;
64
+ getNotifications(): Promise<{
65
+ items: CustomerNotification[];
66
+ preferences: NotificationPreferences;
67
+ }>;
68
+ setNotificationPreferences(preferences: NotificationPreferences): Promise<{
69
+ ok: true;
70
+ }>;
71
+ markNotificationRead(eventId: string): Promise<{
72
+ ok: true;
73
+ }>;
74
+ collectLicense(licenseId: string): Promise<{
75
+ key: string;
76
+ license: {
77
+ id: string;
78
+ plan: string;
79
+ issuedTo: string;
80
+ expiresAt: string | null;
81
+ };
82
+ }>;
83
+ listSupportThreads(): Promise<{
84
+ requests: CustomerThread[];
85
+ }>;
86
+ getSupportThread(id: string): Promise<{
87
+ request: CustomerThread;
88
+ }>;
89
+ createSupportThread(input: SupportRequest): Promise<{
90
+ request: {
91
+ id: string;
92
+ clientRequestId: string;
93
+ status: CustomerThread["status"];
94
+ receivedAt: string;
95
+ };
96
+ serverTime: string;
97
+ }>;
98
+ replyToSupportThread(id: string, input: {
99
+ requestId: string;
100
+ revision: number;
101
+ message: string;
102
+ }): Promise<{
103
+ request: CustomerThread;
104
+ }>;
105
+ }