501 lines
21 KiB
Markdown
501 lines
21 KiB
Markdown
# WRNexusJS — Security & Power Improvement Plan
|
||
|
||
Prepared for WorkRoot · covers `E:\WrNexus` (framework v0.8.4)
|
||
|
||
This plan is scoped and sequenced the way the repo's own roadmap docs are (`ROADMAP-V1.md`,
|
||
`IMPLEMENTATION-ROADMAP-0.8.md`): version-gated phases, one package/file set per item, with
|
||
implementation, tests, and doc updates called out per item so each phase can ship as a real release
|
||
with a `bun run validate:0.X` gate, like 0.7 and 0.8 did.
|
||
|
||
Every proposed API below follows conventions already in the repo (workspace `package.json` shape,
|
||
`Middleware`/`Context` typing from `@wrnexus/core`, ASVS row format in `docs/SECURITY-ASVS-5.md`,
|
||
audit-sink pattern from `packages/authz/src/audit.ts`) rather than inventing new patterns.
|
||
|
||
---
|
||
|
||
## Phasing overview
|
||
|
||
| Phase | Version | Theme | New packages | Est. effort |
|
||
| ----- | ------------- | ----------------------------------------------- | ---------------------------------------------- | ----------- |
|
||
| 1 | 0.8.5 (patch) | Security default fixes, no breaking changes | none | 1–2 weeks |
|
||
| 2 | 0.9 | Distributed rate limiting + live security audit | `@wrnexus/ratelimit-redis` | 2–3 weeks |
|
||
| 3 | 0.10 | Product-critical DX packages | `@wrnexus/mail`, `@wrnexus/flags` | 4–6 weeks |
|
||
| 4 | 0.11 | Search + AI pairing | `@wrnexus/search` | 3–4 weeks |
|
||
| 5 | 0.12 | Enterprise/government readiness | SAML in `@wrnexus/auth`, `@wrnexus/compliance` | 5–7 weeks |
|
||
| 6 | 1.0 | Monetization + ecosystem | `@wrnexus/billing`, public release strategy | 6–10 weeks |
|
||
|
||
Total: roughly 6–8 months at a small-team pace, phased so each release is independently shippable
|
||
and dogfoodable on `workroot.in` / `wrnexusjs.dev` / the WRNexus SaaS itself before the next phase
|
||
starts.
|
||
|
||
---
|
||
|
||
## Phase 1 — v0.8.5: Security default fixes
|
||
|
||
No new packages. Pure hardening of existing code, all changes are config-default flips, so they are
|
||
non-breaking for anyone who already sets these fields explicitly and only change behavior for people
|
||
relying on the current default.
|
||
|
||
### 1.1 Trusted Types default allowlist
|
||
|
||
**File:** `packages/core/src/headers.ts`, function `applyTrustedTypesDirectives`
|
||
|
||
**Problem:** `policyNames` defaults to `["*"]` in production, so any script — including an injected
|
||
one — can register a Trusted Types policy. This defeats the XSS mitigation Trusted Types exists for.
|
||
|
||
**Change:**
|
||
|
||
```ts
|
||
// Before
|
||
const policyNames =
|
||
typeof trustedTypes === "object" && trustedTypes.policyNames?.length
|
||
? trustedTypes.policyNames
|
||
: ["*"];
|
||
|
||
// After
|
||
const policyNames =
|
||
typeof trustedTypes === "object" && trustedTypes.policyNames?.length
|
||
? trustedTypes.policyNames
|
||
: ["wrnexus", "default"];
|
||
```
|
||
|
||
Update the `TrustedTypesConfig.policyNames` doc comment to explain the new default and how to opt
|
||
back into `["*"]` for apps with third-party extensions that need it.
|
||
|
||
**Tests:** update `packages/core/test/headers.test.ts` assertions that currently expect `*`.
|
||
|
||
**Docs:** update the "Production configuration baseline" block in `docs/SECURITY-PERFORMANCE-0.7.md`
|
||
and add a migration note to `docs/UPGRADE-0.8.3.md`-style upgrade doc for 0.8.5.
|
||
|
||
### 1.2 HSTS `preload` default
|
||
|
||
**File:** `packages/core/src/headers.ts`, function `serializeHsts`
|
||
|
||
**Problem:** `preload: true` is on by default whenever `mode === "production"`. Preload-list
|
||
submission is a long-lived commitment (removal takes months across browsers); defaulting it on for
|
||
every production build is a footgun for teams not ready to guarantee HTTPS on every subdomain
|
||
permanently.
|
||
|
||
**Change:**
|
||
|
||
```ts
|
||
function serializeHsts(config: HstsConfig): string {
|
||
const parts = [`max-age=${config.maxAge ?? 31536000}`];
|
||
if (config.includeSubDomains !== false) parts.push("includeSubDomains");
|
||
if (config.preload === true) parts.push("preload"); // was: !== false
|
||
return parts.join("; ");
|
||
}
|
||
```
|
||
|
||
`includeSubDomains` can stay default-on (safe, reversible); only `preload` flips to opt-in.
|
||
|
||
**Tests:** update `packages/core/test/headers.test.ts`.
|
||
|
||
**Docs:** update `SECURITY-ASVS-5.md` row for `v5.0.0-3.4.1` evidence note and the production config
|
||
baseline example (explicitly show `hsts: { preload: true }` as something apps opt into, with a
|
||
one-line warning comment).
|
||
|
||
### 1.3 Live security-header verification (`--url` mode)
|
||
|
||
**File:** `packages/cli/src/security-command.ts`
|
||
|
||
**Problem:** `securityAudit()` already exists and is solid — it loads the local app config, builds a
|
||
synthetic request/response, and runs `withSecurityHeaders` to check what headers _would_ be emitted.
|
||
It never checks what a _deployed_ site is actually serving, so a misconfigured reverse proxy,
|
||
missing env var, or config drift between local and production is invisible to `wrnexus security
|
||
audit` today.
|
||
|
||
**Change:** add a second code path that takes a URL instead of an app root:
|
||
|
||
```ts
|
||
export interface SecurityAuditOptions {
|
||
appRoot?: string;
|
||
/** Fetch a live deployment and audit its actual response headers instead of a local config. */
|
||
url?: string;
|
||
}
|
||
|
||
export async function securityAudit(options: SecurityAuditOptions): Promise<SecurityAuditReport> {
|
||
const headers = options.url
|
||
? await fetchLiveHeaders(options.url)
|
||
: await securityHeaders(resolve(options.appRoot ?? "."));
|
||
// same checks[] logic runs against either header source
|
||
...
|
||
}
|
||
|
||
async function fetchLiveHeaders(url: string): Promise<Record<string, string>> {
|
||
const res = await fetch(url, { method: "HEAD", redirect: "manual" });
|
||
return Object.fromEntries(res.headers.entries());
|
||
}
|
||
```
|
||
|
||
CLI surface: `wrnexus security audit --url=https://workroot.in` — same `SecurityAuditCheck[]` table
|
||
output as the local mode, so it's a drop-in mental model for anyone who's already used the local
|
||
version.
|
||
|
||
**Tests:** `packages/cli/test/security-command.test.ts` — mock `fetch`, assert the same check IDs run
|
||
against a header map built from a fake `Response`.
|
||
|
||
**Docs:** update `SECURITY-ASVS-5.md`'s intro line ("Run `bun run security:asvs` ... `wrnexus
|
||
security audit`") to mention the `--url` mode explicitly.
|
||
|
||
---
|
||
|
||
## Phase 2 — v0.9: Distributed rate limiting
|
||
|
||
### 2.1 `@wrnexus/ratelimit-redis`
|
||
|
||
**Problem:** `packages/core/src/ratelimit.ts` is honestly documented as process-local by default,
|
||
with a clean `RateLimitStore` interface for swapping in a shared store — but no first-party
|
||
implementation ships. Every team running more than one instance has to write their own Redis
|
||
`INCR`/`PEXPIRE` bucket store before rate limiting actually works in production.
|
||
|
||
**New package layout** (mirrors `packages/captcha`'s `stores/redis.ts` pattern, which already
|
||
exists for CAPTCHA — this is literally copying a pattern you've already built once):
|
||
|
||
```
|
||
packages/ratelimit-redis/
|
||
package.json
|
||
src/
|
||
index.ts # createRedisRateLimitStore()
|
||
client.ts # thin ioredis/bun-redis wrapper, injectable client
|
||
test/
|
||
store.test.ts # against a real or mocked Redis
|
||
README.md
|
||
```
|
||
|
||
**API:**
|
||
|
||
```ts
|
||
import { createRedisRateLimitStore } from "@wrnexus/ratelimit-redis";
|
||
import { rateLimit } from "@wrnexus/core";
|
||
|
||
const store = createRedisRateLimitStore({ url: process.env.REDIS_URL! });
|
||
app.use(rateLimit({ store, max: 100, windowMs: 60_000 }));
|
||
```
|
||
|
||
Implementation: one atomic Lua script (`INCR` + conditional `PEXPIRE`) to avoid a race between the
|
||
increment and the expiry set — same correctness bar as the in-memory store's atomicity within a
|
||
single process.
|
||
|
||
**package.json:**
|
||
|
||
```json
|
||
{
|
||
"name": "@wrnexus/ratelimit-redis",
|
||
"version": "0.9.0",
|
||
"type": "module",
|
||
"main": "./src/index.ts",
|
||
"exports": { ".": "./src/index.ts" },
|
||
"dependencies": { "@wrnexus/core": "workspace:*" }
|
||
}
|
||
```
|
||
|
||
**Tests:** `store.test.ts` covering window rollover, concurrent-hit correctness (fire N parallel
|
||
`hit()` calls, assert exact count), and store failure fallback behavior (Redis down → fail open with
|
||
a warning log, documented explicitly so nobody is surprised).
|
||
|
||
**Docs:** add a row to `SECURITY-SUPPORT-MATRIX.md` under a new "Rate limiting" area, and link it from
|
||
the `RateLimitStore` doc comment in `packages/core/src/ratelimit.ts`.
|
||
|
||
---
|
||
|
||
## Phase 3 — v0.10: Product-critical DX packages
|
||
|
||
### 3.1 `@wrnexus/mail`
|
||
|
||
**Problem:** there's no first-party way to actually send email. The queue example
|
||
(`app/queues/welcome-email.ts`) shows _scheduling_ an email job but nothing implements delivery.
|
||
|
||
**Package layout:**
|
||
|
||
```
|
||
packages/mail/
|
||
package.json
|
||
src/
|
||
index.ts
|
||
send.ts # sendMail(), core envelope type
|
||
providers/
|
||
resend.ts
|
||
ses.ts
|
||
postmark.ts
|
||
smtp.ts
|
||
dev-inbox.ts # captures mail in dev instead of sending; wrnexus dev shows it in DevToolbar
|
||
components/ # optional .wrn email-template partials, reusing the compiler
|
||
test/
|
||
send.test.ts
|
||
dev-inbox.test.ts
|
||
README.md
|
||
SECURITY.md
|
||
```
|
||
|
||
**API (mirrors the `SafeUrlPolicy`/provider-adapter shape from `@wrnexus/captcha`'s providers):**
|
||
|
||
```ts
|
||
export interface MailProvider {
|
||
send(message: MailMessage): Promise<MailResult>;
|
||
}
|
||
|
||
export interface MailMessage {
|
||
to: string | string[];
|
||
from: string;
|
||
subject: string;
|
||
html?: string;
|
||
text?: string;
|
||
replyTo?: string;
|
||
headers?: Record<string, string>;
|
||
}
|
||
|
||
export function createMailer(provider: MailProvider): { send(m: MailMessage): Promise<MailResult> };
|
||
|
||
// providers/resend.ts
|
||
export function resendProvider(opts: { apiKey: string }): MailProvider;
|
||
```
|
||
|
||
**Dev-mode behavior:** when `mode !== "production"`, `createMailer` wraps any provider with
|
||
`devInboxProvider()`, which stores messages in memory and surfaces them in the DevToolbar (new panel,
|
||
same pattern as the existing SQL/queue/realtime DevToolbar providers listed in
|
||
`SECURITY-PERFORMANCE-0.7.md` §18) instead of actually sending — this is the single highest-value DX
|
||
win in this package, since it removes the "did my email actually work" debugging loop entirely.
|
||
|
||
**Security notes for `SECURITY-SUPPORT-MATRIX.md`:** framework guarantees provider-secret handling
|
||
stays server-side and never serializes into hydration payloads (reuse the existing serialization
|
||
redaction from `@wrnexus/security/serialization.ts`); app/operator responsibility covers SPF/DKIM/DMARC
|
||
DNS records and provider account reputation.
|
||
|
||
**Effort:** ~2–3 weeks for `resend` + `smtp` providers, dev inbox, and DevToolbar panel; `ses` and
|
||
`postmark` can follow as a fast-follow since they share the same `MailProvider` interface.
|
||
|
||
### 3.2 `@wrnexus/flags`
|
||
|
||
**Problem:** no feature-flag primitive. Every team building past MVP eventually needs gradual
|
||
rollout, and right now they'd bolt on a third-party SDK with no integration into WRNexusJS's
|
||
`.wrn` reactivity or SSR model.
|
||
|
||
**API:**
|
||
|
||
```ts
|
||
export interface FlagsStore {
|
||
get(key: string, ctx: FlagContext): Promise<boolean | string | number>;
|
||
}
|
||
|
||
export function createFlags(store: FlagsStore): {
|
||
isEnabled(key: string, ctx: FlagContext): Promise<boolean>;
|
||
variant(key: string, ctx: FlagContext): Promise<string | undefined>;
|
||
};
|
||
|
||
// stores/memory.ts, stores/db.ts (reuses @wrnexus/db), stores/percentage.ts (deterministic hash rollout)
|
||
```
|
||
|
||
`.wrn` integration: expose `load server { const enabled = await flags.isEnabled("new-dashboard",
|
||
ctx) }` so flags flow into `props`/`state` the same way `load server` results already do — no new
|
||
compiler syntax needed, just a helper package.
|
||
|
||
**Effort:** ~1–2 weeks; the DB-backed store reuses `@wrnexus/db` migration patterns already in the
|
||
repo, so most of the work is the percentage-rollout hashing and the DevToolbar panel.
|
||
|
||
---
|
||
|
||
## Phase 4 — v0.11: Search, paired with `@wrnexus/ai`
|
||
|
||
### 4.1 `@wrnexus/search`
|
||
|
||
**Problem:** no first-party search story, despite already having `@wrnexus/ai` for embeddings/LLM
|
||
calls — search and RAG are the two things most SaaS apps need `@wrnexus/ai` _for_, so this is the
|
||
natural pairing package.
|
||
|
||
**Package layout:**
|
||
|
||
```
|
||
packages/search/
|
||
src/
|
||
index.ts
|
||
adapters/
|
||
postgres-fts.ts # tsvector + GIN index helpers, generated migration
|
||
sqlite-fts5.ts
|
||
pgvector.ts # embeddings via @wrnexus/ai, cosine-distance query helper
|
||
typesense.ts # optional hosted adapter
|
||
test/
|
||
```
|
||
|
||
**API:**
|
||
|
||
```ts
|
||
export function createSearchIndex(adapter: SearchAdapter, table: string, columns: string[]);
|
||
const results = await search.query("customer onboarding", { limit: 10 });
|
||
|
||
// pairs directly with @wrnexus/ai
|
||
import { embed } from "@wrnexus/ai";
|
||
const vector = await embed(text);
|
||
await search.upsertVector(id, vector);
|
||
```
|
||
|
||
**Effort:** ~3–4 weeks — Postgres FTS and SQLite FTS5 adapters first (no new infra dependency,
|
||
reuses `@wrnexus/db`'s existing driver abstraction), pgvector and Typesense as fast-follows.
|
||
|
||
---
|
||
|
||
## Phase 5 — v0.12: Enterprise & government readiness
|
||
|
||
This phase is prioritized specifically because `workroot.in` markets to "enterprises & governments" —
|
||
these two items are the actual procurement blockers for that buyer, more than any generic framework
|
||
feature would be.
|
||
|
||
### 5.1 SAML support in `@wrnexus/auth`
|
||
|
||
**Problem:** OAuth/OIDC is implemented (per `SECURITY-SUPPORT-MATRIX.md`), but large-enterprise and
|
||
government IT departments frequently mandate SAML 2.0 specifically for SSO procurement, regardless of
|
||
OIDC's technical merits.
|
||
|
||
**Location:** `packages/auth/src/saml/` — new subdirectory alongside the existing OAuth/OIDC code,
|
||
same `AuthProvider` interface shape so it plugs into the existing account/session engine
|
||
(`packages/auth/src/engine.ts`) without a parallel auth system.
|
||
|
||
**Scope:** SP-initiated SSO, signed assertion validation, configurable IdP metadata (Okta, Azure AD,
|
||
Google Workspace, ADFS as the four IdPs to certify against first — that covers the large majority of
|
||
enterprise/government IT estates).
|
||
|
||
**Security notes:** reuse the existing `AUTH_SECURITY_EVENT_TYPES` vocabulary for SAML-specific
|
||
events (assertion replay attempt, signature validation failure, clock-skew rejection) so they flow
|
||
into the same audit pipeline as every other auth event — no parallel logging system.
|
||
|
||
**Effort:** ~4–5 weeks; SAML assertion validation is fiddly (XML canonicalization, signature
|
||
wrapping attacks) and deserves a dedicated security review pass before release, not just unit tests.
|
||
|
||
### 5.2 `@wrnexus/compliance`
|
||
|
||
**Problem:** `packages/authz/src/audit.ts` already has a well-built `AuthzAuditSink` interface with
|
||
log-injection-safe formatting (`logSafe()`) and memory/console sinks — but no durable, exportable
|
||
store. For SOC 2-style evidence or India's DPDP Act data-processing records, teams need retained,
|
||
queryable, exportable audit trails, not console lines.
|
||
|
||
**Package layout:**
|
||
|
||
```
|
||
packages/compliance/
|
||
src/
|
||
index.ts
|
||
sinks/
|
||
db.ts # durable AuthzAuditSink + auth security-event sink, reusing @wrnexus/db
|
||
export.ts # CSV/JSON export with retention-window filtering
|
||
retention.ts # configurable retention policy + scheduled purge
|
||
test/
|
||
```
|
||
|
||
**API:**
|
||
|
||
```ts
|
||
import { dbAuditSink } from "@wrnexus/compliance";
|
||
authz.configure({ auditSink: dbAuditSink({ retentionDays: 365 }) });
|
||
|
||
const report = await compliance.exportAuditTrail({ from, to, format: "csv" });
|
||
```
|
||
|
||
This is the package I'd actually build _first_ internally for the WRNexus SaaS itself, since any
|
||
enterprise/government customer of WRNexus will ask WorkRoot for exactly this evidence during their
|
||
own procurement review — dogfooding it validates the design before it ships to other developers.
|
||
|
||
**Effort:** ~2 weeks on top of the existing audit-sink groundwork, since most of the hard part
|
||
(safe event formatting, sink interface) is already done.
|
||
|
||
---
|
||
|
||
## Phase 6 — v1.0: Monetization and ecosystem
|
||
|
||
### 6.1 `@wrnexus/billing`
|
||
|
||
**Problem:** no payments package, despite WRNexus itself being a billed SaaS product — this is the
|
||
package where dogfooding value is highest.
|
||
|
||
**API:**
|
||
|
||
```ts
|
||
export interface BillingProvider {
|
||
createCheckoutSession(params): Promise<{ url: string }>;
|
||
verifyWebhook(req: Request): Promise<BillingEvent>;
|
||
getSubscription(customerId: string): Promise<Subscription>;
|
||
}
|
||
|
||
// providers/stripe.ts, providers/razorpay.ts (India-relevant)
|
||
```
|
||
|
||
Webhook verification reuses `@wrnexus/security`'s constant-time comparison helpers (same primitive
|
||
already used in `packages/core/src/csrf.ts`'s `timingSafeEqual`) for signature checks. Usage metering
|
||
hooks into `@wrnexus/observability`'s existing counters/gauges rather than a new metrics system.
|
||
|
||
**Effort:** ~5–6 weeks for Stripe + Razorpay, subscription lifecycle, and webhook handling with
|
||
proper idempotency-key handling (a real source of billing bugs if skipped).
|
||
|
||
### 6.2 Public release strategy — open-core
|
||
|
||
**Problem:** `@wrnexus/*` is currently private, so nobody outside WorkRoot can `bun install` any of
|
||
it. This is the actual ceiling on "developer power," not any single missing feature.
|
||
|
||
**Recommended split:**
|
||
|
||
| Tier | Packages | License |
|
||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
|
||
| Open (public npm) | `core`, `ssr`, `compiler`, `syntax`, `router`, `store`, `reactive`, `security`, `ui`, `cli`, `dev-server`, `dev-toolbar` | MIT or Apache-2.0 |
|
||
| Paid/enterprise | `billing`, `compliance`, SAML in `auth`, `authz` advanced policy engine | Commercial license, distributed via the private registry you already run |
|
||
|
||
**Rollout steps:**
|
||
|
||
1. Audit the `open` tier packages for any WorkRoot-specific secrets/config baked in (`scripts/`
|
||
already has `generate-sbom.mjs` and `check-public-api.mjs` — extend `check-public-api.mjs` to also
|
||
flag internal-only references before a package is promoted to the public tier).
|
||
2. Publish under the `@wrnexus` npm org with the existing `PUBLISHING.md` process, starting with
|
||
`core` + `cli` (the minimum to `bunx @wrnexus/cli create my-app` publicly).
|
||
3. Public GitHub repo for the open tier only (mirrored from the monorepo via the existing
|
||
`stage:packages` script's publish pipeline, not a manual copy).
|
||
4. Keep `docs/ROADMAP.md`-style public roadmap visible so early external adopters see what's coming.
|
||
|
||
**Effort:** ~4–6 weeks of packaging/licensing/CI work, separate from any new feature work above — this
|
||
can run in parallel with Phase 5.
|
||
|
||
### 6.3 Lean into `@wrnexus/ai` + `@wrnexus/mcp`
|
||
|
||
**Problem:** these packages already exist and are ahead of most frameworks, but aren't positioned as
|
||
a headline feature anywhere in the marketing (`wrnexusjs.dev` homepage doesn't mention AI/MCP at all
|
||
per the live screenshot taken earlier).
|
||
|
||
**Recommendation:** ship an official MCP server (`packages/mcp/src/index.ts` already has a `stdio.ts`
|
||
transport — check whether it currently exposes framework introspection, e.g. route listing, `.wrn`
|
||
component schema, or `wrnexus doctor` output as MCP tools) so agentic coding tools (Claude Code,
|
||
Cursor, etc.) can scaffold and modify WRNexusJS apps with structured tool calls instead of guessing at
|
||
the `.wrn` syntax from grepped examples. This is a low-cost, high-differentiation move given how much
|
||
of the ecosystem is moving toward agent-built apps.
|
||
|
||
**Effort:** ~2–3 weeks to wrap existing CLI commands (`inspect`, `doctor`, `routes`, `generate`) as
|
||
MCP tools, since the underlying logic already exists in `packages/cli/src`.
|
||
|
||
---
|
||
|
||
## Cross-cutting requirements for every phase
|
||
|
||
- Every new/changed package needs a `SECURITY-SUPPORT-MATRIX.md` row (framework guarantee vs.
|
||
app/operator responsibility) before release, matching the existing table format.
|
||
- Every new package needs an ASVS evidence row in `SECURITY-ASVS-5.md` if it touches auth, secrets,
|
||
network requests, or user input — same two-column (implementation evidence / verification evidence)
|
||
format already used.
|
||
- `bun run validate:0.X` release gate (`scripts/validate-0.8.mjs`-style) should get a new
|
||
`validate-0.9.mjs` etc. per phase, following the existing per-minor-version validation script
|
||
pattern rather than one growing script.
|
||
- `CHANGELOG.md` entries per release, same format as the 0.8.3/0.8.0 entries already there.
|
||
- Each phase should ship an example page/route in `examples/basic-app` (mirroring
|
||
`auth-showcase`, `captcha-showcase`, `i18n-showcase`) — e.g. `examples/mail-showcase`,
|
||
`examples/billing-showcase` — so the roadmap's own "executable examples" discipline
|
||
(`ROADMAP-COMPLETION-REPORT.md`) continues.
|
||
|
||
---
|
||
|
||
## Suggested sequencing rationale
|
||
|
||
Phase 1 ships first because it's the only phase with zero new surface area — pure default fixes that
|
||
improve every existing deployment (including `workroot.in` and `wrnexusjs.dev` themselves) without
|
||
anyone changing their code. Phases 2–4 build developer-facing power in the order teams actually hit
|
||
the wall (rate limiting under real traffic → needing email → needing flags → needing search once an
|
||
app has enough data to search). Phase 5 is prioritized ahead of Phase 6 despite being harder, because
|
||
it directly unblocks revenue-relevant deals in WorkRoot's actual pipeline (enterprise/government
|
||
procurement). Phase 6's public release is last on purpose — it should launch once there's a stronger
|
||
package lineup behind it, so the first public impression of `@wrnexus/*` is "batteries-included," not
|
||
"promising but thin."
|