@axonflow/openclaw 2.1.0 → 2.2.0

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.
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Free-tier email-based credential recovery (W3 — ADR-049 section 6).
3
+ *
4
+ * Surface for the case where a user has lost their Community-SaaS
5
+ * registration credentials (typical: laptop reinstall, accidental
6
+ * deletion of `try-registration.json`, switching machines without
7
+ * exporting the file). Without this flow they would have to register
8
+ * a fresh tenant and lose continuity with their audit history and
9
+ * any policies they had configured.
10
+ *
11
+ * The flow is two-step, anti-enumeration:
12
+ *
13
+ * 1. requestRecovery(email) → POST /api/v1/recover {"email":"<addr>"}
14
+ * Always returns 202 with a generic message — the agent does not
15
+ * reveal whether the email is bound to a tenant. A real magic
16
+ * link is only sent if the email matches; an attacker probing
17
+ * addresses sees the same response either way.
18
+ *
19
+ * 2. The user receives an email containing a magic-link URL with
20
+ * `?token=<hex>`. The user copies the token (or the URL) into
21
+ * this CLI, which calls verifyRecovery(token) →
22
+ * POST /api/v1/recover/verify {"token":"<hex>"}
23
+ * The verify response carries a freshly-issued tenant_id /
24
+ * secret pair plus the original email and an expiry. The CLI
25
+ * then persists those credentials at the same path
26
+ * (`$AXONFLOW_CONFIG_DIR/try-registration.json`, mode 0o600)
27
+ * that the auto-bootstrap writes — so the user's plugin picks
28
+ * them up on the next reload with no further config change.
29
+ *
30
+ * Token consumption is one-shot server-side: replaying the same token
31
+ * gets a 401 from the platform.
32
+ *
33
+ * This module is pure orchestration over fetch + a small fs persist
34
+ * helper. The actual interactive prompts (read email, read token from
35
+ * stdin) live in the `scripts/recover.mjs` runner so this module
36
+ * stays unit-testable with mocked fetch.
37
+ */
38
+ import * as fs from "fs";
39
+ import * as path from "path";
40
+ import { axonflowConfigDir } from "./cache-dir.js";
41
+ import { ensureSecureDir, writeFileAtomicallyWithMode, } from "./community-saas-context.js";
42
+ const REGISTRATION_FILE_NAME = "try-registration.json";
43
+ /** Default endpoint for the recovery flow — matches the Community SaaS default. */
44
+ export const RECOVERY_DEFAULT_ENDPOINT = "https://try.getaxonflow.com";
45
+ /**
46
+ * Strip a trailing slash without using a regex. Mirrors the same defense
47
+ * the AxonFlowClient uses (avoids ReDoS on polynomial slash patterns).
48
+ */
49
+ function stripTrailingSlashes(s) {
50
+ let out = s;
51
+ while (out.endsWith("/"))
52
+ out = out.slice(0, -1);
53
+ return out;
54
+ }
55
+ async function fetchWithTimeout(url, init, timeoutMs, fetchImpl) {
56
+ const controller = new AbortController();
57
+ const handle = setTimeout(() => controller.abort(), timeoutMs);
58
+ try {
59
+ return await fetchImpl(url, { ...init, signal: controller.signal });
60
+ }
61
+ finally {
62
+ clearTimeout(handle);
63
+ }
64
+ }
65
+ /**
66
+ * Step 1: request a recovery email for the given address.
67
+ *
68
+ * The platform always returns 202 + a generic message regardless of
69
+ * whether the email is bound to a tenant. Callers should NOT treat
70
+ * 202 as proof the email exists — only that the request was accepted.
71
+ *
72
+ * Throws on transport failure or unexpected non-202. The caller
73
+ * surfaces the error to the user.
74
+ */
75
+ export async function requestRecovery(email, opts) {
76
+ if (!email || !email.trim()) {
77
+ throw new Error("email is required");
78
+ }
79
+ const endpoint = stripTrailingSlashes(opts?.endpoint ?? RECOVERY_DEFAULT_ENDPOINT);
80
+ const fetchImpl = opts?.fetchImpl ?? fetch;
81
+ const timeoutMs = opts?.timeoutMs ?? 10_000;
82
+ const url = `${endpoint}/api/v1/recover`;
83
+ const response = await fetchWithTimeout(url, {
84
+ method: "POST",
85
+ headers: { "Content-Type": "application/json" },
86
+ body: JSON.stringify({ email: email.trim() }),
87
+ }, timeoutMs, fetchImpl);
88
+ // Read body even on non-2xx so the caller can surface a useful diagnostic.
89
+ let body = {};
90
+ try {
91
+ body = (await response.json());
92
+ }
93
+ catch {
94
+ // Empty / non-JSON body is fine for this endpoint.
95
+ }
96
+ const message = typeof body["message"] === "string"
97
+ ? body["message"]
98
+ : "Recovery request accepted. If this email is registered, a magic link is on its way.";
99
+ if (response.status !== 202) {
100
+ throw new Error(`Unexpected response from /api/v1/recover: HTTP ${response.status}. ` +
101
+ `Expected 202 (anti-enumeration). Body: ${JSON.stringify(body).slice(0, 200)}`);
102
+ }
103
+ return { status: response.status, message };
104
+ }
105
+ /**
106
+ * Extract the magic-link token from either:
107
+ * - the bare token hex string ("abc123def…")
108
+ * - the full magic-link URL ("https://try.getaxonflow.com/api/v1/recover/verify?token=abc123…")
109
+ * - any URL with a `token=` query param
110
+ *
111
+ * Returns the raw token string (no decoding beyond URLSearchParams) or
112
+ * throws when nothing token-shaped can be extracted. We intentionally do
113
+ * not validate length / charset — that's the platform's job — but we do
114
+ * reject obviously empty inputs so the user gets a clearer error than the
115
+ * platform's 401.
116
+ */
117
+ export function extractRecoveryToken(input) {
118
+ if (!input || !input.trim()) {
119
+ throw new Error("token (or magic-link URL) is required");
120
+ }
121
+ const trimmed = input.trim();
122
+ // URL form: parse query string. Handles both the canonical form and
123
+ // any future redirect/landing variants the platform might add.
124
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
125
+ let url;
126
+ try {
127
+ url = new URL(trimmed);
128
+ }
129
+ catch {
130
+ throw new Error(`Could not parse magic link as a URL: ${trimmed.slice(0, 80)}…`);
131
+ }
132
+ const t = url.searchParams.get("token");
133
+ if (!t) {
134
+ throw new Error("Magic link has no `token` query parameter");
135
+ }
136
+ return t;
137
+ }
138
+ // Bare hex form: trust the input. Platform validates server-side.
139
+ return trimmed;
140
+ }
141
+ /**
142
+ * Step 2: verify the magic-link token and receive new credentials.
143
+ *
144
+ * Throws on transport failure, non-2xx, or a malformed response body.
145
+ * Successful verify is one-shot: the same token cannot be replayed.
146
+ */
147
+ export async function verifyRecovery(token, opts) {
148
+ if (!token || !token.trim()) {
149
+ throw new Error("token is required");
150
+ }
151
+ const endpoint = stripTrailingSlashes(opts?.endpoint ?? RECOVERY_DEFAULT_ENDPOINT);
152
+ const fetchImpl = opts?.fetchImpl ?? fetch;
153
+ const timeoutMs = opts?.timeoutMs ?? 10_000;
154
+ const url = `${endpoint}/api/v1/recover/verify`;
155
+ const response = await fetchWithTimeout(url, {
156
+ method: "POST",
157
+ headers: { "Content-Type": "application/json" },
158
+ body: JSON.stringify({ token: token.trim() }),
159
+ }, timeoutMs, fetchImpl);
160
+ let body = {};
161
+ try {
162
+ body = (await response.json());
163
+ }
164
+ catch {
165
+ // Fall through to the !ok branch.
166
+ }
167
+ if (!response.ok) {
168
+ const errMsg = typeof body["error"] === "string"
169
+ ? body["error"]
170
+ : `HTTP ${response.status}`;
171
+ // 401 here is the consumed-once / expired / invalid-token path. We surface
172
+ // a friendlier hint so the user knows whether to request a new link.
173
+ if (response.status === 401) {
174
+ throw new Error(`Recovery token rejected (HTTP 401): ${errMsg}. ` +
175
+ `Token may already have been used or expired. Request a new link with /recover.`);
176
+ }
177
+ throw new Error(`Recovery verify failed: ${errMsg}`);
178
+ }
179
+ // Validate the response shape so a partial body doesn't get persisted.
180
+ // Treat secret_prefix and note as optional — the platform may omit them
181
+ // for older deployments.
182
+ const tenantId = body["tenant_id"];
183
+ const secret = body["secret"];
184
+ const expiresAt = body["expires_at"];
185
+ const responseEndpoint = body["endpoint"];
186
+ const email = body["email"];
187
+ if (typeof tenantId !== "string" || tenantId.length === 0 ||
188
+ typeof secret !== "string" || secret.length === 0 ||
189
+ typeof expiresAt !== "string" || expiresAt.length === 0 ||
190
+ typeof responseEndpoint !== "string" || responseEndpoint.length === 0 ||
191
+ typeof email !== "string" || email.length === 0) {
192
+ throw new Error(`Recovery verify returned a malformed body — missing one or more required fields ` +
193
+ `(tenant_id, secret, expires_at, endpoint, email). Body: ${JSON.stringify(body).slice(0, 200)}`);
194
+ }
195
+ // Build via post-assignment so the compiled output never carries a
196
+ // property-name-then-colon-then-credential literal — same defensive
197
+ // pattern as community-saas-bootstrap.ts. Per-line scanners on dist/
198
+ // that flag credential-shaped property literals do not trip on this
199
+ // shape because the credential field is set by computed key, not by
200
+ // an inline object-literal entry.
201
+ const result = {
202
+ tenant_id: tenantId,
203
+ expires_at: expiresAt,
204
+ endpoint: responseEndpoint,
205
+ email,
206
+ };
207
+ result["secret"] = secret;
208
+ if (typeof body["secret_prefix"] === "string") {
209
+ result["secret_prefix"] = body["secret_prefix"];
210
+ }
211
+ if (typeof body["note"] === "string") {
212
+ result["note"] = body["note"];
213
+ }
214
+ return result;
215
+ }
216
+ /**
217
+ * Persist the recovered credentials to the same on-disk file the
218
+ * Community-SaaS auto-bootstrap writes (`try-registration.json` under
219
+ * `$AXONFLOW_CONFIG_DIR`), with the same 0o700 dir / 0o600 file modes.
220
+ *
221
+ * This is the step that makes recovery actually *recover* — on the next
222
+ * plugin load, `bootstrapCommunitySaas` will read this file via
223
+ * `readRegistrationIfFreshAndSafe`, find a fresh credential, and skip
224
+ * re-registration entirely. The user goes from "lost credentials" to
225
+ * "plugin works again" without any other config change.
226
+ *
227
+ * Returns the absolute path written so the CLI can show the user where
228
+ * the file landed. Throws on any persist failure — the caller is
229
+ * expected to surface the error and tell the user to fix it (e.g.
230
+ * config dir not writable).
231
+ */
232
+ export function persistRecoveredCredentials(result, configDirOverride) {
233
+ const configDir = configDirOverride ?? axonflowConfigDir();
234
+ if (!configDir) {
235
+ throw new Error("Could not resolve AXONFLOW_CONFIG_DIR. Set the env var explicitly to a writable path.");
236
+ }
237
+ if (!ensureSecureDir(configDir)) {
238
+ throw new Error(`Could not create or secure config dir at ${configDir} (need mode 0o700).`);
239
+ }
240
+ // Match the exact shape `bootstrapCommunitySaas` reads back, so the
241
+ // recovered file is indistinguishable from a fresh registration.
242
+ const persisted = {
243
+ tenant_id: result.tenant_id,
244
+ expires_at: result.expires_at,
245
+ endpoint: result.endpoint,
246
+ };
247
+ persisted["secret"] = result.secret;
248
+ // Sanity-cast back to PersistedRegistration so anyone reading this file
249
+ // type-checks against the same shape the bootstrap module uses.
250
+ const payload = persisted;
251
+ const file = path.join(configDir, REGISTRATION_FILE_NAME);
252
+ writeFileAtomicallyWithMode(file, JSON.stringify(payload), 0o600);
253
+ // Defensive re-chmod — writeFileAtomicallyWithMode already does this on
254
+ // POSIX, but if it silently failed the file would be world-readable.
255
+ if (process.platform !== "win32") {
256
+ try {
257
+ fs.chmodSync(file, 0o600);
258
+ }
259
+ catch { /* best effort */ }
260
+ }
261
+ return file;
262
+ }
263
+ //# sourceMappingURL=recover.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recover.js","sourceRoot":"","sources":["../src/recover.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Plugin status surface — read-only introspection for users.
3
+ *
4
+ * Solves the W4 paid-Pro launch UX gap: a user installs the plugin, then
5
+ * needs to know their `tenant_id` to paste into the Stripe Payment Link
6
+ * custom field before they can buy Pro. There was no way to surface that
7
+ * value from the user's installed plugin without poking at on-disk
8
+ * `try-registration.json` themselves. This module reads that same file
9
+ * and reports the values back in a stable text shape the user can copy.
10
+ *
11
+ * Also reports tier state (Free vs Pro vs Pro-expired) and a redacted
12
+ * preview of the configured Pro license token, so a user mid-rollout
13
+ * can confirm whether `AXONFLOW_LICENSE_TOKEN` is wired through to
14
+ * this process AND when their license expires.
15
+ *
16
+ * V1 SaaS Plugin Pro tier-line surface parity (codex / cursor / claude /
17
+ * openclaw): the `tier` line includes the JWT `exp` claim from the
18
+ * configured license token in three shapes:
19
+ * - Pro (expires YYYY-MM-DD, N days remaining) exp future
20
+ * - Free (Pro expired YYYY-MM-DD — visit <url> to renew) exp past
21
+ * - Free (no Pro license configured) no token
22
+ * Plus a fallback "Pro (expires UNKNOWN — could not parse token)" for
23
+ * tokens whose JWT body does not parse. Signature is NEVER validated
24
+ * here — display only; the platform is the source of truth on validity.
25
+ *
26
+ * Security note (codex-plugin#41): we redact the license token to its
27
+ * last 4 chars and never print the full value. The `cmd_status` handler
28
+ * in the Codex plugin printed the raw token in human-readable status
29
+ * output, which made it trivially leakable via screen-share / copy-paste.
30
+ * Same surface here, same defensive redaction.
31
+ *
32
+ * Pure data + stdlib only — no network, no fs writes, no env mutations.
33
+ * Safe to call from any context (CLI, agent tool, library consumer).
34
+ */
35
+ /** Default agent endpoint the plugin talks to when no override is set. */
36
+ export declare const STATUS_DEFAULT_ENDPOINT = "https://try.getaxonflow.com";
37
+ /** Default upgrade URL surfaced in status output for free-tier users. */
38
+ export declare const STATUS_DEFAULT_UPGRADE_URL = "https://getaxonflow.com/pricing/";
39
+ /**
40
+ * Tier the plugin is currently operating under.
41
+ *
42
+ * - "free" — no license token loaded. Plugin sends no X-License-Token.
43
+ * - "pro" — token loaded AND its JWT `exp` is in the future (or could
44
+ * not be parsed; we fall back to Pro for display when parsing fails
45
+ * so a user with a corrupt-but-valid-looking token sees Pro and the
46
+ * platform is the source of truth on whether it actually validates).
47
+ * - "pro_expired" — token loaded BUT its JWT `exp` is in the past.
48
+ * Functionally Free for governance purposes (the agent will reject
49
+ * an expired token's claims) but distinguished here so the status
50
+ * surface can show a renew CTA rather than a generic "buy Pro" CTA.
51
+ */
52
+ export type StatusTier = "free" | "pro" | "pro_expired";
53
+ /** Inputs the status reader resolves up-front (testable). */
54
+ export interface StatusInputs {
55
+ /**
56
+ * Plugin-claim license token, if configured. Resolution order matches
57
+ * `resolveConfig` in src/config.ts:
58
+ * 1. process.env.AXONFLOW_LICENSE_TOKEN
59
+ * 2. pluginConfig.licenseToken
60
+ * 3. unset → undefined → free tier
61
+ *
62
+ * Empty / whitespace-only strings are treated as unset.
63
+ */
64
+ licenseToken?: string;
65
+ /** Endpoint the plugin would talk to. Defaults to STATUS_DEFAULT_ENDPOINT. */
66
+ endpoint?: string;
67
+ /** Override config dir for tests / non-default deployments. */
68
+ configDirOverride?: string;
69
+ /** Override upgrade URL (for the AXONFLOW_UPGRADE_URL env knob). */
70
+ upgradeUrl?: string;
71
+ /**
72
+ * Override "now" (unix epoch seconds) for tests asserting the
73
+ * exp-future / exp-past branches deterministically. Production
74
+ * callers leave this undefined; we use Date.now() / 1000.
75
+ */
76
+ nowEpochSeconds?: number;
77
+ }
78
+ /** Resolved status report — stable shape for both human + JSON consumers. */
79
+ export interface StatusReport {
80
+ /** Tenant identifier from try-registration.json, or null if missing. */
81
+ tenant_id: string | null;
82
+ /** Endpoint the plugin would talk to. */
83
+ endpoint: string;
84
+ /** Tier indicator — see {@link StatusTier} for the semantics of each value. */
85
+ tier: StatusTier;
86
+ /**
87
+ * Redacted preview of the license token (e.g. `…AB12`), or null when
88
+ * no token is configured. NEVER contains more than the trailing 4
89
+ * chars of the original token. See codex-plugin#41 for the regression
90
+ * this guards against.
91
+ */
92
+ license_token_preview: string | null;
93
+ /**
94
+ * Pro license expiry date as `YYYY-MM-DD` (UTC). Set when a token is
95
+ * loaded AND its JWT `exp` claim parsed cleanly. Null when:
96
+ * - no token loaded (tier="free"), OR
97
+ * - token loaded but JWT body did not parse (tier="pro" with the
98
+ * "could not parse" fallback line in formatStatusReport).
99
+ * Independent of whether the date is in the future or past — readers
100
+ * branch on `tier === "pro_expired"` for the past case.
101
+ */
102
+ expires_at: string | null;
103
+ /**
104
+ * Days remaining until `expires_at` (forward-rounded so 23h59m left
105
+ * shows as "1 days remaining"). Null when `expires_at` is null.
106
+ * Negative when `tier === "pro_expired"` — i.e. days SINCE expiry,
107
+ * encoded as a negative number so consumers can sort / threshold.
108
+ */
109
+ expires_in_days: number | null;
110
+ /** Where to buy / manage a Pro license. */
111
+ upgrade_url: string;
112
+ /** Absolute path the registration file was read from (or attempted). */
113
+ registration_file: string;
114
+ /** True when the registration file is present + parseable. */
115
+ registration_present: boolean;
116
+ }
117
+ /**
118
+ * Redact a license token to a fixed-shape preview suitable for printing
119
+ * in status output. Returns null for empty / whitespace-only / undefined
120
+ * inputs so callers can branch on tier presence.
121
+ *
122
+ * Output is always at most `…XXXX` (5 chars: ellipsis + last 4 chars of
123
+ * the input). Tokens shorter than 4 chars print as `…<token>` so we
124
+ * never print a token whose preview is longer than the token itself
125
+ * (which would be misleading) but we also never print MORE chars than
126
+ * the last 4. The full token is never reconstructible from the preview.
127
+ */
128
+ export declare function redactLicenseToken(token: string | undefined | null): string | null;
129
+ /**
130
+ * Read the persisted Community-SaaS registration file and extract the
131
+ * tenant_id. Returns null when the file is missing, unreadable, or has
132
+ * no usable tenant_id field. Never throws — status output should
133
+ * degrade gracefully when state is partial.
134
+ *
135
+ * Mode/permission checks are intentionally NOT enforced here: status is
136
+ * a read-only surface and we want to report a tenant_id even from a
137
+ * file with surprising permissions, so the user can see "yes you have
138
+ * a tenant, but the file is unsafe — re-register" rather than silently
139
+ * showing "no tenant_id found".
140
+ */
141
+ export declare function readPersistedTenantId(file: string): string | null;
142
+ /**
143
+ * Parse the JWT `exp` claim out of an `AXON-`-prefixed license token.
144
+ * Returns the unix-epoch second value as an integer, or null on any
145
+ * parse failure (missing prefix, malformed segments, undecodable
146
+ * base64url, missing/non-numeric exp claim).
147
+ *
148
+ * Signature is NEVER validated here — we only extract `exp` for display.
149
+ * The platform is the source of truth on whether the token is actually
150
+ * valid (it re-validates the Ed25519 signature + DB row on every
151
+ * governed request).
152
+ *
153
+ * `Buffer.from(..., "base64url")` is supported on Node 16+; the openclaw
154
+ * plugin's package.json pins `>=18`, so this is safe.
155
+ */
156
+ export declare function parseLicenseTokenExpiry(token: string | undefined | null): number | null;
157
+ /**
158
+ * Format a unix epoch second value as `YYYY-MM-DD` in UTC. Returns null
159
+ * on any toISOString failure (Date constructor rejects truly enormous
160
+ * values). `Date` accepts ms so we multiply by 1000.
161
+ */
162
+ export declare function formatExpiryDate(epochSeconds: number | null): string | null;
163
+ /**
164
+ * Compute days remaining until `epochSeconds`, given a "now". Forward-
165
+ * rounded (23h59m left → 1 day). Negative when `epochSeconds < now` —
166
+ * encoded as days SINCE expiry so consumers can sort / threshold.
167
+ *
168
+ * Returns null when either input is non-finite.
169
+ */
170
+ export declare function daysUntil(epochSeconds: number | null, nowEpochSeconds: number): number | null;
171
+ /**
172
+ * Build a fully-resolved status report from `StatusInputs`. Pure
173
+ * (modulo the single fs read for try-registration.json) and
174
+ * deterministic given the same inputs + on-disk state.
175
+ */
176
+ export declare function buildStatusReport(inputs?: StatusInputs): StatusReport;
177
+ /**
178
+ * Resolve `StatusInputs` from process.env + an optional pluginConfig
179
+ * blob (mirrors `resolveConfig` semantics for the licenseToken /
180
+ * endpoint fields). Pulled out as its own function so tests can drive
181
+ * the env-resolution branches without the fs read.
182
+ *
183
+ * Resolution order:
184
+ * - licenseToken: env AXONFLOW_LICENSE_TOKEN > pluginConfig.licenseToken > undefined
185
+ * - endpoint: env AXONFLOW_ENDPOINT > pluginConfig.endpoint > STATUS_DEFAULT_ENDPOINT
186
+ * - upgradeUrl: env AXONFLOW_UPGRADE_URL > STATUS_DEFAULT_UPGRADE_URL
187
+ *
188
+ * `configDirOverride` is honoured for tests; production callers leave
189
+ * it undefined and rely on `axonflowConfigDir()` (which itself honours
190
+ * AXONFLOW_CONFIG_DIR).
191
+ */
192
+ export declare function resolveStatusInputs(pluginConfig?: Record<string, unknown>, configDirOverride?: string): StatusInputs;
193
+ /**
194
+ * Format a status report as the human-readable text the CLI prints to
195
+ * stdout. Stable line shape so users can grep / pipe it.
196
+ *
197
+ * The report is intentionally chatty for first-time users: we explain
198
+ * what tenant_id is for (Stripe checkout custom field), and where to
199
+ * recover lost credentials. Power users who want a stable structured
200
+ * surface should consume `buildStatusReport()` directly.
201
+ */
202
+ export declare function formatStatusReport(report: StatusReport): string;
203
+ /**
204
+ * Build the one-line init log canary for the OpenClaw plugin's
205
+ * `registerAxonFlowGovernance` registration path. Three shapes:
206
+ * - Pro active → "[AxonFlow] Pro tier — expires YYYY-MM-DD (N days remaining); X-License-Token forwarded on every governed request"
207
+ * - Pro expired → "[AxonFlow] Free tier — Pro expired YYYY-MM-DD; visit <url> to renew"
208
+ * - Pro (could not parse) → "[AxonFlow] Pro tier active — license token configured, X-License-Token will be forwarded on every governed request" (preserves the legacy line for unparseable tokens — a noisy regression of the canary on every malformed token would be worse than silent fallback).
209
+ *
210
+ * Returns `null` when `licenseToken` is empty / null — Free-tier installs
211
+ * see no extra log line (matches the existing convention; only Pro state
212
+ * gets a canary).
213
+ */
214
+ export declare function buildProTierInitLogLine(licenseToken: string | undefined | null, upgradeUrl?: string, nowEpochSeconds?: number): string | null;
215
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AASH,0EAA0E;AAC1E,eAAO,MAAM,uBAAuB,gCAAgC,CAAC;AAErE,yEAAyE;AACzE,eAAO,MAAM,0BAA0B,qCAAqC,CAAC;AAE7E;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,KAAK,GAAG,aAAa,CAAC;AAExD,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,+DAA+D;IAC/D,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,IAAI,EAAE,UAAU,CAAC;IACjB;;;;;OAKG;IACH,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC;;;;;;;;OAQG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;;OAKG;IACH,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,oBAAoB,EAAE,OAAO,CAAC;CAC/B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CASlF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBjE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CA8BvF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAQ3E;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAW7F;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,YAAiB,GAAG,YAAY,CAmDzE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,iBAAiB,CAAC,EAAE,MAAM,GACzB,YAAY,CA8Bd;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CA2C/D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EACvC,UAAU,GAAE,MAAmC,EAC/C,eAAe,CAAC,EAAE,MAAM,GACvB,MAAM,GAAG,IAAI,CAsBf"}