stagent 0.9.5 → 0.10.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/README.md +5 -42
- package/dist/cli.js +42 -18
- package/docs/.coverage-gaps.json +13 -55
- package/docs/.last-generated +1 -1
- package/docs/features/provider-runtimes.md +4 -0
- package/docs/features/schedules.md +32 -4
- package/docs/features/settings.md +28 -5
- package/docs/features/tables.md +9 -2
- package/docs/features/workflows.md +10 -4
- package/docs/journeys/developer.md +15 -1
- package/docs/journeys/personal-use.md +21 -4
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
- package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
- package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
- package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
- package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
- package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
- package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
- package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
- package/package.json +3 -2
- package/src/__tests__/instrumentation-smoke.test.ts +15 -0
- package/src/app/analytics/page.tsx +1 -21
- package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
- package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
- package/src/app/api/instance/config/route.ts +41 -0
- package/src/app/api/instance/init/route.ts +34 -0
- package/src/app/api/instance/upgrade/check/route.ts +26 -0
- package/src/app/api/instance/upgrade/route.ts +96 -0
- package/src/app/api/instance/upgrade/status/route.ts +35 -0
- package/src/app/api/memory/route.ts +0 -11
- package/src/app/api/notifications/route.ts +4 -2
- package/src/app/api/projects/[id]/route.ts +5 -155
- package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
- package/src/app/api/schedules/[id]/execute/route.ts +111 -0
- package/src/app/api/schedules/[id]/route.ts +9 -1
- package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
- package/src/app/api/schedules/route.ts +3 -12
- package/src/app/api/settings/openai/login/route.ts +22 -0
- package/src/app/api/settings/openai/logout/route.ts +7 -0
- package/src/app/api/settings/openai/route.ts +21 -1
- package/src/app/api/settings/providers/route.ts +35 -8
- package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
- package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
- package/src/app/api/tables/[id]/enrich/route.ts +147 -0
- package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
- package/src/app/api/tasks/[id]/execute/route.ts +0 -21
- package/src/app/api/workflows/[id]/resume/route.ts +59 -0
- package/src/app/api/workflows/[id]/status/route.ts +22 -8
- package/src/app/api/workspace/context/route.ts +2 -0
- package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
- package/src/app/chat/page.tsx +11 -0
- package/src/app/inbox/page.tsx +12 -5
- package/src/app/layout.tsx +42 -21
- package/src/app/page.tsx +0 -2
- package/src/app/settings/page.tsx +6 -9
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
- package/src/components/chat/chat-command-popover.tsx +2 -2
- package/src/components/chat/chat-input.tsx +2 -3
- package/src/components/chat/chat-session-provider.tsx +720 -0
- package/src/components/chat/chat-shell.tsx +92 -401
- package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
- package/src/components/instance/instance-section.tsx +382 -0
- package/src/components/instance/upgrade-badge.tsx +219 -0
- package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
- package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
- package/src/components/notifications/batch-proposal-review.tsx +20 -5
- package/src/components/notifications/inbox-list.tsx +11 -2
- package/src/components/notifications/notification-item.tsx +56 -2
- package/src/components/notifications/pending-approval-host.tsx +56 -37
- package/src/components/schedules/schedule-create-sheet.tsx +19 -1
- package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
- package/src/components/schedules/schedule-form.tsx +31 -0
- package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
- package/src/components/settings/auth-method-selector.tsx +19 -4
- package/src/components/settings/auth-status-badge.tsx +28 -3
- package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
- package/src/components/settings/openai-runtime-section.tsx +7 -1
- package/src/components/settings/providers-runtimes-section.tsx +138 -19
- package/src/components/shared/app-sidebar.tsx +4 -3
- package/src/components/shared/command-palette.tsx +4 -5
- package/src/components/shared/theme-toggle.tsx +5 -24
- package/src/components/shared/workspace-indicator.tsx +61 -2
- package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
- package/src/components/tables/table-create-sheet.tsx +4 -0
- package/src/components/tables/table-enrichment-runs.tsx +103 -0
- package/src/components/tables/table-enrichment-sheet.tsx +538 -0
- package/src/components/tables/table-spreadsheet.tsx +29 -5
- package/src/components/tables/table-toolbar.tsx +10 -1
- package/src/components/tasks/kanban-board.tsx +1 -0
- package/src/components/tasks/kanban-column.tsx +53 -14
- package/src/components/tasks/task-bento-grid.tsx +19 -0
- package/src/components/tasks/task-card.tsx +26 -3
- package/src/components/tasks/task-chip-bar.tsx +24 -0
- package/src/components/tasks/task-result-renderer.tsx +1 -1
- package/src/components/workflows/delay-step-body.tsx +109 -0
- package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
- package/src/components/workflows/loop-status-view.tsx +1 -1
- package/src/components/workflows/shared/step-result.tsx +78 -0
- package/src/components/workflows/shared/workflow-header.tsx +141 -0
- package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
- package/src/components/workflows/swarm-dashboard.tsx +2 -15
- package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
- package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
- package/src/components/workflows/workflow-form-view.tsx +133 -16
- package/src/components/workflows/workflow-status-view.tsx +30 -740
- package/src/instrumentation-node.ts +94 -0
- package/src/instrumentation.ts +4 -48
- package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
- package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
- package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
- package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
- package/src/lib/agents/claude-agent.ts +155 -18
- package/src/lib/agents/execution-manager.ts +0 -35
- package/src/lib/agents/learned-context.ts +0 -12
- package/src/lib/agents/learning-session.ts +18 -5
- package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
- package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
- package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
- package/src/lib/agents/runtime/openai-codex.ts +29 -60
- package/src/lib/agents/runtime/types.ts +8 -0
- package/src/lib/book/chapter-mapping.ts +11 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
- package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
- package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
- package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
- package/src/lib/chat/active-streams.ts +27 -0
- package/src/lib/chat/codex-engine.ts +16 -17
- package/src/lib/chat/context-builder.ts +5 -3
- package/src/lib/chat/engine.ts +50 -3
- package/src/lib/chat/reconcile.ts +117 -0
- package/src/lib/chat/stagent-tools.ts +1 -0
- package/src/lib/chat/stream-telemetry.ts +132 -0
- package/src/lib/chat/suggested-prompts.ts +28 -1
- package/src/lib/chat/system-prompt.ts +26 -1
- package/src/lib/chat/tool-catalog.ts +2 -1
- package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
- package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
- package/src/lib/chat/tools/document-tools.ts +29 -13
- package/src/lib/chat/tools/helpers.ts +39 -0
- package/src/lib/chat/tools/notification-tools.ts +9 -5
- package/src/lib/chat/tools/project-tools.ts +33 -0
- package/src/lib/chat/tools/schedule-tools.ts +44 -11
- package/src/lib/chat/tools/table-tools.ts +71 -0
- package/src/lib/chat/tools/task-tools.ts +84 -20
- package/src/lib/chat/tools/workflow-tools.ts +234 -32
- package/src/lib/constants/settings.ts +8 -18
- package/src/lib/data/__tests__/clear.test.ts +56 -2
- package/src/lib/data/clear.ts +20 -15
- package/src/lib/data/delete-project.ts +171 -0
- package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
- package/src/lib/db/bootstrap.ts +45 -16
- package/src/lib/db/index.ts +5 -0
- package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
- package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
- package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
- package/src/lib/db/migrations/0026_drop_license.sql +3 -0
- package/src/lib/db/migrations/meta/_journal.json +21 -0
- package/src/lib/db/schema.ts +68 -23
- package/src/lib/environment/workspace-context.ts +13 -1
- package/src/lib/import/dedup.ts +4 -54
- package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
- package/src/lib/instance/__tests__/detect.test.ts +115 -0
- package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
- package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
- package/src/lib/instance/__tests__/settings.test.ts +83 -0
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
- package/src/lib/instance/bootstrap.ts +270 -0
- package/src/lib/instance/detect.ts +49 -0
- package/src/lib/instance/fingerprint.ts +78 -0
- package/src/lib/instance/git-ops.ts +95 -0
- package/src/lib/instance/settings.ts +61 -0
- package/src/lib/instance/types.ts +77 -0
- package/src/lib/instance/upgrade-poller.ts +153 -0
- package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
- package/src/lib/notifications/visibility.ts +33 -0
- package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
- package/src/lib/schedules/__tests__/config.test.ts +62 -0
- package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
- package/src/lib/schedules/__tests__/integration.test.ts +82 -0
- package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
- package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
- package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
- package/src/lib/schedules/collision-check.ts +105 -0
- package/src/lib/schedules/config.ts +53 -0
- package/src/lib/schedules/scheduler.ts +232 -13
- package/src/lib/schedules/slot-claim.ts +105 -0
- package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
- package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
- package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
- package/src/lib/settings/openai-auth.ts +105 -10
- package/src/lib/settings/openai-login-manager.ts +260 -0
- package/src/lib/settings/runtime-setup.ts +14 -4
- package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
- package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
- package/src/lib/tables/enrichment-planner.ts +454 -0
- package/src/lib/tables/enrichment.ts +328 -0
- package/src/lib/tables/query-builder.ts +5 -2
- package/src/lib/tables/trigger-evaluator.ts +3 -2
- package/src/lib/theme.ts +71 -0
- package/src/lib/usage/ledger.ts +2 -18
- package/src/lib/util/__tests__/similarity.test.ts +106 -0
- package/src/lib/util/similarity.ts +77 -0
- package/src/lib/utils/format-timestamp.ts +24 -0
- package/src/lib/utils/stagent-paths.ts +12 -0
- package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
- package/src/lib/validators/__tests__/settings.test.ts +10 -0
- package/src/lib/validators/blueprint.ts +70 -9
- package/src/lib/validators/profile.ts +2 -2
- package/src/lib/validators/settings.ts +3 -1
- package/src/lib/workflows/__tests__/delay.test.ts +196 -0
- package/src/lib/workflows/__tests__/engine.test.ts +8 -0
- package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
- package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
- package/src/lib/workflows/blueprints/instantiator.ts +22 -1
- package/src/lib/workflows/blueprints/types.ts +10 -2
- package/src/lib/workflows/delay.ts +106 -0
- package/src/lib/workflows/engine.ts +207 -4
- package/src/lib/workflows/loop-executor.ts +349 -24
- package/src/lib/workflows/post-action.ts +91 -0
- package/src/lib/workflows/types.ts +166 -1
- package/src/app/api/license/checkout/route.ts +0 -28
- package/src/app/api/license/portal/route.ts +0 -26
- package/src/app/api/license/route.ts +0 -89
- package/src/app/api/license/usage/route.ts +0 -63
- package/src/app/api/marketplace/browse/route.ts +0 -15
- package/src/app/api/marketplace/import/route.ts +0 -28
- package/src/app/api/marketplace/publish/route.ts +0 -40
- package/src/app/api/onboarding/email/route.ts +0 -53
- package/src/app/api/settings/telemetry/route.ts +0 -14
- package/src/app/api/sync/export/route.ts +0 -54
- package/src/app/api/sync/restore/route.ts +0 -37
- package/src/app/api/sync/sessions/route.ts +0 -24
- package/src/app/auth/callback/route.ts +0 -73
- package/src/app/marketplace/page.tsx +0 -19
- package/src/components/analytics/analytics-gate-card.tsx +0 -101
- package/src/components/marketplace/blueprint-card.tsx +0 -61
- package/src/components/marketplace/marketplace-browser.tsx +0 -131
- package/src/components/onboarding/email-capture-card.tsx +0 -104
- package/src/components/settings/activation-form.tsx +0 -95
- package/src/components/settings/cloud-account-section.tsx +0 -147
- package/src/components/settings/cloud-sync-section.tsx +0 -155
- package/src/components/settings/subscription-section.tsx +0 -410
- package/src/components/settings/telemetry-section.tsx +0 -80
- package/src/components/shared/premium-gate-overlay.tsx +0 -50
- package/src/components/shared/schedule-gate-dialog.tsx +0 -64
- package/src/components/shared/upgrade-banner.tsx +0 -112
- package/src/hooks/use-supabase-auth.ts +0 -79
- package/src/lib/billing/email.ts +0 -54
- package/src/lib/billing/products.ts +0 -80
- package/src/lib/billing/stripe.ts +0 -101
- package/src/lib/cloud/supabase-browser.ts +0 -32
- package/src/lib/cloud/supabase-client.ts +0 -56
- package/src/lib/license/__tests__/features.test.ts +0 -56
- package/src/lib/license/__tests__/key-format.test.ts +0 -88
- package/src/lib/license/__tests__/manager.test.ts +0 -64
- package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
- package/src/lib/license/cloud-validation.ts +0 -60
- package/src/lib/license/features.ts +0 -44
- package/src/lib/license/key-format.ts +0 -101
- package/src/lib/license/limit-check.ts +0 -111
- package/src/lib/license/limit-queries.ts +0 -51
- package/src/lib/license/manager.ts +0 -345
- package/src/lib/license/notifications.ts +0 -59
- package/src/lib/license/tier-limits.ts +0 -71
- package/src/lib/marketplace/marketplace-client.ts +0 -107
- package/src/lib/sync/cloud-sync.ts +0 -235
- package/src/lib/telemetry/conversion-events.ts +0 -71
- package/src/lib/telemetry/queue.ts +0 -122
- package/src/lib/validators/license.ts +0 -33
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect } from "react";
|
|
4
|
-
import Link from "next/link";
|
|
5
|
-
import { AlertTriangle, Lock } from "lucide-react";
|
|
6
|
-
import { Button } from "@/components/ui/button";
|
|
7
|
-
import { cn } from "@/lib/utils";
|
|
8
|
-
import { TIER_LABELS, type LicenseTier } from "@/lib/license/tier-limits";
|
|
9
|
-
import type { LimitResource } from "@/lib/license/tier-limits";
|
|
10
|
-
import { trackConversionEvent } from "@/lib/telemetry/conversion-events";
|
|
11
|
-
|
|
12
|
-
const BANNER_TITLES: Record<LimitResource, string> = {
|
|
13
|
-
agentMemories: "Memory limit approaching",
|
|
14
|
-
contextVersions: "Context version limit reached",
|
|
15
|
-
activeSchedules: "Schedule limit reached",
|
|
16
|
-
historyRetentionDays: "History retention limit",
|
|
17
|
-
parallelWorkflows: "Parallel workflow limit reached",
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
function getBannerMessage(
|
|
21
|
-
resource: LimitResource,
|
|
22
|
-
current: number,
|
|
23
|
-
max: number,
|
|
24
|
-
requiredTier: string
|
|
25
|
-
): string {
|
|
26
|
-
const tierLabel = TIER_LABELS[requiredTier as LicenseTier] ?? requiredTier;
|
|
27
|
-
switch (resource) {
|
|
28
|
-
case "agentMemories":
|
|
29
|
-
return `${current} of ${max} agent memories used. Upgrade to ${tierLabel} for more capacity.`;
|
|
30
|
-
case "contextVersions":
|
|
31
|
-
return `${current} of ${max} context versions used. Upgrade to ${tierLabel} to unlock more.`;
|
|
32
|
-
case "activeSchedules":
|
|
33
|
-
return `${current} of ${max} active schedules. Upgrade to ${tierLabel} for more schedules.`;
|
|
34
|
-
case "historyRetentionDays":
|
|
35
|
-
return `Execution history limited to ${max} days. Upgrade to ${tierLabel} for longer retention.`;
|
|
36
|
-
case "parallelWorkflows":
|
|
37
|
-
return `${current} of ${max} parallel workflows running. Upgrade to ${tierLabel} for more concurrency.`;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface UpgradeBannerProps {
|
|
42
|
-
resource: LimitResource;
|
|
43
|
-
current: number;
|
|
44
|
-
max: number;
|
|
45
|
-
requiredTier: string;
|
|
46
|
-
variant: "warning" | "blocked";
|
|
47
|
-
onDismiss?: () => void;
|
|
48
|
-
onSnooze?: () => void;
|
|
49
|
-
className?: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function UpgradeBanner({
|
|
53
|
-
resource,
|
|
54
|
-
current,
|
|
55
|
-
max,
|
|
56
|
-
requiredTier,
|
|
57
|
-
variant,
|
|
58
|
-
onDismiss,
|
|
59
|
-
onSnooze,
|
|
60
|
-
className,
|
|
61
|
-
}: UpgradeBannerProps) {
|
|
62
|
-
const Icon = variant === "blocked" ? Lock : AlertTriangle;
|
|
63
|
-
const tierLabel = TIER_LABELS[requiredTier as LicenseTier] ?? requiredTier;
|
|
64
|
-
|
|
65
|
-
// Track banner impression on mount
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
trackConversionEvent("banner_impression", resource);
|
|
68
|
-
}, [resource]);
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<div
|
|
72
|
-
role="alert"
|
|
73
|
-
aria-live="polite"
|
|
74
|
-
className={cn(
|
|
75
|
-
"surface-card-muted rounded-lg border p-4 flex items-start gap-3",
|
|
76
|
-
variant === "warning" && "border-amber-500/25",
|
|
77
|
-
variant === "blocked" && "border-destructive/25",
|
|
78
|
-
className
|
|
79
|
-
)}
|
|
80
|
-
>
|
|
81
|
-
<Icon
|
|
82
|
-
className={cn(
|
|
83
|
-
"size-4 shrink-0 mt-0.5",
|
|
84
|
-
variant === "warning" ? "text-amber-500" : "text-destructive"
|
|
85
|
-
)}
|
|
86
|
-
/>
|
|
87
|
-
<div className="flex-1 space-y-1">
|
|
88
|
-
<p className="text-sm font-medium">{BANNER_TITLES[resource]}</p>
|
|
89
|
-
<p className="text-xs text-muted-foreground">
|
|
90
|
-
{getBannerMessage(resource, current, max, requiredTier)}
|
|
91
|
-
</p>
|
|
92
|
-
<div className="flex items-center gap-2 pt-2">
|
|
93
|
-
<Button size="sm" asChild onClick={() => trackConversionEvent("banner_click", resource)}>
|
|
94
|
-
<Link href={`/settings?highlight=${requiredTier}`}>
|
|
95
|
-
Upgrade to {tierLabel}
|
|
96
|
-
</Link>
|
|
97
|
-
</Button>
|
|
98
|
-
{onSnooze && (
|
|
99
|
-
<Button size="sm" variant="ghost" onClick={onSnooze}>
|
|
100
|
-
Remind later
|
|
101
|
-
</Button>
|
|
102
|
-
)}
|
|
103
|
-
{onDismiss && (
|
|
104
|
-
<Button size="sm" variant="ghost" onClick={onDismiss}>
|
|
105
|
-
Dismiss
|
|
106
|
-
</Button>
|
|
107
|
-
)}
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
|
-
);
|
|
112
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, useCallback } from "react";
|
|
4
|
-
import { getSupabaseBrowserClient } from "@/lib/cloud/supabase-browser";
|
|
5
|
-
import type { Session, User } from "@supabase/supabase-js";
|
|
6
|
-
|
|
7
|
-
interface AuthState {
|
|
8
|
-
session: Session | null;
|
|
9
|
-
user: User | null;
|
|
10
|
-
email: string | null;
|
|
11
|
-
loading: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Hook for Supabase Auth state in the browser.
|
|
16
|
-
* Tracks session, provides sign-in/sign-out helpers.
|
|
17
|
-
*/
|
|
18
|
-
export function useSupabaseAuth() {
|
|
19
|
-
const [auth, setAuth] = useState<AuthState>({
|
|
20
|
-
session: null,
|
|
21
|
-
user: null,
|
|
22
|
-
email: null,
|
|
23
|
-
loading: true,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
const supabase = getSupabaseBrowserClient();
|
|
28
|
-
|
|
29
|
-
// Get initial session
|
|
30
|
-
supabase.auth.getSession().then(({ data: { session } }) => {
|
|
31
|
-
setAuth({
|
|
32
|
-
session,
|
|
33
|
-
user: session?.user ?? null,
|
|
34
|
-
email: session?.user?.email ?? null,
|
|
35
|
-
loading: false,
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// Listen for auth state changes (magic link callback, sign-out, etc.)
|
|
40
|
-
const {
|
|
41
|
-
data: { subscription },
|
|
42
|
-
} = supabase.auth.onAuthStateChange((_event, session) => {
|
|
43
|
-
setAuth({
|
|
44
|
-
session,
|
|
45
|
-
user: session?.user ?? null,
|
|
46
|
-
email: session?.user?.email ?? null,
|
|
47
|
-
loading: false,
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
return () => subscription.unsubscribe();
|
|
52
|
-
}, []);
|
|
53
|
-
|
|
54
|
-
const signInWithEmail = useCallback(async (email: string) => {
|
|
55
|
-
const supabase = getSupabaseBrowserClient();
|
|
56
|
-
const redirectTo = typeof window !== "undefined"
|
|
57
|
-
? `${window.location.origin}/auth/callback`
|
|
58
|
-
: "http://localhost:3000/auth/callback";
|
|
59
|
-
|
|
60
|
-
const { error } = await supabase.auth.signInWithOtp({
|
|
61
|
-
email,
|
|
62
|
-
options: { emailRedirectTo: redirectTo },
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
return { error: error?.message ?? null };
|
|
66
|
-
}, []);
|
|
67
|
-
|
|
68
|
-
const signOut = useCallback(async () => {
|
|
69
|
-
const supabase = getSupabaseBrowserClient();
|
|
70
|
-
await supabase.auth.signOut();
|
|
71
|
-
}, []);
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
...auth,
|
|
75
|
-
isSignedIn: !!auth.session,
|
|
76
|
-
signInWithEmail,
|
|
77
|
-
signOut,
|
|
78
|
-
};
|
|
79
|
-
}
|
package/src/lib/billing/email.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transactional email helpers via Resend.
|
|
3
|
-
*
|
|
4
|
-
* All emails are sent through the Supabase `send-email` Edge Function.
|
|
5
|
-
* This module provides typed wrappers with no-op behavior when
|
|
6
|
-
* the cloud backend is not configured.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { isCloudConfigured, getSupabaseUrl, getSupabaseAnonKey } from "@/lib/cloud/supabase-client";
|
|
10
|
-
|
|
11
|
-
async function sendEmail(
|
|
12
|
-
template: string,
|
|
13
|
-
to: string,
|
|
14
|
-
data: Record<string, unknown>
|
|
15
|
-
): Promise<void> {
|
|
16
|
-
if (!isCloudConfigured()) return;
|
|
17
|
-
|
|
18
|
-
const supabaseUrl = getSupabaseUrl();
|
|
19
|
-
const anonKey = getSupabaseAnonKey();
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
await fetch(`${supabaseUrl}/functions/v1/send-email`, {
|
|
23
|
-
method: "POST",
|
|
24
|
-
headers: {
|
|
25
|
-
"Content-Type": "application/json",
|
|
26
|
-
Authorization: `Bearer ${anonKey}`,
|
|
27
|
-
},
|
|
28
|
-
body: JSON.stringify({ template, to, data }),
|
|
29
|
-
});
|
|
30
|
-
} catch {
|
|
31
|
-
// Email sending is non-critical — log and continue
|
|
32
|
-
console.warn(`[email] Failed to send ${template} to ${to}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Welcome email for marketing site purchasers with install instructions */
|
|
37
|
-
export function sendWelcomeWithInstall(email: string, tier: string): Promise<void> {
|
|
38
|
-
return sendEmail("welcome-install", email, { tier });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Upgrade confirmation for in-app purchasers */
|
|
42
|
-
export function sendUpgradeConfirmation(email: string, tier: string): Promise<void> {
|
|
43
|
-
return sendEmail("upgrade-confirmation", email, { tier });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Memory cap warning (approaching limit) */
|
|
47
|
-
export function sendMemoryWarning(
|
|
48
|
-
email: string,
|
|
49
|
-
profileName: string,
|
|
50
|
-
current: number,
|
|
51
|
-
limit: number
|
|
52
|
-
): Promise<void> {
|
|
53
|
-
return sendEmail("memory-warning", email, { profileName, current, limit });
|
|
54
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stripe product and price configuration.
|
|
3
|
-
*
|
|
4
|
-
* Price IDs are placeholders — replace with actual Stripe dashboard values
|
|
5
|
-
* after creating products. The structure supports both monthly and annual
|
|
6
|
-
* billing for each tier.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { LicenseTier } from "@/lib/license/tier-limits";
|
|
10
|
-
|
|
11
|
-
export type BillingInterval = "monthly" | "annual";
|
|
12
|
-
|
|
13
|
-
export interface StripeProduct {
|
|
14
|
-
tier: Exclude<LicenseTier, "community">;
|
|
15
|
-
name: string;
|
|
16
|
-
description: string;
|
|
17
|
-
prices: Record<BillingInterval, StripePrice>;
|
|
18
|
-
paymentLinks: Record<BillingInterval, string>; // Static Stripe Payment Links for marketing site
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface StripePrice {
|
|
22
|
-
id: string; // Stripe Price ID (price_xxx)
|
|
23
|
-
amount: number; // In cents
|
|
24
|
-
currency: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Stripe product catalog — live Price IDs and Payment Links. */
|
|
28
|
-
export const STRIPE_PRODUCTS: StripeProduct[] = [
|
|
29
|
-
{
|
|
30
|
-
tier: "solo",
|
|
31
|
-
name: "Stagent Solo",
|
|
32
|
-
description: "Power users — expanded limits and advanced history",
|
|
33
|
-
prices: {
|
|
34
|
-
monthly: { id: "price_1TJ2d5RCxnzBPkIX4SnajFok", amount: 1900, currency: "usd" },
|
|
35
|
-
annual: { id: "price_1TJ2d5RCxnzBPkIXjjiyc7lb", amount: 19000, currency: "usd" },
|
|
36
|
-
},
|
|
37
|
-
paymentLinks: {
|
|
38
|
-
monthly: "https://buy.stagent.io/fZufZjgKC4q9azrgDzdwc06",
|
|
39
|
-
annual: "https://buy.stagent.io/bJe00l1PI7Cl7nf1IFdwc0b",
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
tier: "operator",
|
|
44
|
-
name: "Stagent Operator",
|
|
45
|
-
description: "Professionals — analytics, cloud sync, marketplace publishing",
|
|
46
|
-
prices: {
|
|
47
|
-
monthly: { id: "price_1TJ2e1RCxnzBPkIXZg47cNbO", amount: 4900, currency: "usd" },
|
|
48
|
-
annual: { id: "price_1TJ2e1RCxnzBPkIXODs5fZW2", amount: 49000, currency: "usd" },
|
|
49
|
-
},
|
|
50
|
-
paymentLinks: {
|
|
51
|
-
monthly: "https://buy.stagent.io/aFa4gB0LE9Kt22Vevrdwc07",
|
|
52
|
-
annual: "https://buy.stagent.io/bJe6oJdyq2i1bDv1IFdwc0a",
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
tier: "scale",
|
|
57
|
-
name: "Stagent Scale",
|
|
58
|
-
description: "Teams — unlimited everything, featured marketplace, priority support",
|
|
59
|
-
prices: {
|
|
60
|
-
monthly: { id: "price_1TJ2evRCxnzBPkIXy9mBqBHB", amount: 9900, currency: "usd" },
|
|
61
|
-
annual: { id: "price_1TJ2evRCxnzBPkIXqIRaDxQp", amount: 99000, currency: "usd" },
|
|
62
|
-
},
|
|
63
|
-
paymentLinks: {
|
|
64
|
-
monthly: "https://buy.stagent.io/9B628t2TM5udazr72Zdwc08",
|
|
65
|
-
annual: "https://buy.stagent.io/dRmfZjbqicWF5f7873dwc09",
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
];
|
|
69
|
-
|
|
70
|
-
/** Map from Stripe Price ID → tier for webhook processing */
|
|
71
|
-
export const PRICE_TO_TIER: Record<string, LicenseTier> = Object.fromEntries(
|
|
72
|
-
STRIPE_PRODUCTS.flatMap((p) =>
|
|
73
|
-
Object.values(p.prices).map((price) => [price.id, p.tier])
|
|
74
|
-
)
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
/** Get the product for a specific tier */
|
|
78
|
-
export function getProductForTier(tier: Exclude<LicenseTier, "community">): StripeProduct | undefined {
|
|
79
|
-
return STRIPE_PRODUCTS.find((p) => p.tier === tier);
|
|
80
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stripe billing helpers.
|
|
3
|
-
*
|
|
4
|
-
* All Stripe API calls go through Supabase Edge Functions —
|
|
5
|
-
* no Stripe secret key in the local app. This module calls
|
|
6
|
-
* Edge Functions to create Checkout Sessions and Portal Sessions.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { isCloudConfigured, getSupabaseUrl, getSupabaseAnonKey } from "@/lib/cloud/supabase-client";
|
|
10
|
-
import { getProductForTier } from "./products";
|
|
11
|
-
import type { LicenseTier } from "@/lib/license/tier-limits";
|
|
12
|
-
import type { BillingInterval } from "./products";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Create a Stripe Checkout Session for in-app upgrade.
|
|
16
|
-
* Returns the checkout URL to redirect the user to.
|
|
17
|
-
*/
|
|
18
|
-
export async function createCheckoutSession(
|
|
19
|
-
tier: Exclude<LicenseTier, "community">,
|
|
20
|
-
billingPeriod: BillingInterval = "monthly",
|
|
21
|
-
returnUrl?: string
|
|
22
|
-
): Promise<{ url: string } | { error: string }> {
|
|
23
|
-
if (!isCloudConfigured()) {
|
|
24
|
-
return { error: "Cloud backend not configured" };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const product = getProductForTier(tier);
|
|
28
|
-
if (!product) {
|
|
29
|
-
return { error: `Unknown tier: ${tier}` };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const priceId = product.prices[billingPeriod].id;
|
|
33
|
-
const supabaseUrl = getSupabaseUrl();
|
|
34
|
-
const anonKey = getSupabaseAnonKey();
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const response = await fetch(
|
|
38
|
-
`${supabaseUrl}/functions/v1/create-checkout-session`,
|
|
39
|
-
{
|
|
40
|
-
method: "POST",
|
|
41
|
-
headers: {
|
|
42
|
-
"Content-Type": "application/json",
|
|
43
|
-
Authorization: `Bearer ${anonKey}`,
|
|
44
|
-
},
|
|
45
|
-
body: JSON.stringify({
|
|
46
|
-
priceId,
|
|
47
|
-
returnUrl: returnUrl ?? `${typeof window !== "undefined" ? window.location.origin : "http://localhost:3000"}/settings`,
|
|
48
|
-
}),
|
|
49
|
-
}
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
if (!response.ok) {
|
|
53
|
-
const data = await response.json().catch(() => ({}));
|
|
54
|
-
return { error: data.error ?? `HTTP ${response.status}` };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const data = await response.json();
|
|
58
|
-
return { url: data.url };
|
|
59
|
-
} catch (err) {
|
|
60
|
-
return { error: err instanceof Error ? err.message : "Checkout failed" };
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Create a Stripe Customer Portal session for billing management.
|
|
66
|
-
* Returns the portal URL to redirect the user to.
|
|
67
|
-
*/
|
|
68
|
-
export async function createPortalSession(
|
|
69
|
-
email: string
|
|
70
|
-
): Promise<{ url: string } | { error: string }> {
|
|
71
|
-
if (!isCloudConfigured()) {
|
|
72
|
-
return { error: "Cloud backend not configured" };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const supabaseUrl = getSupabaseUrl();
|
|
76
|
-
const anonKey = getSupabaseAnonKey();
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const response = await fetch(
|
|
80
|
-
`${supabaseUrl}/functions/v1/create-portal-session`,
|
|
81
|
-
{
|
|
82
|
-
method: "POST",
|
|
83
|
-
headers: {
|
|
84
|
-
"Content-Type": "application/json",
|
|
85
|
-
Authorization: `Bearer ${anonKey}`,
|
|
86
|
-
},
|
|
87
|
-
body: JSON.stringify({ email }),
|
|
88
|
-
}
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
if (!response.ok) {
|
|
92
|
-
const data = await response.json().catch(() => ({}));
|
|
93
|
-
return { error: data.error ?? `HTTP ${response.status}` };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const data = await response.json();
|
|
97
|
-
return { url: data.url };
|
|
98
|
-
} catch (err) {
|
|
99
|
-
return { error: err instanceof Error ? err.message : "Portal failed" };
|
|
100
|
-
}
|
|
101
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Browser-side Supabase client with session persistence.
|
|
5
|
-
*
|
|
6
|
-
* Used for auth flows (magic link sign-in) and authenticated operations
|
|
7
|
-
* (Storage uploads for cloud sync). The server-side client in
|
|
8
|
-
* supabase-client.ts is for server components and API routes.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { createClient, type SupabaseClient } from "@supabase/supabase-js";
|
|
12
|
-
import { getSupabaseUrl, getSupabaseAnonKey } from "@/lib/cloud/supabase-client";
|
|
13
|
-
|
|
14
|
-
let browserClient: SupabaseClient | null = null;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Get the browser-side Supabase client with session persistence.
|
|
18
|
-
* Sessions are stored in localStorage and auto-refreshed.
|
|
19
|
-
*/
|
|
20
|
-
export function getSupabaseBrowserClient(): SupabaseClient {
|
|
21
|
-
if (browserClient) return browserClient;
|
|
22
|
-
|
|
23
|
-
browserClient = createClient(getSupabaseUrl(), getSupabaseAnonKey(), {
|
|
24
|
-
auth: {
|
|
25
|
-
autoRefreshToken: true,
|
|
26
|
-
persistSession: true,
|
|
27
|
-
storageKey: "stagent-auth",
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
return browserClient;
|
|
32
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Supabase client singleton for cloud features.
|
|
3
|
-
*
|
|
4
|
-
* Connects to the Stagent cloud backend for license validation,
|
|
5
|
-
* marketplace, telemetry, and cloud sync. The anon key is safe to
|
|
6
|
-
* embed — Row Level Security policies protect all data.
|
|
7
|
-
*
|
|
8
|
-
* Env vars override the defaults (for self-hosted or development).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { createClient, type SupabaseClient } from "@supabase/supabase-js";
|
|
12
|
-
|
|
13
|
-
/** Production Stagent cloud backend — safe to embed (anon key, RLS-protected) */
|
|
14
|
-
const DEFAULT_SUPABASE_URL = "https://yznantjbmacbllhcyzwc.supabase.co";
|
|
15
|
-
const DEFAULT_SUPABASE_ANON_KEY =
|
|
16
|
-
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl6bmFudGpibWFjYmxsaGN5endjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI1MDg1ODMsImV4cCI6MjA4ODA4NDU4M30.i-P7MXpR1_emBjhUkzbFeSX7fgjgPDv90_wkqF7sW3Y";
|
|
17
|
-
|
|
18
|
-
/** Resolved Supabase URL (env override or production default) */
|
|
19
|
-
export function getSupabaseUrl(): string {
|
|
20
|
-
return process.env.NEXT_PUBLIC_SUPABASE_URL || DEFAULT_SUPABASE_URL;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Resolved Supabase anon key (env override or production default) */
|
|
24
|
-
export function getSupabaseAnonKey(): string {
|
|
25
|
-
return process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
let client: SupabaseClient | null = null;
|
|
29
|
-
let initialized = false;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Get the Supabase client. Uses the production Stagent backend by default.
|
|
33
|
-
* Override with NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY
|
|
34
|
-
* env vars for self-hosted or development setups.
|
|
35
|
-
*/
|
|
36
|
-
export function getSupabaseClient(): SupabaseClient | null {
|
|
37
|
-
if (initialized) return client;
|
|
38
|
-
initialized = true;
|
|
39
|
-
|
|
40
|
-
client = createClient(getSupabaseUrl(), getSupabaseAnonKey(), {
|
|
41
|
-
auth: {
|
|
42
|
-
autoRefreshToken: true,
|
|
43
|
-
persistSession: false, // Server-side — no browser session
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
return client;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Cloud is always configured — defaults to Stagent production backend.
|
|
52
|
-
* Returns false only if explicitly disabled via STAGENT_CLOUD_DISABLED=true.
|
|
53
|
-
*/
|
|
54
|
-
export function isCloudConfigured(): boolean {
|
|
55
|
-
return process.env.STAGENT_CLOUD_DISABLED !== "true";
|
|
56
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { canAccessFeature, LICENSE_FEATURES, type LicenseFeature } from "../features";
|
|
3
|
-
import type { LicenseTier } from "../tier-limits";
|
|
4
|
-
|
|
5
|
-
describe("features", () => {
|
|
6
|
-
it("community can browse marketplace", () => {
|
|
7
|
-
expect(canAccessFeature("community", "marketplace-browse")).toBe(true);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("community cannot access analytics", () => {
|
|
11
|
-
expect(canAccessFeature("community", "analytics")).toBe(false);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("community cannot access cloud sync", () => {
|
|
15
|
-
expect(canAccessFeature("community", "cloud-sync")).toBe(false);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("solo can import from marketplace", () => {
|
|
19
|
-
expect(canAccessFeature("solo", "marketplace-import")).toBe(true);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("solo cannot publish to marketplace", () => {
|
|
23
|
-
expect(canAccessFeature("solo", "marketplace-publish")).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("operator can access analytics and cloud sync", () => {
|
|
27
|
-
expect(canAccessFeature("operator", "analytics")).toBe(true);
|
|
28
|
-
expect(canAccessFeature("operator", "cloud-sync")).toBe(true);
|
|
29
|
-
expect(canAccessFeature("operator", "marketplace-publish")).toBe(true);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("scale can access everything", () => {
|
|
33
|
-
for (const feature of LICENSE_FEATURES) {
|
|
34
|
-
expect(canAccessFeature("scale", feature)).toBe(true);
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("higher tiers inherit lower tier features", () => {
|
|
39
|
-
const tiers: LicenseTier[] = ["community", "solo", "operator", "scale"];
|
|
40
|
-
for (const feature of LICENSE_FEATURES) {
|
|
41
|
-
let unlocked = false;
|
|
42
|
-
for (const tier of tiers) {
|
|
43
|
-
if (canAccessFeature(tier, feature)) {
|
|
44
|
-
unlocked = true;
|
|
45
|
-
}
|
|
46
|
-
// Once unlocked, must stay unlocked for all higher tiers
|
|
47
|
-
if (unlocked) {
|
|
48
|
-
expect(
|
|
49
|
-
canAccessFeature(tier, feature),
|
|
50
|
-
`${tier} should access ${feature} since a lower tier can`
|
|
51
|
-
).toBe(true);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { generateLicenseKey, validateLicenseKey, formatKeyInput } from "../key-format";
|
|
3
|
-
|
|
4
|
-
describe("key-format", () => {
|
|
5
|
-
describe("generateLicenseKey", () => {
|
|
6
|
-
it("generates a key in STAG-XXXX-XXXX-XXXX-XXXX format", () => {
|
|
7
|
-
const key = generateLicenseKey();
|
|
8
|
-
expect(key).toMatch(/^STAG-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}$/);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("generates keys that pass validation", () => {
|
|
12
|
-
for (let i = 0; i < 20; i++) {
|
|
13
|
-
const key = generateLicenseKey();
|
|
14
|
-
const result = validateLicenseKey(key);
|
|
15
|
-
expect(result.valid, `Key ${key} should be valid`).toBe(true);
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("generates unique keys", () => {
|
|
20
|
-
const keys = new Set<string>();
|
|
21
|
-
for (let i = 0; i < 50; i++) {
|
|
22
|
-
keys.add(generateLicenseKey());
|
|
23
|
-
}
|
|
24
|
-
expect(keys.size).toBe(50);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe("validateLicenseKey", () => {
|
|
29
|
-
it("accepts a valid generated key", () => {
|
|
30
|
-
const key = generateLicenseKey();
|
|
31
|
-
expect(validateLicenseKey(key)).toEqual({ valid: true });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("rejects empty string", () => {
|
|
35
|
-
expect(validateLicenseKey("")).toEqual({
|
|
36
|
-
valid: false,
|
|
37
|
-
error: expect.stringContaining("Invalid format"),
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("rejects wrong prefix", () => {
|
|
42
|
-
expect(validateLicenseKey("XXXX-ABCD-EFGH-JKMN-PQRS")).toEqual({
|
|
43
|
-
valid: false,
|
|
44
|
-
error: expect.stringContaining("Invalid format"),
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("rejects ambiguous characters (0, O, 1, I, L)", () => {
|
|
49
|
-
expect(validateLicenseKey("STAG-0OIL-ABCD-EFGH-JKMN")).toEqual({
|
|
50
|
-
valid: false,
|
|
51
|
-
error: expect.stringContaining("Invalid format"),
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("rejects a key with a bad checksum", () => {
|
|
56
|
-
const key = generateLicenseKey();
|
|
57
|
-
// Flip the last character
|
|
58
|
-
const bad = key.slice(0, -1) + (key.slice(-1) === "A" ? "B" : "A");
|
|
59
|
-
const result = validateLicenseKey(bad);
|
|
60
|
-
expect(result.valid).toBe(false);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("accepts lowercase input (case-insensitive)", () => {
|
|
64
|
-
const key = generateLicenseKey();
|
|
65
|
-
expect(validateLicenseKey(key.toLowerCase())).toEqual({ valid: true });
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe("formatKeyInput", () => {
|
|
70
|
-
it("adds dashes and STAG prefix", () => {
|
|
71
|
-
expect(formatKeyInput("ABCD")).toBe("STAG-ABCD");
|
|
72
|
-
expect(formatKeyInput("ABCDEFGH")).toBe("STAG-ABCD-EFGH");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("strips invalid characters", () => {
|
|
76
|
-
expect(formatKeyInput("AB!@CD")).toBe("STAG-ABCD");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("converts to uppercase", () => {
|
|
80
|
-
expect(formatKeyInput("abcd")).toBe("STAG-ABCD");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("limits to 16 characters", () => {
|
|
84
|
-
expect(formatKeyInput("ABCDEFGHJKMNPQRS")).toBe("STAG-ABCD-EFGH-JKMN-PQRS");
|
|
85
|
-
expect(formatKeyInput("ABCDEFGHJKMNPQRSTUV")).toBe("STAG-ABCD-EFGH-JKMN-PQRS");
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
});
|