Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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. The type includes the optional `le` claim (the license's own expiration, independent of the short-lived token TTL `e`). Includes a test that fetches the license from a live backend and asserts the required payload fields are always present.
46 changes: 46 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,46 @@
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");

// Token expiration must come after the token's 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("token valid from:", new Date(payload.v * 1000).toISOString());
console.log("token expires at:", new Date(payload.e * 1000).toISOString());
if (payload.le !== undefined) {
console.log("license expires at:", new Date(payload.le * 1000).toISOString());
}
if (payload.w !== undefined) {
console.log("warn after:", new Date(payload.w * 1000).toISOString());
}
});
136 changes: 136 additions & 0 deletions lib/node/pl-client/src/core/license.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* 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 itself is valid from (issuance), not a token time. */
v: number;
/**
* Unix timestamp (seconds) this license token expires after — the token's
* TTL, not the license's own validity period. Tokens are short-lived and
* re-minted (e.g. a 24h TTL yields `e` = issued-at + 86400) even when the
* underlying license is valid far longer, so `e` is not the license
* expiration date.
*/
e: number;
/**
* [optional] Unix timestamp (seconds) the license itself expires — the
* underlying license/contract end date, independent of the short-lived
* token TTL `e`. Derived from the contract expiration before any token-TTL
* clamping, so this is the field to use when reasoning about whether the
* license (not the current token) has expired. Absent on tokens from older
* licensing servers that predate the `le` claim.
*/
le?: 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. Internal:
* callers get validation for free via {@link decodeLicenseToken}.
*/
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 — as token is used by MiLM manager to access its API, this signature
* is for MiLM itself, who issued the token.
*/
export function decodeLicenseToken(token: string): LicensePayload {
const segments = token.split(".");
const [prefix, base64Payload, watermark, signature] = segments;
if (
segments.length !== 4 ||
prefix !== LICENSE_TOKEN_PREFIX ||
watermark !== LICENSE_TOKEN_WATERMARK ||
typeof base64Payload !== "string" ||
base64Payload.length === 0 ||
typeof signature !== "string" ||
signature.length === 0
) {
throw new Error("invalid license token: unexpected envelope format");
}

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