Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/license-payload-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@milaboratories/pl-client": minor
---

Add `LicensePayload` type and `decodeLicenseToken` helper, describing the decoded body of a Platforma license token next to the Maintenance API `license()` call that returns it. Includes a test that fetches the license from a live backend and asserts the required payload fields are always present.
43 changes: 43 additions & 0 deletions lib/node/pl-client/src/core/license.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { getTestClient } from "../test/test_config";
import { decodeLicenseToken, type LicensePayload } from "./license";
import { test, expect } from "vitest";

/**
* Fetches the license token from a live backend, decodes it, and checks that the
* required {@link LicensePayload} fields are always populated.
*
* This doubles as an investigation surface: point it at any backend via
* `PL_ADDRESS` / `PL_TEST_USER` / `PL_TEST_PASSWORD` and read the logged payload
* (notably `e`, the expiration timestamp) to inspect that backend's license.
*/
test("license payload carries all required fields", async () => {
const client = await getTestClient();

const resp = await client.license();
expect(resp.isOk).toBe(true);

// `responseBody` is the raw licensing-server body: a JSON-encoded token string.
const token = JSON.parse(Buffer.from(resp.responseBody).toString("utf8")) as string;

// decodeLicenseToken() itself asserts required fields are present and well-typed;
// an incomplete license from the backend would throw here.
const payload: LicensePayload = decodeLicenseToken(token);

// Explicit expectations mirror the required fields of LicensePayload, kept here
// so the contract is visible and easy to extend for future investigations.
expect(typeof payload.v).toBe("number");
expect(typeof payload.e).toBe("number");
expect(typeof payload.u).toBe("string");
expect(payload.u.length).toBeGreaterThan(0);
expect(typeof payload.m).toBe("string");

// Expiration must come after the valid-from moment.
expect(payload.e).toBeGreaterThan(payload.v);

console.log("license payload:", JSON.stringify(payload, null, 2));
Comment thread
DenKoren marked this conversation as resolved.
console.log("valid from:", new Date(payload.v * 1000).toISOString());
console.log("expires at:", new Date(payload.e * 1000).toISOString());
if (payload.w !== undefined) {
console.log("warn after:", new Date(payload.w * 1000).toISOString());
}
});
115 changes: 115 additions & 0 deletions lib/node/pl-client/src/core/license.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* Monitoring mode for a single telemetry channel, as encoded in the license.
*
* See https://github.com/milaboratory/text/blob/main/features/monitoring/platforma-monitoring-prd.md
*/
export type LicenseMonitoringMode = "with_id" | "no_id" | "none";

/**
* License payload — the decoded body of a Platforma license token.
*
* The token is issued by the licensing server (milm2) and returned to clients
* verbatim by the backend's Maintenance API — see {@link PlClient.license}. The
* token has the form `I.<base64Payload>.<watermark>.<signature>`; this type
* describes the JSON found in `<base64Payload>` once base64-decoded (via
* {@link decodeLicenseToken}).
*
* Issuer reference: https://github.com/milaboratory/milm2/blob/master/src/types/index.ts
* Backend counterpart: `core/pl/cmd/platforma/license.go` (`License` struct).
*/
export interface LicensePayload {
/** Unix timestamp (seconds) the license is valid from. */
v: number;
/** Unix timestamp (seconds) the license expires after. */
e: number;
/** UID of the customer. */
u: string;
/** Metric marker — attached to usage statistics. */
m: string;
/** [optional] Fallback license code, used once the license has expired. */
l?: string;
/** [optional] Unix timestamp (seconds); warn the user about expiration after this moment. */
w?: number;
/** [optional] Warning text; if `w` is set while `wt` is absent, a default warning should be shown. */
wt?: string;
/** [optional] Expiration text — shown to the user once the license has expired. */
et?: string;
c?: Record<string, unknown>;
t?: Record<string, unknown>;
hw?: string;
f?: Record<string, string>;
/**
* Monitoring configuration.
* https://github.com/milaboratory/text/blob/main/features/monitoring/platforma-monitoring-prd.md
*/
s?: {
usage?: LicenseMonitoringMode;
performance?: LicenseMonitoringMode;
errors?: LicenseMonitoringMode;
safeErrors?: LicenseMonitoringMode;
errorTraces?: LicenseMonitoringMode;
};
}

/** Fixed first segment of a well-formed license token. */
const LICENSE_TOKEN_PREFIX = "I";
/** Fixed watermark segment of a well-formed license token. */
const LICENSE_TOKEN_WATERMARK = "CPECUVF";

/**
* Runtime guard: asserts that a value parsed from a license token carries every
* required field with the expected type. Throws a descriptive error otherwise.
*
* Only the always-present fields (`v`, `e`, `u`, `m`) are validated — everything
* else in {@link LicensePayload} is optional and issuer-dependent.
*/
export function assertLicensePayload(value: unknown): asserts value is LicensePayload {
if (typeof value !== "object" || value === null) {
throw new Error(`invalid license payload: expected an object, got ${typeof value}`);
}
const p = value as Record<string, unknown>;
const requireType = (field: string, type: "number" | "string") => {
if (typeof p[field] !== type) {
throw new Error(
`invalid license payload: field "${field}" must be a ${type}, got ${typeof p[field]}`,
);
}
};
requireType("v", "number");
requireType("e", "number");
requireType("u", "string");
requireType("m", "string");
}

/**
* Decode a raw license token into a validated {@link LicensePayload}.
*
* Mirrors the desktop app's token parser and the backend's `NewLicenseFromPayload`:
* it splits the `I.<base64Payload>.<watermark>.<signature>` envelope, checks the
* fixed prefix/watermark, base64-decodes the payload segment and validates that
* all required fields are present. This does NOT verify the cryptographic
* signature — that is a separate concern owned by the desktop's LicenseManager.
*/
export function decodeLicenseToken(token: string): LicensePayload {
const [prefix, base64Payload, watermark, signature] = token.split(".");
if (
prefix !== LICENSE_TOKEN_PREFIX ||
watermark !== LICENSE_TOKEN_WATERMARK ||
typeof base64Payload !== "string" ||
typeof signature !== "string"
) {
throw new Error("invalid license token: unexpected envelope format");
}
Comment thread
DenKoren marked this conversation as resolved.
Outdated

const payloadStr = Buffer.from(base64Payload, "base64").toString("utf8");

let parsed: unknown;
try {
parsed = JSON.parse(payloadStr);
} catch (cause) {
throw new Error("invalid license token: payload is not valid JSON", { cause });
}

assertLicensePayload(parsed);
return parsed;
}
1 change: 1 addition & 0 deletions lib/node/pl-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from "./core/types";
export * as Pl from "./helpers/pl";
export * from "./core/config";
export * from "./core/client";
export * from "./core/license";
export * from "./core/driver";
export * from "./core/transaction";
export * from "./core/errors";
Expand Down
Loading