xcoder-dna 0.0.0-stage → 1.3.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.
- package/LICENSE +66 -0
- package/README.md +87 -2
- package/dist/agents/ai-chat-debate.d.ts +36 -0
- package/dist/agents/ai-chat-debate.js +300 -0
- package/dist/agents/ai-chat-report-writer.d.ts +7 -0
- package/dist/agents/ai-chat-report-writer.js +100 -0
- package/dist/agents/duplicate-detector.d.ts +41 -0
- package/dist/agents/duplicate-detector.js +65 -0
- package/dist/agents/genesis-agent.d.ts +61 -0
- package/dist/agents/genesis-agent.js +340 -0
- package/dist/agents/samurai-agent.d.ts +53 -0
- package/dist/agents/samurai-agent.js +279 -0
- package/dist/agents/sub-agent.d.ts +56 -0
- package/dist/agents/sub-agent.js +353 -0
- package/dist/backlog/backlog-requirement-validator.d.ts +10 -0
- package/dist/backlog/backlog-requirement-validator.js +60 -0
- package/dist/backlog/backlog-store.d.ts +29 -0
- package/dist/backlog/backlog-store.js +148 -0
- package/dist/backlog/po-agent.d.ts +16 -0
- package/dist/backlog/po-agent.js +121 -0
- package/dist/backlog/types.d.ts +25 -0
- package/dist/backlog/types.js +4 -0
- package/dist/bench/run.d.ts +11 -0
- package/dist/bench/run.js +185 -0
- package/dist/bench/types.d.ts +50 -0
- package/dist/bench/types.js +1 -0
- package/dist/cli/swarm.d.ts +10 -0
- package/dist/cli/swarm.js +100 -0
- package/dist/cli/task.d.ts +14 -0
- package/dist/cli/task.js +121 -0
- package/dist/cli/wbc-arg.d.ts +6 -0
- package/dist/cli/wbc-arg.js +40 -0
- package/dist/cli/wbc.d.ts +45 -0
- package/dist/cli/wbc.js +98 -0
- package/dist/commands/license.d.ts +9 -0
- package/dist/commands/license.js +140 -0
- package/dist/commands/release.d.ts +44 -0
- package/dist/commands/release.js +180 -0
- package/dist/commands/test.d.ts +50 -0
- package/dist/commands/test.js +385 -0
- package/dist/config/loadConfig.d.ts +82 -0
- package/dist/config/loadConfig.js +240 -0
- package/dist/config/secure-mode.d.ts +29 -0
- package/dist/config/secure-mode.js +63 -0
- package/dist/config/user-config.d.ts +39 -0
- package/dist/config/user-config.js +202 -0
- package/dist/engine/reasoning-engine.d.ts +21 -0
- package/dist/engine/reasoning-engine.js +59 -0
- package/dist/events/bus.d.ts +22 -0
- package/dist/events/bus.js +18 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +77 -0
- package/dist/indexing/ignoreRules.d.ts +1 -0
- package/dist/indexing/ignoreRules.js +53 -0
- package/dist/instance-swarm/instance-swarm-orchestrator.d.ts +57 -0
- package/dist/instance-swarm/instance-swarm-orchestrator.js +204 -0
- package/dist/instance-swarm/protocol-name-resolver.d.ts +23 -0
- package/dist/instance-swarm/protocol-name-resolver.js +64 -0
- package/dist/license/gate.d.ts +31 -0
- package/dist/license/gate.js +188 -0
- package/dist/license/http-client.d.ts +43 -0
- package/dist/license/http-client.js +195 -0
- package/dist/license/manager.d.ts +39 -0
- package/dist/license/manager.js +175 -0
- package/dist/license/receipt.d.ts +27 -0
- package/dist/license/receipt.js +108 -0
- package/dist/license/state-codec.d.ts +72 -0
- package/dist/license/state-codec.js +218 -0
- package/dist/license/state-paths.d.ts +17 -0
- package/dist/license/state-paths.js +64 -0
- package/dist/license/store.d.ts +44 -0
- package/dist/license/store.js +223 -0
- package/dist/license/trust-anchor.d.ts +24 -0
- package/dist/license/trust-anchor.js +74 -0
- package/dist/license/types.d.ts +34 -0
- package/dist/license/types.js +19 -0
- package/dist/llm/budget.d.ts +23 -0
- package/dist/llm/budget.js +58 -0
- package/dist/llm/client.d.ts +53 -0
- package/dist/llm/client.js +122 -0
- package/dist/llm/factory.d.ts +14 -0
- package/dist/llm/factory.js +32 -0
- package/dist/orchestrator/defect-logger.d.ts +31 -0
- package/dist/orchestrator/defect-logger.js +101 -0
- package/dist/orchestrator/defect-manager.d.ts +118 -0
- package/dist/orchestrator/defect-manager.js +338 -0
- package/dist/orchestrator/defect-migration.d.ts +14 -0
- package/dist/orchestrator/defect-migration.js +174 -0
- package/dist/orchestrator/defect-store.d.ts +48 -0
- package/dist/orchestrator/defect-store.js +76 -0
- package/dist/orchestrator/healing.d.ts +14 -0
- package/dist/orchestrator/healing.js +11 -0
- package/dist/orchestrator/interactive-steering.d.ts +64 -0
- package/dist/orchestrator/interactive-steering.js +107 -0
- package/dist/orchestrator/interrupt-handler.d.ts +30 -0
- package/dist/orchestrator/interrupt-handler.js +70 -0
- package/dist/orchestrator/lesson-manager.d.ts +25 -0
- package/dist/orchestrator/lesson-manager.js +122 -0
- package/dist/orchestrator/progress-summary.d.ts +15 -0
- package/dist/orchestrator/progress-summary.js +106 -0
- package/dist/orchestrator/reasoning-score.d.ts +49 -0
- package/dist/orchestrator/reasoning-score.js +117 -0
- package/dist/orchestrator/swarm.d.ts +36 -0
- package/dist/orchestrator/swarm.js +77 -0
- package/dist/orchestrator/task-factory.d.ts +15 -0
- package/dist/orchestrator/task-factory.js +176 -0
- package/dist/orchestrator/validation-gate.d.ts +23 -0
- package/dist/orchestrator/validation-gate.js +155 -0
- package/dist/protocol/react-contract.d.ts +136 -0
- package/dist/protocol/react-contract.js +698 -0
- package/dist/protocol/wks-protocol-manager.d.ts +22 -0
- package/dist/protocol/wks-protocol-manager.js +72 -0
- package/dist/sdlc-runtime/conversation-runner.d.ts +8 -0
- package/dist/sdlc-runtime/conversation-runner.js +64 -0
- package/dist/sdlc-runtime/dag-utils.d.ts +15 -0
- package/dist/sdlc-runtime/dag-utils.js +67 -0
- package/dist/sdlc-runtime/defect-integration.d.ts +7 -0
- package/dist/sdlc-runtime/defect-integration.js +99 -0
- package/dist/sdlc-runtime/execution-loop.d.ts +38 -0
- package/dist/sdlc-runtime/execution-loop.js +274 -0
- package/dist/sdlc-runtime/lesson-recorder.d.ts +9 -0
- package/dist/sdlc-runtime/lesson-recorder.js +27 -0
- package/dist/sdlc-runtime/plan-initializer.d.ts +47 -0
- package/dist/sdlc-runtime/plan-initializer.js +135 -0
- package/dist/sdlc-runtime/progress-persister.d.ts +10 -0
- package/dist/sdlc-runtime/progress-persister.js +34 -0
- package/dist/sdlc-runtime/protocol-loader.d.ts +10 -0
- package/dist/sdlc-runtime/protocol-loader.js +21 -0
- package/dist/sdlc-runtime/reporting.d.ts +24 -0
- package/dist/sdlc-runtime/reporting.js +68 -0
- package/dist/sdlc-runtime/sdlc-orchestrator.d.ts +59 -0
- package/dist/sdlc-runtime/sdlc-orchestrator.js +170 -0
- package/dist/sdlc-runtime/sdlc-skill.d.ts +44 -0
- package/dist/sdlc-runtime/sdlc-skill.js +148 -0
- package/dist/sdlc-runtime/steering-handler.d.ts +14 -0
- package/dist/sdlc-runtime/steering-handler.js +51 -0
- package/dist/sdlc-runtime/trace-recorder.d.ts +10 -0
- package/dist/sdlc-runtime/trace-recorder.js +7 -0
- package/dist/security/index.d.ts +16 -0
- package/dist/security/index.js +12 -0
- package/dist/security/input-screen.d.ts +30 -0
- package/dist/security/input-screen.js +75 -0
- package/dist/security/tool-call-guard.d.ts +16 -0
- package/dist/security/tool-call-guard.js +163 -0
- package/dist/security/untrusted-content.d.ts +34 -0
- package/dist/security/untrusted-content.js +63 -0
- package/dist/skills/advanced-dfir/SKILL.md +85 -0
- package/dist/skills/advanced-pentest-lab/SKILL.md +88 -0
- package/dist/skills/analyst/SKILL.md +55 -0
- package/dist/skills/architect/SKILL.md +54 -0
- package/dist/skills/aws/SKILL.md +92 -0
- package/dist/skills/azure/SKILL.md +90 -0
- package/dist/skills/bylaw-context.d.ts +45 -0
- package/dist/skills/bylaw-context.js +61 -0
- package/dist/skills/bylaw-review.d.ts +20 -0
- package/dist/skills/bylaw-review.js +101 -0
- package/dist/skills/conversation/SKILL.md +29 -0
- package/dist/skills/design-patterns/SKILL.md +190 -0
- package/dist/skills/devops/SKILL.md +55 -0
- package/dist/skills/directives.d.ts +20 -0
- package/dist/skills/directives.js +313 -0
- package/dist/skills/docker/SKILL.md +76 -0
- package/dist/skills/docker-expert/SKILL.md +56 -0
- package/dist/skills/enterprise-systems-analyst/SKILL.md +83 -0
- package/dist/skills/enterprise-systems-analyst/references/crm.md +41 -0
- package/dist/skills/enterprise-systems-analyst/references/erp.md +40 -0
- package/dist/skills/enterprise-systems-analyst/references/financial-systems.md +35 -0
- package/dist/skills/enterprise-systems-analyst/references/hcm.md +38 -0
- package/dist/skills/enterprise-systems-analyst/references/mrp.md +37 -0
- package/dist/skills/enterprise-systems-analyst/references/scm.md +32 -0
- package/dist/skills/ethical-hacking-methodology/SKILL.md +119 -0
- package/dist/skills/externalSkillLoader.d.ts +25 -0
- package/dist/skills/externalSkillLoader.js +65 -0
- package/dist/skills/externalSkillRegistry.d.ts +33 -0
- package/dist/skills/externalSkillRegistry.js +94 -0
- package/dist/skills/filesystem-management/SKILL.md +61 -0
- package/dist/skills/first-run-seed.d.ts +26 -0
- package/dist/skills/first-run-seed.js +75 -0
- package/dist/skills/git-vcs/SKILL.md +84 -0
- package/dist/skills/kafka/SKILL.md +89 -0
- package/dist/skills/kubernetes/SKILL.md +93 -0
- package/dist/skills/kubernetes-expert/SKILL.md +54 -0
- package/dist/skills/openshift/SKILL.md +93 -0
- package/dist/skills/pentester/SKILL.md +59 -0
- package/dist/skills/performance-tester/SKILL.md +57 -0
- package/dist/skills/playwright-ui-tester/SKILL.md +67 -0
- package/dist/skills/programmer/SKILL.md +58 -0
- package/dist/skills/project-scaffolding/SKILL.md +133 -0
- package/dist/skills/qa-engineer/SKILL.md +91 -0
- package/dist/skills/rca/SKILL.md +58 -0
- package/dist/skills/redhat/SKILL.md +92 -0
- package/dist/skills/rosa/SKILL.md +80 -0
- package/dist/skills/scrum-framework/SKILL.md +74 -0
- package/dist/skills/scrum-framework/references/artifacts_and_quality.md +79 -0
- package/dist/skills/scrum-framework/references/roles_and_events.md +105 -0
- package/dist/skills/scrum-master-agent/SKILL.md +74 -0
- package/dist/skills/scrum-master-agent/references/behaviors.md +56 -0
- package/dist/skills/sdlc-skill.d.ts +1 -0
- package/dist/skills/sdlc-skill.js +4 -0
- package/dist/skills/secops/SKILL.md +52 -0
- package/dist/skills/secure-architect.bylaws.d.ts +68 -0
- package/dist/skills/secure-architect.bylaws.js +525 -0
- package/dist/skills/seed-skills.d.ts +48 -0
- package/dist/skills/seed-skills.js +213 -0
- package/dist/skills/skill-authoring/SKILL.md +123 -0
- package/dist/skills/skill-header.d.ts +51 -0
- package/dist/skills/skill-header.js +163 -0
- package/dist/skills/skill-path-guard.d.ts +41 -0
- package/dist/skills/skill-path-guard.js +95 -0
- package/dist/skills/skillRouter.d.ts +51 -0
- package/dist/skills/skillRouter.js +90 -0
- package/dist/skills/skills-root.d.ts +64 -0
- package/dist/skills/skills-root.js +168 -0
- package/dist/skills/software-architect/SKILL.md +77 -0
- package/dist/skills/software-engineer/SKILL.md +74 -0
- package/dist/skills/solid-principles/SKILL.md +94 -0
- package/dist/skills/task-planning/SKILL.md +115 -0
- package/dist/skills/tester/SKILL.md +54 -0
- package/dist/skills/ubuntu/SKILL.md +87 -0
- package/dist/skills/ui-ux-design/SKILL.md +114 -0
- package/dist/skills/workspace-context/SKILL.md +103 -0
- package/dist/sprint/controller.d.ts +37 -0
- package/dist/sprint/controller.js +220 -0
- package/dist/sprint/dag-builder.d.ts +14 -0
- package/dist/sprint/dag-builder.js +105 -0
- package/dist/sprint/reporter.d.ts +11 -0
- package/dist/sprint/reporter.js +97 -0
- package/dist/sprint/state-store.d.ts +10 -0
- package/dist/sprint/state-store.js +71 -0
- package/dist/sprint/types.d.ts +24 -0
- package/dist/sprint/types.js +4 -0
- package/dist/store/file-version-store.d.ts +24 -0
- package/dist/store/file-version-store.js +73 -0
- package/dist/store/task-store.d.ts +90 -0
- package/dist/store/task-store.js +166 -0
- package/dist/task-ingest/architect-detailer.d.ts +43 -0
- package/dist/task-ingest/architect-detailer.js +141 -0
- package/dist/task-ingest/detail-schema.d.ts +9 -0
- package/dist/task-ingest/detail-schema.js +195 -0
- package/dist/task-ingest/detailed-run.d.ts +36 -0
- package/dist/task-ingest/detailed-run.js +108 -0
- package/dist/task-ingest/extract-id.d.ts +9 -0
- package/dist/task-ingest/extract-id.js +24 -0
- package/dist/task-ingest/index.d.ts +23 -0
- package/dist/task-ingest/index.js +14 -0
- package/dist/task-ingest/ingest.d.ts +45 -0
- package/dist/task-ingest/ingest.js +163 -0
- package/dist/task-ingest/list-ingested.d.ts +13 -0
- package/dist/task-ingest/list-ingested.js +30 -0
- package/dist/task-ingest/pending-filter.d.ts +30 -0
- package/dist/task-ingest/pending-filter.js +59 -0
- package/dist/task-ingest/scanner.d.ts +56 -0
- package/dist/task-ingest/scanner.js +216 -0
- package/dist/task-ingest/source-reader.d.ts +25 -0
- package/dist/task-ingest/source-reader.js +93 -0
- package/dist/task-ingest/spec-builder.d.ts +34 -0
- package/dist/task-ingest/spec-builder.js +89 -0
- package/dist/task-ingest/wbs-extract.d.ts +50 -0
- package/dist/task-ingest/wbs-extract.js +226 -0
- package/dist/task-ingest/wbs-parser.d.ts +36 -0
- package/dist/task-ingest/wbs-parser.js +170 -0
- package/dist/tools/apiTestTool.d.ts +37 -0
- package/dist/tools/apiTestTool.js +128 -0
- package/dist/tools/ast-tools.d.ts +183 -0
- package/dist/tools/ast-tools.js +853 -0
- package/dist/tools/browse-tool.d.ts +105 -0
- package/dist/tools/browse-tool.js +394 -0
- package/dist/tools/credential.d.ts +17 -0
- package/dist/tools/credential.js +32 -0
- package/dist/tools/dependency-graph.d.ts +23 -0
- package/dist/tools/dependency-graph.js +164 -0
- package/dist/tools/edit-tool.d.ts +34 -0
- package/dist/tools/edit-tool.js +95 -0
- package/dist/tools/file-header-manager.d.ts +27 -0
- package/dist/tools/file-header-manager.js +153 -0
- package/dist/tools/fs-tools.d.ts +154 -0
- package/dist/tools/fs-tools.js +337 -0
- package/dist/tools/git-tools.d.ts +79 -0
- package/dist/tools/git-tools.js +348 -0
- package/dist/tools/github-tool.d.ts +48 -0
- package/dist/tools/github-tool.js +147 -0
- package/dist/tools/globTool.d.ts +20 -0
- package/dist/tools/globTool.js +34 -0
- package/dist/tools/grepTool.d.ts +19 -0
- package/dist/tools/grepTool.js +44 -0
- package/dist/tools/index-freshness.d.ts +58 -0
- package/dist/tools/index-freshness.js +190 -0
- package/dist/tools/index-glob.d.ts +48 -0
- package/dist/tools/index-glob.js +175 -0
- package/dist/tools/load-skill-tool.d.ts +15 -0
- package/dist/tools/load-skill-tool.js +114 -0
- package/dist/tools/patch-tools.d.ts +58 -0
- package/dist/tools/patch-tools.js +153 -0
- package/dist/tools/playwright-tool.d.ts +66 -0
- package/dist/tools/playwright-tool.js +121 -0
- package/dist/tools/project-detect.d.ts +20 -0
- package/dist/tools/project-detect.js +87 -0
- package/dist/tools/prompt-guidance.d.ts +22 -0
- package/dist/tools/prompt-guidance.js +23 -0
- package/dist/tools/scheduleTool.d.ts +19 -0
- package/dist/tools/scheduleTool.js +181 -0
- package/dist/tools/search-tools.d.ts +27 -0
- package/dist/tools/search-tools.js +78 -0
- package/dist/tools/sed-tool.d.ts +66 -0
- package/dist/tools/sed-tool.js +402 -0
- package/dist/tools/shell-result.d.ts +11 -0
- package/dist/tools/shell-result.js +1 -0
- package/dist/tools/shell-tool.d.ts +45 -0
- package/dist/tools/shell-tool.js +141 -0
- package/dist/tools/siteCrawlerTool.d.ts +32 -0
- package/dist/tools/siteCrawlerTool.js +205 -0
- package/dist/tools/tool-registry.d.ts +67 -0
- package/dist/tools/tool-registry.js +596 -0
- package/dist/tools/validate-tool.d.ts +23 -0
- package/dist/tools/validate-tool.js +201 -0
- package/dist/tools/workspace-index.d.ts +46 -0
- package/dist/tools/workspace-index.js +173 -0
- package/dist/types.d.ts +222 -0
- package/dist/types.js +7 -0
- package/dist/updater/index.d.ts +14 -0
- package/dist/updater/index.js +120 -0
- package/dist/util/banner.d.ts +25 -0
- package/dist/util/banner.js +140 -0
- package/dist/util/command-suggester.d.ts +6 -0
- package/dist/util/command-suggester.js +25 -0
- package/dist/util/confirm-interrupt.d.ts +10 -0
- package/dist/util/confirm-interrupt.js +31 -0
- package/dist/util/continue-trigger.d.ts +11 -0
- package/dist/util/continue-trigger.js +20 -0
- package/dist/util/env-info.d.ts +18 -0
- package/dist/util/env-info.js +37 -0
- package/dist/util/file-access-log.d.ts +35 -0
- package/dist/util/file-access-log.js +140 -0
- package/dist/util/footer-manager.d.ts +124 -0
- package/dist/util/footer-manager.js +195 -0
- package/dist/util/footer-renderer.d.ts +67 -0
- package/dist/util/footer-renderer.js +319 -0
- package/dist/util/footer-state.d.ts +34 -0
- package/dist/util/footer-state.js +18 -0
- package/dist/util/footer-text.d.ts +51 -0
- package/dist/util/footer-text.js +74 -0
- package/dist/util/ids.d.ts +1 -0
- package/dist/util/ids.js +4 -0
- package/dist/util/levenshtein.d.ts +6 -0
- package/dist/util/levenshtein.js +34 -0
- package/dist/util/logger.d.ts +10 -0
- package/dist/util/logger.js +41 -0
- package/dist/util/logs.d.ts +26 -0
- package/dist/util/logs.js +88 -0
- package/dist/util/output-handler.d.ts +89 -0
- package/dist/util/output-handler.js +193 -0
- package/dist/util/output-interceptor.d.ts +18 -0
- package/dist/util/output-interceptor.js +49 -0
- package/dist/util/output-paths.d.ts +43 -0
- package/dist/util/output-paths.js +121 -0
- package/dist/util/package-json.d.ts +6 -0
- package/dist/util/package-json.js +36 -0
- package/dist/util/process-kill.d.ts +27 -0
- package/dist/util/process-kill.js +148 -0
- package/dist/util/protocol-error-logger.d.ts +33 -0
- package/dist/util/protocol-error-logger.js +73 -0
- package/dist/util/redact.d.ts +25 -0
- package/dist/util/redact.js +103 -0
- package/dist/util/scroll-region-guard.d.ts +33 -0
- package/dist/util/scroll-region-guard.js +63 -0
- package/dist/util/spinner.d.ts +15 -0
- package/dist/util/spinner.js +49 -0
- package/dist/util/terminal-output.d.ts +34 -0
- package/dist/util/terminal-output.js +1 -0
- package/dist/util/terminal-silent.d.ts +6 -0
- package/dist/util/terminal-silent.js +20 -0
- package/dist/util/terminal.d.ts +56 -0
- package/dist/util/terminal.js +124 -0
- package/dist/util/text-sanitize.d.ts +8 -0
- package/dist/util/text-sanitize.js +45 -0
- package/dist/util/thinking-spinner.d.ts +27 -0
- package/dist/util/thinking-spinner.js +189 -0
- package/dist/util/tool-error-logger.d.ts +33 -0
- package/dist/util/tool-error-logger.js +77 -0
- package/dist/util/tty.d.ts +13 -0
- package/dist/util/tty.js +16 -0
- package/dist/util/xcoder-paths.d.ts +56 -0
- package/dist/util/xcoder-paths.js +63 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/dist/worker-agent/to-subtask-node.d.ts +35 -0
- package/dist/worker-agent/to-subtask-node.js +41 -0
- package/dist/worker-agent/worker-orchestrator.d.ts +104 -0
- package/dist/worker-agent/worker-orchestrator.js +201 -0
- package/package.json +39 -4
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// xcoder:version 4 | xcoder:task task-689517 | xcoder:updated 2026-11-01T09:16:05.783Z | xcoder:subtask code-st-828150
|
|
2
|
+
// The single choke point for licensed features.
|
|
3
|
+
//
|
|
4
|
+
// Rules this module guarantees:
|
|
5
|
+
// * Fail closed. `requireLicense()` never throws and never does network I/O;
|
|
6
|
+
// a missing file, an EACCES, corrupt JSON or a broken trust anchor all
|
|
7
|
+
// resolve to `{ allowed: false }`.
|
|
8
|
+
// * One predicate. `isLicensedFeature()` is the only definition of what is
|
|
9
|
+
// gated, so a new command cannot acquire a licence check by accident.
|
|
10
|
+
// * Offline by default. Refresh is opportunistic and runs only AFTER the
|
|
11
|
+
// gate has passed, so an unreachable server never blocks a paid user.
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { verifyReceipt } from "./receipt.js";
|
|
16
|
+
import { createLicenseStore, resolveCandidateFiles } from "./store.js";
|
|
17
|
+
import { primaryStatePath } from "./state-paths.js";
|
|
18
|
+
import { decodeStateFile, recordFromClaims, writeStateFile } from "./state-codec.js";
|
|
19
|
+
import { createLicenseApi } from "./http-client.js";
|
|
20
|
+
import { createLicenseManager } from "./manager.js";
|
|
21
|
+
import { LICENSE_API_BASE_URL, LICENSE_ISSUER, LICENSE_PRODUCT_ID, TRUST_ANCHOR } from "./trust-anchor.js";
|
|
22
|
+
import { DENIAL_DETAIL } from "./types.js";
|
|
23
|
+
/** Exit code for "this command needs an active license". */
|
|
24
|
+
export const LICENSE_REQUIRED_EXIT_CODE = 4;
|
|
25
|
+
/**
|
|
26
|
+
* The licensed surface: entering the SDLC orchestrator run loop.
|
|
27
|
+
* Everything else (index, status, backlog, sprint status/retro/abort,
|
|
28
|
+
* browse, observe, bench, test, swarm, task, release, license) stays ungated.
|
|
29
|
+
*/
|
|
30
|
+
const LICENSED_FEATURES = new Set(["run", "resume", "sprint-start"]);
|
|
31
|
+
export function isLicensedFeature(commandPath) {
|
|
32
|
+
return LICENSED_FEATURES.has(commandPath.trim().toLowerCase());
|
|
33
|
+
}
|
|
34
|
+
/** True when an activation file is present but the store could not parse it. */
|
|
35
|
+
function activationFileExists(file) {
|
|
36
|
+
try {
|
|
37
|
+
return fs.existsSync(file);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Upgrade a verified pre-unification record into the shared store.
|
|
45
|
+
*
|
|
46
|
+
* This is the adopt-on-read half of the shared-state fix: a user who activated
|
|
47
|
+
* with v3 (or an older v1) keeps their seat, and the very next command in any
|
|
48
|
+
* binary finds the activation at the shared path. Best-effort by contract — a
|
|
49
|
+
* failed write must never block a paid user.
|
|
50
|
+
*/
|
|
51
|
+
function adoptLegacy(claims, receipt, installationId) {
|
|
52
|
+
try {
|
|
53
|
+
// Keep the machine identity the legacy record already carried: a fresh id
|
|
54
|
+
// would make this machine look like a second installation to the server.
|
|
55
|
+
const id = installationId ?? createLicenseStore().readInstallation()?.installationId ?? randomUUID();
|
|
56
|
+
// The verified receipt MUST survive the upgrade: without it the rewritten
|
|
57
|
+
// file decodes as incomplete and the next command asks for a key again —
|
|
58
|
+
// the very bug this adoption exists to fix.
|
|
59
|
+
writeStateFile(primaryStatePath(), recordFromClaims(claims, { installationId: id, receipt }));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Never fatal: the gate already has verified claims to return.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The hint a denial prints. Per-tree, so a v1 user is told v1's own binary
|
|
67
|
+
* name rather than another tree's command.
|
|
68
|
+
*/
|
|
69
|
+
export const ACTIVATE_COMMAND = "xcoder license activate";
|
|
70
|
+
function denialMessage(reason) {
|
|
71
|
+
return `licensed feature unavailable (${reason}: ${DENIAL_DETAIL[reason]}) — run \`${ACTIVATE_COMMAND}\``;
|
|
72
|
+
}
|
|
73
|
+
function isDenialReason(value) {
|
|
74
|
+
return typeof value === "string" && value in DENIAL_DETAIL;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Decide whether a licensed feature may run.
|
|
78
|
+
*
|
|
79
|
+
* Never throws, never touches the network. Returns the verified claims on
|
|
80
|
+
* success so the caller does not have to re-read the store.
|
|
81
|
+
*/
|
|
82
|
+
export function requireLicense(opts) {
|
|
83
|
+
let reason = null;
|
|
84
|
+
let claims = null;
|
|
85
|
+
try {
|
|
86
|
+
const store = opts?.store ?? createLicenseStore();
|
|
87
|
+
const anchor = opts?.anchor ?? TRUST_ANCHOR;
|
|
88
|
+
const now = opts?.now ?? (() => Date.now());
|
|
89
|
+
// An injected store is a fixture: read exactly the file it owns. The
|
|
90
|
+
// production path walks the shared discovery list so ONE activation made by
|
|
91
|
+
// any xcoder binary satisfies every other one on the machine.
|
|
92
|
+
const files = opts?.store ? [path.join(store.dir, "activation.json")] : resolveCandidateFiles();
|
|
93
|
+
let sawDecodableReceipt = false;
|
|
94
|
+
let sawUnreadableStore = false;
|
|
95
|
+
for (const file of files) {
|
|
96
|
+
const decoded = decodeStateFile(file);
|
|
97
|
+
// A pre-unification record carries a receipt but none of the activation
|
|
98
|
+
// fields. It is a candidate all the same: verify the receipt and, if it
|
|
99
|
+
// holds, adopt it into the shared store on first use.
|
|
100
|
+
const legacy = !decoded.ok && decoded.reason === "host-legacy" ? decoded.partial : null;
|
|
101
|
+
const receipt = decoded.ok ? decoded.record.receipt : legacy ? legacy.receipt : null;
|
|
102
|
+
if (receipt) {
|
|
103
|
+
sawDecodableReceipt = true;
|
|
104
|
+
const result = verifyReceipt(receipt, {
|
|
105
|
+
publicKeyPem: anchor.publicKeyPem,
|
|
106
|
+
pinnedPublicKeyPem: anchor.publicKeyPem,
|
|
107
|
+
issuer: LICENSE_ISSUER,
|
|
108
|
+
audience: LICENSE_PRODUCT_ID,
|
|
109
|
+
now: now(),
|
|
110
|
+
});
|
|
111
|
+
if (result.ok) {
|
|
112
|
+
claims = result.claims;
|
|
113
|
+
if (legacy && !opts?.store)
|
|
114
|
+
adoptLegacy(result.claims, receipt, legacy.installationId);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
// Keep the most specific verdict seen so far; candidates are ordered by
|
|
118
|
+
// priority, so a later, lower-priority file never overwrites it.
|
|
119
|
+
if (!reason)
|
|
120
|
+
reason = isDenialReason(result.reason) ? result.reason : "corrupt-store";
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// The store never deletes anything, so the gate is the layer that tells
|
|
124
|
+
// "unreadable" apart from "not activated yet" (and from a record that is
|
|
125
|
+
// merely awaiting the schema upgrade).
|
|
126
|
+
if (!decoded.ok && decoded.reason !== "absent" && decoded.reason !== "incomplete") {
|
|
127
|
+
sawUnreadableStore = true;
|
|
128
|
+
}
|
|
129
|
+
else if (activationFileExists(file)) {
|
|
130
|
+
sawUnreadableStore = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!claims) {
|
|
134
|
+
if (sawUnreadableStore)
|
|
135
|
+
reason = "corrupt-store";
|
|
136
|
+
else if (!sawDecodableReceipt)
|
|
137
|
+
reason = "no-activation";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Any unexpected fault (EACCES, unreadable dir, bad anchor) fails closed
|
|
142
|
+
// and is reported as a corrupt store rather than leaking internals.
|
|
143
|
+
reason = "corrupt-store";
|
|
144
|
+
claims = null;
|
|
145
|
+
}
|
|
146
|
+
if (claims)
|
|
147
|
+
return { allowed: true, claims };
|
|
148
|
+
const denied = reason ?? "corrupt-store";
|
|
149
|
+
return { allowed: false, reason: denied, message: denialMessage(denied) };
|
|
150
|
+
}
|
|
151
|
+
/** Hard ceiling on an opportunistic refresh; never blocks for longer. */
|
|
152
|
+
const REFRESH_TIMEOUT_MS = 5_000;
|
|
153
|
+
/**
|
|
154
|
+
* Opportunity-refresh after a successful gate. Best-effort by contract: any
|
|
155
|
+
* failure is swallowed so an offline machine still runs the command.
|
|
156
|
+
*/
|
|
157
|
+
export async function maybeRefreshLicense(mgr, now) {
|
|
158
|
+
try {
|
|
159
|
+
const status = mgr.status(now ? now() : undefined);
|
|
160
|
+
if (!status.activation || !status.needsRefresh)
|
|
161
|
+
return;
|
|
162
|
+
await withTimeout(mgr.refresh(), REFRESH_TIMEOUT_MS);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Deliberately ignored — the caller logs at dim level at most.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function withTimeout(promise, ms) {
|
|
169
|
+
return new Promise((resolve, reject) => {
|
|
170
|
+
const timer = setTimeout(() => reject(new Error("license refresh timed out")), ms);
|
|
171
|
+
timer.unref?.();
|
|
172
|
+
promise.then((value) => {
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
resolve(value);
|
|
175
|
+
}, (err) => {
|
|
176
|
+
clearTimeout(timer);
|
|
177
|
+
reject(err);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/** Build a manager with production wiring (used by licensed call sites). */
|
|
182
|
+
export function createDefaultLicenseManager(api, store) {
|
|
183
|
+
return createLicenseManager({
|
|
184
|
+
store: store ?? createLicenseStore(),
|
|
185
|
+
api: api ?? createLicenseApi({ baseUrl: LICENSE_API_BASE_URL, retries: 1 }),
|
|
186
|
+
anchor: TRUST_ANCHOR,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { LicenseClaims } from "./types.js";
|
|
2
|
+
export interface ActivationRequest {
|
|
3
|
+
serialKey: string;
|
|
4
|
+
productId: "xcoder-sdlc-cli";
|
|
5
|
+
installationId: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface RefreshRequest {
|
|
9
|
+
activationId: string;
|
|
10
|
+
installationId: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ReleaseRequest {
|
|
13
|
+
activationId: string;
|
|
14
|
+
installationId: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ActivationResponse {
|
|
17
|
+
receipt: string;
|
|
18
|
+
activationId: string;
|
|
19
|
+
claims: LicenseClaims;
|
|
20
|
+
/** INFORMATIONAL. Never a trust anchor. Never read. */
|
|
21
|
+
publicKey?: string;
|
|
22
|
+
}
|
|
23
|
+
export type LicenseApiErrorKind = "invalid-input" | "license-invalid" | "seat-limit" | "network" | "timeout" | "server" | "bad-response";
|
|
24
|
+
export declare class LicenseApiError extends Error {
|
|
25
|
+
readonly kind: LicenseApiErrorKind;
|
|
26
|
+
readonly status?: number;
|
|
27
|
+
constructor(kind: LicenseApiErrorKind, message: string, status?: number);
|
|
28
|
+
}
|
|
29
|
+
export interface LicenseApi {
|
|
30
|
+
activate(req: ActivationRequest, signal?: AbortSignal): Promise<ActivationResponse>;
|
|
31
|
+
refresh(req: RefreshRequest, signal?: AbortSignal): Promise<ActivationResponse>;
|
|
32
|
+
release(req: ReleaseRequest, signal?: AbortSignal): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export interface HttpClientOptions {
|
|
35
|
+
baseUrl: string;
|
|
36
|
+
/** Default 5_000 ms; clamped to a 10_000 ms hard ceiling. */
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
/** Injection point for tests; defaults to the platform fetch. */
|
|
39
|
+
fetchImpl?: typeof fetch;
|
|
40
|
+
/** Retries for transient failures. Defaults to 0. */
|
|
41
|
+
retries?: number;
|
|
42
|
+
}
|
|
43
|
+
export declare function createLicenseApi(opts: HttpClientOptions): LicenseApi;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// xcoder:version 3 | xcoder:task task-689517 | xcoder:updated 2026-11-01T08:22:46.853Z | xcoder:subtask code-st-828150
|
|
2
|
+
// Typed HTTP access to the Key System activation API.
|
|
3
|
+
//
|
|
4
|
+
// Every response body is treated as hostile input and zod-validated before
|
|
5
|
+
// use — this includes the `publicKey` field, which is *discarded*: a
|
|
6
|
+
// server-supplied key must never become a trust anchor. Trust in the receipt
|
|
7
|
+
// is decided in exactly one place (receipt.ts) against the build-pinned key.
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
export class LicenseApiError extends Error {
|
|
10
|
+
kind;
|
|
11
|
+
status;
|
|
12
|
+
constructor(kind, message, status) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "LicenseApiError";
|
|
15
|
+
this.kind = kind;
|
|
16
|
+
this.status = status;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
20
|
+
const MAX_TIMEOUT_MS = 10_000;
|
|
21
|
+
/**
|
|
22
|
+
* Hard ceiling on a response body. A receipt is a few hundred bytes; anything
|
|
23
|
+
* near this is a misrouted or hostile endpoint, and buffering it unbounded is
|
|
24
|
+
* a cheap denial-of-service against the caller.
|
|
25
|
+
*/
|
|
26
|
+
const MAX_RESPONSE_BYTES = 64 * 1024;
|
|
27
|
+
/**
|
|
28
|
+
* Refuse an oversized body, then read it.
|
|
29
|
+
*
|
|
30
|
+
* `content-length` is advisory (a hostile server can understate or omit it), so
|
|
31
|
+
* the declared size is a fast reject and the actual length is re-checked after
|
|
32
|
+
* the read. Neither path silently truncates: a truncated receipt would fail
|
|
33
|
+
* signature verification anyway, and the honest verdict is `bad-response`.
|
|
34
|
+
*/
|
|
35
|
+
async function readBounded(response) {
|
|
36
|
+
const declared = Number(response.headers.get("content-length"));
|
|
37
|
+
if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES) {
|
|
38
|
+
throw new LicenseApiError("bad-response", USER_MESSAGE["bad-response"], response.status);
|
|
39
|
+
}
|
|
40
|
+
const text = await response.text();
|
|
41
|
+
if (text.length > MAX_RESPONSE_BYTES) {
|
|
42
|
+
throw new LicenseApiError("bad-response", USER_MESSAGE["bad-response"], response.status);
|
|
43
|
+
}
|
|
44
|
+
return text;
|
|
45
|
+
}
|
|
46
|
+
const ClaimsSchema = z.strictObject({
|
|
47
|
+
iss: z.string().min(1),
|
|
48
|
+
aud: z.string().min(1),
|
|
49
|
+
licenseId: z.string().min(1),
|
|
50
|
+
activationId: z.string().min(1),
|
|
51
|
+
plan: z.string().min(1),
|
|
52
|
+
issuedAt: z.string().min(1),
|
|
53
|
+
expiresAt: z.string().min(1),
|
|
54
|
+
refreshBy: z.string().min(1),
|
|
55
|
+
});
|
|
56
|
+
const ActivationResponseSchema = z.object({
|
|
57
|
+
receipt: z.string().min(1).max(8 * 1024),
|
|
58
|
+
activationId: z.string().min(1).max(256),
|
|
59
|
+
claims: ClaimsSchema,
|
|
60
|
+
publicKey: z.string().optional(), // parsed, then intentionally ignored
|
|
61
|
+
});
|
|
62
|
+
const ErrorBodySchema = z.object({ error: z.object({ code: z.string().min(1) }) });
|
|
63
|
+
const USER_MESSAGE = {
|
|
64
|
+
"invalid-input": "the serial key was rejected as malformed",
|
|
65
|
+
"license-invalid": "the serial key is not valid",
|
|
66
|
+
"seat-limit": "this serial key has no free seats left",
|
|
67
|
+
network: "could not reach the license server",
|
|
68
|
+
timeout: "the license server did not respond in time",
|
|
69
|
+
server: "the license server reported an error",
|
|
70
|
+
"bad-response": "the license server returned an unexpected response",
|
|
71
|
+
};
|
|
72
|
+
function statusToKind(status) {
|
|
73
|
+
if (status === 400)
|
|
74
|
+
return "invalid-input";
|
|
75
|
+
if (status === 403)
|
|
76
|
+
return "license-invalid";
|
|
77
|
+
if (status === 409)
|
|
78
|
+
return "seat-limit";
|
|
79
|
+
if (status >= 500)
|
|
80
|
+
return "server";
|
|
81
|
+
if (status >= 200 && status < 300)
|
|
82
|
+
return null;
|
|
83
|
+
// Any other 4xx (e.g. 404/429) is a deployment/surface problem, not a
|
|
84
|
+
// user-actionable license state — surface it as a bad response.
|
|
85
|
+
return "bad-response";
|
|
86
|
+
}
|
|
87
|
+
function isRetryable(kind) {
|
|
88
|
+
return kind === "network" || kind === "timeout" || kind === "server";
|
|
89
|
+
}
|
|
90
|
+
function readErrorCode(text) {
|
|
91
|
+
try {
|
|
92
|
+
const parsed = ErrorBodySchema.safeParse(JSON.parse(text));
|
|
93
|
+
return parsed.success ? parsed.data.error.code : undefined;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function createLicenseApi(opts) {
|
|
100
|
+
const baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
101
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
102
|
+
const timeoutMs = Math.min(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
103
|
+
const retries = Math.max(0, opts.retries ?? 0);
|
|
104
|
+
async function once(url, body, outerSignal) {
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
107
|
+
const onOuterAbort = () => controller.abort();
|
|
108
|
+
outerSignal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
109
|
+
try {
|
|
110
|
+
return await fetchImpl(url, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
113
|
+
body: JSON.stringify(body),
|
|
114
|
+
signal: controller.signal,
|
|
115
|
+
// A license endpoint never legitimately redirects. Following one would
|
|
116
|
+
// hand the serial key to whatever host the redirect names, so a 3xx is
|
|
117
|
+
// a hard transport failure rather than a transparent hop.
|
|
118
|
+
redirect: "error",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
outerSignal?.removeEventListener("abort", onOuterAbort);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function post(path, body, outerSignal) {
|
|
127
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
128
|
+
const lastAttempt = attempt === retries;
|
|
129
|
+
let response;
|
|
130
|
+
try {
|
|
131
|
+
response = await once(`${baseUrl}${path}`, body, outerSignal);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
// An abort surfaces as AbortError (web fetch) or a DOMException-ish
|
|
135
|
+
// error whose name is "AbortError"; anything else is a transport fault.
|
|
136
|
+
const kind = err?.name === "AbortError" ? "timeout" : "network";
|
|
137
|
+
if (!lastAttempt && isRetryable(kind))
|
|
138
|
+
continue;
|
|
139
|
+
throw new LicenseApiError(kind, USER_MESSAGE[kind]);
|
|
140
|
+
}
|
|
141
|
+
let text;
|
|
142
|
+
try {
|
|
143
|
+
text = await readBounded(response);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
if (err instanceof LicenseApiError)
|
|
147
|
+
throw err;
|
|
148
|
+
if (!lastAttempt)
|
|
149
|
+
continue;
|
|
150
|
+
throw new LicenseApiError("network", USER_MESSAGE.network);
|
|
151
|
+
}
|
|
152
|
+
const kind = statusToKind(response.status);
|
|
153
|
+
if (kind === null)
|
|
154
|
+
return text;
|
|
155
|
+
const code = readErrorCode(text);
|
|
156
|
+
if (!lastAttempt && isRetryable(kind))
|
|
157
|
+
continue;
|
|
158
|
+
const message = code ? `${USER_MESSAGE[kind]} (${code})` : USER_MESSAGE[kind];
|
|
159
|
+
throw new LicenseApiError(kind, message, response.status);
|
|
160
|
+
}
|
|
161
|
+
// Unreachable: the loop either returns or throws.
|
|
162
|
+
throw new LicenseApiError("network", USER_MESSAGE.network);
|
|
163
|
+
}
|
|
164
|
+
function parseActivation(text) {
|
|
165
|
+
let raw;
|
|
166
|
+
try {
|
|
167
|
+
raw = JSON.parse(text);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
throw new LicenseApiError("bad-response", USER_MESSAGE["bad-response"]);
|
|
171
|
+
}
|
|
172
|
+
const parsed = ActivationResponseSchema.safeParse(raw);
|
|
173
|
+
if (!parsed.success) {
|
|
174
|
+
throw new LicenseApiError("bad-response", USER_MESSAGE["bad-response"]);
|
|
175
|
+
}
|
|
176
|
+
// `publicKey` is dropped here on purpose: it is parsed only so the schema
|
|
177
|
+
// stays honest about the wire shape, never used as a trust anchor.
|
|
178
|
+
return {
|
|
179
|
+
receipt: parsed.data.receipt,
|
|
180
|
+
activationId: parsed.data.activationId,
|
|
181
|
+
claims: parsed.data.claims,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
async activate(req, signal) {
|
|
186
|
+
return parseActivation(await post("/activations", req, signal));
|
|
187
|
+
},
|
|
188
|
+
async refresh(req, signal) {
|
|
189
|
+
return parseActivation(await post("/activations/refresh", req, signal));
|
|
190
|
+
},
|
|
191
|
+
async release(req, signal) {
|
|
192
|
+
await post("/activations/release", req, signal);
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LicenseApi } from "./http-client.js";
|
|
2
|
+
import type { LicenseStore, ActivationRecord } from "./store.js";
|
|
3
|
+
import type { VerifyResult } from "./types.js";
|
|
4
|
+
import { type TrustAnchor } from "./trust-anchor.js";
|
|
5
|
+
/** Opportunistic refresh starts this far before `refreshBy`. */
|
|
6
|
+
export declare const REFRESH_WINDOW_MS: number;
|
|
7
|
+
export interface LicenseStatus {
|
|
8
|
+
activation: ActivationRecord | null;
|
|
9
|
+
/** The verification verdict at `now`; never throws. */
|
|
10
|
+
verification: VerifyResult | {
|
|
11
|
+
ok: false;
|
|
12
|
+
reason: "no-activation";
|
|
13
|
+
};
|
|
14
|
+
/** True when now >= refreshBy - REFRESH_WINDOW_MS. */
|
|
15
|
+
needsRefresh: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface LicenseManager {
|
|
18
|
+
activate(input: {
|
|
19
|
+
serialKey: string;
|
|
20
|
+
label?: string;
|
|
21
|
+
}): Promise<ActivationRecord>;
|
|
22
|
+
refresh(): Promise<ActivationRecord>;
|
|
23
|
+
release(): Promise<void>;
|
|
24
|
+
status(now?: number): LicenseStatus;
|
|
25
|
+
}
|
|
26
|
+
export interface LicenseManagerDeps {
|
|
27
|
+
store: LicenseStore;
|
|
28
|
+
api: LicenseApi;
|
|
29
|
+
anchor: TrustAnchor;
|
|
30
|
+
now?: () => number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Normalise and sanity-check a serial key locally, before any network call.
|
|
34
|
+
* Throws a typed `invalid-input` so the CLI has one error shape to print.
|
|
35
|
+
*/
|
|
36
|
+
export declare function normalizeSerialKey(raw: string): string;
|
|
37
|
+
/** Short, non-reversible stand-in used for logging/debugging only. */
|
|
38
|
+
export declare function fingerprintSerialKey(serialKey: string): string;
|
|
39
|
+
export declare function createLicenseManager(deps: LicenseManagerDeps): LicenseManager;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// xcoder:version 4 | xcoder:task task-689517 | xcoder:updated 2026-11-01T08:22:08.457Z | xcoder:subtask code-st-828150
|
|
2
|
+
// License use-cases: activate / refresh / release / status.
|
|
3
|
+
//
|
|
4
|
+
// Composes the store, the HTTP client and the pure verifier. No CLI
|
|
5
|
+
// formatting and no process exit here — those belong to the command layer,
|
|
6
|
+
// so every rule below is unit-testable without spawning a process.
|
|
7
|
+
//
|
|
8
|
+
// The two rules the acceptance criteria hang on:
|
|
9
|
+
// * a receipt is verified BEFORE it is persisted, so a bad or malicious
|
|
10
|
+
// server response can never be written to disk;
|
|
11
|
+
// * a refresh can never extend validity — the new receipt is persisted
|
|
12
|
+
// only when its offline window is at least as long as the old one.
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { verifyReceipt, validUntilMs } from "./receipt.js";
|
|
15
|
+
import { LicenseApiError } from "./http-client.js";
|
|
16
|
+
import { recordFromClaims } from "./state-codec.js";
|
|
17
|
+
import { LICENSE_ISSUER, LICENSE_PRODUCT_ID, RECEIPT_TTL_MS } from "./trust-anchor.js";
|
|
18
|
+
/** Opportunistic refresh starts this far before `refreshBy`. */
|
|
19
|
+
export const REFRESH_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
20
|
+
/** Local, pre-network sanity limits on a serial key. */
|
|
21
|
+
const SERIAL_KEY_MAX_LENGTH = 200;
|
|
22
|
+
const SERIAL_KEY_PATTERN = /^[A-Z0-9-]+$/;
|
|
23
|
+
/**
|
|
24
|
+
* Normalise and sanity-check a serial key locally, before any network call.
|
|
25
|
+
* Throws a typed `invalid-input` so the CLI has one error shape to print.
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeSerialKey(raw) {
|
|
28
|
+
const trimmed = typeof raw === "string" ? raw.trim().toUpperCase() : "";
|
|
29
|
+
// Reject control characters outright; never echo the value back.
|
|
30
|
+
if (!trimmed || trimmed.length > SERIAL_KEY_MAX_LENGTH || /[\u0000-\u001f\u007f]/.test(trimmed)) {
|
|
31
|
+
throw new LicenseApiError("invalid-input", "the serial key was rejected as malformed");
|
|
32
|
+
}
|
|
33
|
+
if (!SERIAL_KEY_PATTERN.test(trimmed)) {
|
|
34
|
+
throw new LicenseApiError("invalid-input", "the serial key contains unsupported characters");
|
|
35
|
+
}
|
|
36
|
+
return trimmed;
|
|
37
|
+
}
|
|
38
|
+
/** Short, non-reversible stand-in used for logging/debugging only. */
|
|
39
|
+
export function fingerprintSerialKey(serialKey) {
|
|
40
|
+
return createHash("sha256").update(serialKey).digest("hex").slice(0, 12);
|
|
41
|
+
}
|
|
42
|
+
function denialToApiError(reason) {
|
|
43
|
+
return new LicenseApiError("bad-response", `the license server returned a receipt this build does not trust (${reason})`);
|
|
44
|
+
}
|
|
45
|
+
export function createLicenseManager(deps) {
|
|
46
|
+
const { store, api, anchor } = deps;
|
|
47
|
+
const now = deps.now ?? (() => Date.now());
|
|
48
|
+
function verify(rawReceipt, at) {
|
|
49
|
+
return verifyReceipt(rawReceipt, {
|
|
50
|
+
publicKeyPem: anchor.publicKeyPem,
|
|
51
|
+
pinnedPublicKeyPem: anchor.publicKeyPem,
|
|
52
|
+
issuer: LICENSE_ISSUER,
|
|
53
|
+
audience: LICENSE_PRODUCT_ID,
|
|
54
|
+
now: at,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Persistable record, projected from VERIFIED claims.
|
|
59
|
+
*
|
|
60
|
+
* The activation id comes from the claims too: the wire `activationId` is
|
|
61
|
+
* only an echo of the request, while the claims are what the pinned key
|
|
62
|
+
* actually signed. `recordFromClaims` (state-codec) keeps this in step with
|
|
63
|
+
* the shared on-disk schema the other two binaries read.
|
|
64
|
+
*/
|
|
65
|
+
function toRecord(response, claims, installationId) {
|
|
66
|
+
const projected = recordFromClaims(claims, {
|
|
67
|
+
installationId,
|
|
68
|
+
receipt: response.receipt,
|
|
69
|
+
savedAt: new Date(now()).toISOString(),
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
activationId: projected.activationId,
|
|
73
|
+
licenseId: projected.licenseId,
|
|
74
|
+
plan: projected.plan,
|
|
75
|
+
receipt: projected.receipt,
|
|
76
|
+
expiresAt: projected.expiresAt,
|
|
77
|
+
refreshBy: projected.refreshBy,
|
|
78
|
+
savedAt: projected.savedAt,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
async activate(input) {
|
|
83
|
+
const serialKey = normalizeSerialKey(input.serialKey);
|
|
84
|
+
const installation = store.ensureInstallation();
|
|
85
|
+
const response = await api.activate({
|
|
86
|
+
serialKey,
|
|
87
|
+
productId: LICENSE_PRODUCT_ID,
|
|
88
|
+
installationId: installation.installationId,
|
|
89
|
+
...(input.label ? { label: input.label.slice(0, 120) } : {}),
|
|
90
|
+
});
|
|
91
|
+
// Verify BEFORE persisting: an untrusted receipt is never written.
|
|
92
|
+
const verdict = verify(response.receipt, now());
|
|
93
|
+
if (!verdict.ok)
|
|
94
|
+
throw denialToApiError(verdict.reason);
|
|
95
|
+
const record = toRecord(response, verdict.claims, installation.installationId);
|
|
96
|
+
store.writeActivation(record); // replace, never append (AC3)
|
|
97
|
+
return record;
|
|
98
|
+
},
|
|
99
|
+
async refresh() {
|
|
100
|
+
const existing = store.readActivation();
|
|
101
|
+
if (!existing) {
|
|
102
|
+
throw new LicenseApiError("license-invalid", "there is no activation to refresh — run `xcoder license activate`");
|
|
103
|
+
}
|
|
104
|
+
const installation = store.readInstallation();
|
|
105
|
+
if (!installation) {
|
|
106
|
+
throw new LicenseApiError("license-invalid", "this machine has no installation id — run `xcoder license activate`");
|
|
107
|
+
}
|
|
108
|
+
// A refresh failing throws; the stored record is left untouched, so a
|
|
109
|
+
// failed refresh can never extend validity.
|
|
110
|
+
const response = await api.refresh({
|
|
111
|
+
activationId: existing.activationId,
|
|
112
|
+
installationId: installation.installationId,
|
|
113
|
+
});
|
|
114
|
+
const verdict = verify(response.receipt, now());
|
|
115
|
+
if (!verdict.ok)
|
|
116
|
+
throw denialToApiError(verdict.reason);
|
|
117
|
+
// Never shorten. Also re-assert the TTL bound against the old window so
|
|
118
|
+
// a compromised server cannot push the deadline arbitrarily far out.
|
|
119
|
+
// The stored record carries the verified claims' expiresAt/refreshBy,
|
|
120
|
+
// so this is exactly validUntilMs() of the previous receipt.
|
|
121
|
+
const previousUntil = Math.min(Date.parse(existing.expiresAt), Date.parse(existing.refreshBy));
|
|
122
|
+
const nextUntil = validUntilMs(verdict.claims);
|
|
123
|
+
const issuedAt = Date.parse(verdict.claims.issuedAt);
|
|
124
|
+
const ttlBound = Math.min(Date.parse(verdict.claims.expiresAt), issuedAt + RECEIPT_TTL_MS);
|
|
125
|
+
if (Date.parse(verdict.claims.refreshBy) > ttlBound)
|
|
126
|
+
throw denialToApiError("malformed-receipt");
|
|
127
|
+
if (Number.isNaN(nextUntil) || nextUntil < previousUntil)
|
|
128
|
+
throw denialToApiError("malformed-receipt");
|
|
129
|
+
const record = toRecord(response, verdict.claims, installation.installationId);
|
|
130
|
+
store.writeActivation(record);
|
|
131
|
+
return record;
|
|
132
|
+
},
|
|
133
|
+
async release() {
|
|
134
|
+
const existing = store.readActivation();
|
|
135
|
+
if (!existing)
|
|
136
|
+
return; // nothing activated locally — nothing to free
|
|
137
|
+
const installation = store.readInstallation();
|
|
138
|
+
if (!installation) {
|
|
139
|
+
store.clearActivation();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
await api.release({
|
|
144
|
+
activationId: existing.activationId,
|
|
145
|
+
installationId: installation.installationId,
|
|
146
|
+
});
|
|
147
|
+
store.clearActivation();
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
const kind = err instanceof LicenseApiError ? err.kind : undefined;
|
|
151
|
+
// The seat is already gone server-side if the activation is unknown
|
|
152
|
+
// or revoked — clear locally so the user is not stuck.
|
|
153
|
+
if (kind === "license-invalid") {
|
|
154
|
+
store.clearActivation();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// network / timeout / server: keep local state so the user can retry.
|
|
158
|
+
throw err instanceof LicenseApiError
|
|
159
|
+
? err
|
|
160
|
+
: new LicenseApiError("network", "could not reach the license server");
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
status(at) {
|
|
164
|
+
const moment = at ?? now();
|
|
165
|
+
const activation = store.readActivation();
|
|
166
|
+
if (!activation) {
|
|
167
|
+
return { activation: null, verification: { ok: false, reason: "no-activation" }, needsRefresh: false };
|
|
168
|
+
}
|
|
169
|
+
const verification = verify(activation.receipt, moment);
|
|
170
|
+
const refreshBy = Date.parse(activation.refreshBy);
|
|
171
|
+
const needsRefresh = !Number.isNaN(refreshBy) && moment >= refreshBy - REFRESH_WINDOW_MS;
|
|
172
|
+
return { activation, verification, needsRefresh };
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { LicenseClaims, VerifyResult } from "./types.js";
|
|
2
|
+
export interface VerifyContext {
|
|
3
|
+
publicKeyPem: string;
|
|
4
|
+
issuer: string;
|
|
5
|
+
audience: string;
|
|
6
|
+
/** Epoch ms — injected, never read from the ambient clock. */
|
|
7
|
+
now: number;
|
|
8
|
+
/** Tolerance for `not-yet-valid`. Defaults to 60 s. */
|
|
9
|
+
clockSkewMs?: number;
|
|
10
|
+
/**
|
|
11
|
+
* The single key this build trusts. When supplied, a `publicKeyPem` that
|
|
12
|
+
* differs from it is rejected as `untrusted-key` *before* any signature is
|
|
13
|
+
* checked — a receipt is never validated against a key that is not pinned.
|
|
14
|
+
*/
|
|
15
|
+
pinnedPublicKeyPem?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Single source of truth for the offline window: a receipt is usable until
|
|
19
|
+
* whichever comes first, its subscription expiry or its refresh deadline.
|
|
20
|
+
*/
|
|
21
|
+
export declare function validUntilMs(claims: LicenseClaims): number;
|
|
22
|
+
/**
|
|
23
|
+
* Verify a signed receipt against a pinned key.
|
|
24
|
+
*
|
|
25
|
+
* Pure and total: returns a typed result for every input, including garbage.
|
|
26
|
+
*/
|
|
27
|
+
export declare function verifyReceipt(rawReceipt: string, ctx: VerifyContext): VerifyResult;
|