@farthershore/farthershore-js 0.0.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.
Files changed (86) hide show
  1. package/README.md +137 -0
  2. package/dist/bootstrap.d.ts +6 -0
  3. package/dist/bootstrap.d.ts.map +1 -0
  4. package/dist/bootstrap.js +147 -0
  5. package/dist/bootstrap.js.map +1 -0
  6. package/dist/catalog.d.ts +81 -0
  7. package/dist/catalog.d.ts.map +1 -0
  8. package/dist/catalog.js +281 -0
  9. package/dist/catalog.js.map +1 -0
  10. package/dist/client.d.ts +33 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +56 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/config.d.ts +53 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +48 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/errors.d.ts +27 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +47 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/http.d.ts +22 -0
  23. package/dist/http.d.ts.map +1 -0
  24. package/dist/http.js +104 -0
  25. package/dist/http.js.map +1 -0
  26. package/dist/index.d.ts +16 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +18 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/react/hooks.d.ts +47 -0
  31. package/dist/react/hooks.d.ts.map +1 -0
  32. package/dist/react/hooks.js +87 -0
  33. package/dist/react/hooks.js.map +1 -0
  34. package/dist/react/index.d.ts +7 -0
  35. package/dist/react/index.d.ts.map +1 -0
  36. package/dist/react/index.js +10 -0
  37. package/dist/react/index.js.map +1 -0
  38. package/dist/react/provider.d.ts +15 -0
  39. package/dist/react/provider.d.ts.map +1 -0
  40. package/dist/react/provider.js +25 -0
  41. package/dist/react/provider.js.map +1 -0
  42. package/dist/react/use-async.d.ts +14 -0
  43. package/dist/react/use-async.d.ts.map +1 -0
  44. package/dist/react/use-async.js +39 -0
  45. package/dist/react/use-async.js.map +1 -0
  46. package/dist/resources/_shared.d.ts +6 -0
  47. package/dist/resources/_shared.d.ts.map +1 -0
  48. package/dist/resources/_shared.js +13 -0
  49. package/dist/resources/_shared.js.map +1 -0
  50. package/dist/resources/analytics.d.ts +8 -0
  51. package/dist/resources/analytics.d.ts.map +1 -0
  52. package/dist/resources/analytics.js +9 -0
  53. package/dist/resources/analytics.js.map +1 -0
  54. package/dist/resources/auth.d.ts +18 -0
  55. package/dist/resources/auth.d.ts.map +1 -0
  56. package/dist/resources/auth.js +61 -0
  57. package/dist/resources/auth.js.map +1 -0
  58. package/dist/resources/billing.d.ts +33 -0
  59. package/dist/resources/billing.d.ts.map +1 -0
  60. package/dist/resources/billing.js +61 -0
  61. package/dist/resources/billing.js.map +1 -0
  62. package/dist/resources/feature.d.ts +17 -0
  63. package/dist/resources/feature.d.ts.map +1 -0
  64. package/dist/resources/feature.js +14 -0
  65. package/dist/resources/feature.js.map +1 -0
  66. package/dist/resources/keys.d.ts +15 -0
  67. package/dist/resources/keys.d.ts.map +1 -0
  68. package/dist/resources/keys.js +55 -0
  69. package/dist/resources/keys.js.map +1 -0
  70. package/dist/resources/plans.d.ts +42 -0
  71. package/dist/resources/plans.d.ts.map +1 -0
  72. package/dist/resources/plans.js +43 -0
  73. package/dist/resources/plans.js.map +1 -0
  74. package/dist/resources/product.d.ts +7 -0
  75. package/dist/resources/product.d.ts.map +1 -0
  76. package/dist/resources/product.js +8 -0
  77. package/dist/resources/product.js.map +1 -0
  78. package/dist/resources/usage.d.ts +17 -0
  79. package/dist/resources/usage.d.ts.map +1 -0
  80. package/dist/resources/usage.js +35 -0
  81. package/dist/resources/usage.js.map +1 -0
  82. package/dist/types.d.ts +206 -0
  83. package/dist/types.d.ts.map +1 -0
  84. package/dist/types.js +12 -0
  85. package/dist/types.js.map +1 -0
  86. package/package.json +64 -0
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # @farthershore/farthershore-js
2
+
3
+ The **Farther Shore Frontend SDK** — the canonical browser integration layer
4
+ between a static frontend artifact (the default dev-portal template, generated
5
+ frontends, managed components, custom frontends) and the Farther Shore platform.
6
+
7
+ > **Internal & experimental.** Not published to npm yet (`private: true`).
8
+ > Breaking changes are expected while we validate the API against the dev portal,
9
+ > managed components, and a real builder-defined feature. The eventual public
10
+ > name is `@farthershore/farthershore-js` (the `@stripe/stripe-js` analog to the
11
+ > server-side `@farthershore/sdk`).
12
+
13
+ ## Philosophy
14
+
15
+ Frontend code never sees Core URLs, Gateway URLs, product backend URLs, auth
16
+ endpoints, or routing rules. It **expresses intent**:
17
+
18
+ ```ts
19
+ import { createFartherShoreClient } from "@farthershore/farthershore-js";
20
+
21
+ const fs = createFartherShoreClient({
22
+ coreUrl: "https://core.farthershore.com",
23
+ });
24
+
25
+ const { product } = await fs.bootstrap(); // discover the product for this host
26
+ const keys = await fs.keys.list(); // → Core (platform)
27
+ const forecast = await fs.feature("weather").json("/forecast?city=NYC"); // → Gateway
28
+ ```
29
+
30
+ The SDK decides **where** each request goes (Core for platform concerns, the
31
+ Gateway for builder features), **how** it's authenticated, and the host/env
32
+ scoping headers Core needs.
33
+
34
+ ## Surface
35
+
36
+ | Namespace | Routes to | Methods |
37
+ | -------------------------------------- | --------------------- | ----------------------------------------------------------------------- |
38
+ | `fs.bootstrap()` | Core (public resolve) | discover product/env/gateway/capabilities |
39
+ | `fs.product` | (bootstrap) | `get()` |
40
+ | `fs.auth` | Core | `getSession()`, `signIn({apiKey})` (persona), `signOut()`, `setToken()` |
41
+ | `fs.keys` | Core | `list()`, `create()`, `revoke()`, `rotate()` |
42
+ | `fs.usage` | Core | `summary()`, `events()`, ~~`timeseries()`~~ |
43
+ | `fs.billing` | Core | `subscription()`, `openBillingPortal()`, ~~`invoices()`~~ |
44
+ | `fs.analytics` | Core | ~~`requests()`~~ |
45
+ | `fs.feature(name)` / `fs.invoke(path)` | **Gateway** | `fetch(path)`, `json(path)` |
46
+
47
+ Struck-through methods have **no platform endpoint yet** and throw
48
+ `FartherShoreNotImplementedError` (the SDK never silently no-ops a missing
49
+ capability) — see Deferred.
50
+
51
+ ## Auth
52
+
53
+ - **Core** calls use the consumer **session token** as `Authorization: Bearer`.
54
+ The host app owns the session:
55
+ - _Clerk envs:_ pass `getToken` (from the Clerk browser SDK) in the config, or
56
+ call `fs.auth.setToken(token)`.
57
+ - _Persona/preview envs:_ `fs.auth.signIn({ apiKey: "fsk_test_…" })` — the SDK
58
+ exchanges the key for a session and manages the token itself.
59
+ - **Gateway** (feature) calls use the consumer **API key** (`fsk_…`):
60
+ `fs.setApiKey(key)` (or `fs.feature(name).fetch(path, { apiKey })`). The gateway
61
+ host is discovered at bootstrap (the product's `runtimeHostname`).
62
+
63
+ ## Configuration
64
+
65
+ ```ts
66
+ createFartherShoreClient({
67
+ coreUrl, // required — platform Core base URL
68
+ portalHost, // defaults to window.location.host
69
+ productId, // optional — bootstrap() discovers it
70
+ environmentId, // ephemeral/preview env scoping
71
+ organizationId, // org-owned subscriptions
72
+ gatewayUrl, // override (else derived from the product runtimeHostname)
73
+ apiKey, // consumer key for Gateway calls
74
+ getToken, // () => session bearer (Clerk) | null
75
+ fetch, // injectable (tests / SSR)
76
+ });
77
+ ```
78
+
79
+ ## React (`@farthershore/farthershore-js/react`)
80
+
81
+ The hooks live in a **subpath** of the same package — `react` is an _optional_
82
+ peer dependency, so the core stays framework-agnostic and only React apps pull it
83
+ in. (No second package to version.)
84
+
85
+ ```tsx
86
+ import { createFartherShoreClient } from "@farthershore/farthershore-js";
87
+ import {
88
+ FartherShoreProvider,
89
+ useProduct,
90
+ useApiKeys,
91
+ useUsage,
92
+ useBilling,
93
+ useSession,
94
+ useFeature,
95
+ } from "@farthershore/farthershore-js/react";
96
+
97
+ const fs = createFartherShoreClient({ coreUrl: import.meta.env.VITE_CORE_URL });
98
+
99
+ function App() {
100
+ return (
101
+ <FartherShoreProvider client={fs}>
102
+ <Portal />
103
+ </FartherShoreProvider>
104
+ );
105
+ }
106
+
107
+ function ApiKeys() {
108
+ const { data, loading, create, revoke } = useApiKeys();
109
+ // …each hook is { data, loading, error, refresh } plus its mutations.
110
+ }
111
+ ```
112
+
113
+ Hooks: `useBootstrap`, `useProduct`, `useSession` (+ `signIn`/`signOut`),
114
+ `useApiKeys` (+ `create`/`revoke`/`rotate`), `useUsage`, `useBilling` (+
115
+ `openBillingPortal`), `useFeature(name)`, plus `useFartherShore()` for the raw
116
+ client. They're thin wrappers over the client — no duplicated logic.
117
+
118
+ ## Deferred (no platform endpoint yet)
119
+
120
+ - `usage.timeseries()` — Core exposes `summary` + recent `events` only; derive a
121
+ series client-side or add a Core timeseries route.
122
+ - `billing.invoices()` — invoices are viewed through `billing.openBillingPortal()`
123
+ (Stripe-hosted); there's no JSON invoice list.
124
+ - `analytics.requests()` — no consumer analytics endpoint; closest is
125
+ `usage.events()` + audit logs.
126
+ - **Clerk browser sign-in** is host-app-driven (pass `getToken`); the SDK doesn't
127
+ bundle `@clerk/clerk-js`.
128
+ - A browser-safe **short-lived gateway token** (vs. a long-lived `fsk_` key in the
129
+ page) — the platform's `fsc_` context token is server-minted today.
130
+
131
+ ## Scripts
132
+
133
+ ```bash
134
+ pnpm --filter @farthershore/farthershore-js build # tsc → dist/
135
+ pnpm --filter @farthershore/farthershore-js test # vitest
136
+ pnpm --filter @farthershore/farthershore-js typecheck
137
+ ```
@@ -0,0 +1,6 @@
1
+ import { type ClientContext } from "./config.js";
2
+ import type { Bootstrap } from "./types.js";
3
+ /** Resolve + cache the product/env/gateway onto `ctx`. Idempotent — safe to call
4
+ * repeatedly; the client memoizes the result. */
5
+ export declare function resolveBootstrap(ctx: ClientContext): Promise<Bootstrap>;
6
+ //# sourceMappingURL=bootstrap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,KAAK,EAEV,SAAS,EASV,MAAM,YAAY,CAAC;AA8LpB;kDACkD;AAClD,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,SAAS,CAAC,CA+B7E"}
@@ -0,0 +1,147 @@
1
+ // Capability discovery. `bootstrap()` resolves the product the frontend is
2
+ // running for from the portal host — the SAME public endpoint the portal renders
3
+ // from — and caches productId / environmentId / gateway origin on the client so
4
+ // every later call knows where to go.
5
+ //
6
+ // The resolve DTO carries the product's full catalog: the plan list (the rich
7
+ // 5-knob unified billing shape — recurring fee, metered $/unit, trial,
8
+ // min/max spend, included quotas, credit grants), product-level meters, and the
9
+ // docs/legal MDX origins. `bootstrap()` surfaces ALL of it so a generated or
10
+ // managed frontend can render plans/pricing/quotas/credit/docs/legal and a
11
+ // subscribe flow — not just an account dashboard.
12
+ import { coreFetch } from "./http.js";
13
+ import { FartherShoreNotReadyError } from "./errors.js";
14
+ // ── Mappers ───────────────────────────────────────────────────────────────
15
+ function mapMeter(m) {
16
+ const key = m.key ?? m.dimension;
17
+ if (!key)
18
+ return null;
19
+ return {
20
+ key,
21
+ display: m.display ?? m.label ?? key,
22
+ ...(m.unit ? { unit: m.unit } : {}),
23
+ };
24
+ }
25
+ function mapBranding(p) {
26
+ return {
27
+ displayName: p.displayName ?? p.name,
28
+ logoUrl: p.logoUrl ?? null,
29
+ primaryColor: p.primaryColor ?? null,
30
+ description: p.description ?? null,
31
+ };
32
+ }
33
+ function mapProduct(p) {
34
+ const meters = (p.meters ?? [])
35
+ .map(mapMeter)
36
+ .filter((m) => m !== null);
37
+ return {
38
+ id: p.id,
39
+ slug: p.runtimeHostname.split(".")[0] ?? p.id,
40
+ name: p.name,
41
+ description: p.description ?? null,
42
+ branding: mapBranding(p),
43
+ gatewayHost: p.runtimeHostname,
44
+ portalHost: p.portalHostname,
45
+ docsBaseUrl: p.docsBaseUrl ?? null,
46
+ legalBaseUrl: p.legalBaseUrl ?? null,
47
+ featuredCompiledPlanId: p.featuredCompiledPlanId ?? null,
48
+ meters,
49
+ };
50
+ }
51
+ function mapEnv(e) {
52
+ return {
53
+ id: e.id,
54
+ name: e.name,
55
+ slug: e.slug,
56
+ authStrategy: e.customerAuthStrategy ?? "clerk",
57
+ branch: e.branch ?? null,
58
+ stripeMode: e.stripeMode ?? null,
59
+ };
60
+ }
61
+ function mapPlanMeter(m) {
62
+ return {
63
+ dimension: m.dimension,
64
+ price_per_unit_micros: m.price_per_unit_micros,
65
+ };
66
+ }
67
+ function mapPlanLimit(l) {
68
+ return {
69
+ dimension: l.dimension,
70
+ window: l.window.type === "named"
71
+ ? { type: "named", name: l.window.name }
72
+ : { type: "custom", seconds: l.window.seconds },
73
+ capacity: l.capacity,
74
+ ...(l.enforcement ? { enforcement: l.enforcement } : {}),
75
+ };
76
+ }
77
+ function mapPlan(p) {
78
+ return {
79
+ id: p.id,
80
+ key: p.key,
81
+ name: p.name,
82
+ description: p.description ?? null,
83
+ recurringFeeCents: p.recurring_fee_cents,
84
+ grants: p.grants ?? [],
85
+ trialDays: p.trial_days,
86
+ maxMonthlySpendCents: p.max_monthly_spend_cents ?? null,
87
+ minMonthlySpendCents: p.min_monthly_spend_cents ?? null,
88
+ meters: (p.meters ?? []).map(mapPlanMeter),
89
+ limits: (p.limits ?? []).map(mapPlanLimit),
90
+ planDetails: p.planDetails ?? [],
91
+ };
92
+ }
93
+ /** Derive coarse capability keys from the resolved product + plans so a
94
+ * managed frontend can branch on what the product actually offers (instead of
95
+ * the previously hard-coded empty array). */
96
+ function deriveCapabilities(product, plans) {
97
+ const caps = new Set();
98
+ if (plans.length > 0)
99
+ caps.add("plans");
100
+ if (plans.some((p) => p.recurringFeeCents > 0 || p.meters.length > 0)) {
101
+ caps.add("billing");
102
+ }
103
+ if (plans.some((p) => p.meters.length > 0))
104
+ caps.add("usage-metered");
105
+ if (plans.some((p) => p.limits.some((l) => l.window.type === "named" && l.window.name === "month"))) {
106
+ caps.add("usage-quota");
107
+ }
108
+ if (plans.some((p) => p.grants.length > 0 || p.trialDays > 0)) {
109
+ caps.add("credit-grants");
110
+ }
111
+ if (product.docsBaseUrl)
112
+ caps.add("docs");
113
+ if (product.legalBaseUrl)
114
+ caps.add("legal");
115
+ return [...caps];
116
+ }
117
+ /** Resolve + cache the product/env/gateway onto `ctx`. Idempotent — safe to call
118
+ * repeatedly; the client memoizes the result. */
119
+ export async function resolveBootstrap(ctx) {
120
+ if (!ctx.portalHost) {
121
+ throw new FartherShoreNotReadyError("no portalHost to bootstrap from — set config.portalHost (or run in a browser).");
122
+ }
123
+ const raw = await coreFetch(ctx, {
124
+ method: "GET",
125
+ path: "/portal/public/resolve",
126
+ query: { host: ctx.portalHost },
127
+ auth: "none",
128
+ });
129
+ const product = mapProduct(raw.product);
130
+ const environment = raw.environment ? mapEnv(raw.environment) : null;
131
+ const plans = (raw.plans ?? []).map(mapPlan);
132
+ // Cache routing facts for subsequent calls (config values still win).
133
+ ctx.productId = ctx.productId ?? product.id;
134
+ if (environment)
135
+ ctx.environmentId = ctx.environmentId ?? environment.id;
136
+ ctx.gatewayUrl = ctx.gatewayUrl ?? `https://${product.gatewayHost}`;
137
+ return {
138
+ product,
139
+ environment,
140
+ branding: product.branding,
141
+ plans,
142
+ frontendReleaseHash: raw.frontendReleaseHash ?? null,
143
+ capabilities: deriveCapabilities(product, plans),
144
+ features: [],
145
+ };
146
+ }
147
+ //# sourceMappingURL=bootstrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ import type { Grant, Meter, Plan, PlanMeter } from "./types.js";
2
+ export type PlanKind = "free" | "pay_as_you_go" | "subscription" | "hybrid" | "prepaid" | "trial" | "custom";
3
+ /**
4
+ * Classify a plan into a single canonical kind — mirrors the shared
5
+ * `classifyPlan` decision tree so this surface labels a plan identically to the
6
+ * SSR portal / web-ui / fs-admin.
7
+ */
8
+ export declare function classifyPlan(plan: Plan): PlanKind;
9
+ /** True iff the plan charges nothing at all (no fee, no meters, no prepaid, no
10
+ * trial). Free plans skip Stripe — `subscribe()` activates them directly. */
11
+ export declare function isFreePlan(plan: Plan): boolean;
12
+ /** True iff the plan is purely usage-based (no recurring fee, ≥1 meter). */
13
+ export declare function isPayAsYouGoPlan(plan: Plan): boolean;
14
+ /** Render-ready headline price for a plan card: `{ price, period }`.
15
+ * - free → `{ "$0", "forever" }`
16
+ * - pay-as-you-go → `{ "$0", "+ usage" }`
17
+ * - otherwise → `{ "$N", "/mo" }` */
18
+ export declare function formatPlanPrice(plan: Plan): {
19
+ price: string;
20
+ period: string;
21
+ };
22
+ /** Format an integer-cents value as "$X" (or "$X.YY" when fractional). */
23
+ export declare function formatCents(cents: number): string;
24
+ /**
25
+ * Format a micros-per-unit value as a dollar price string (mirrors the SSR
26
+ * portal's `formatPriceFromMicros`):
27
+ * 1_000_000 → "$1" 100_000 → "$0.10" 1_000 → "$0.001" 1 → "$0.000001"
28
+ */
29
+ export declare function formatPriceFromMicros(micros: number): string;
30
+ /** Per-meter pricing headline, e.g. "$0.001 / request". */
31
+ export declare function summarizeMeterPricing(meter: PlanMeter): string;
32
+ /** One-line minimum-spend floor summary, or null when the plan has no floor. */
33
+ export declare function formatMinimumSpend(plan: Plan): string | null;
34
+ /**
35
+ * Synthesize a short feature-bullet list for a plan card. Uses the builder's
36
+ * `planDetails` verbatim when present, else derives from the plan's monthly
37
+ * quotas + kind — mirrors the SSR portal's `synthesizeFeatures`.
38
+ */
39
+ export declare function synthesizePlanFeatures(plan: Plan): string[];
40
+ export type GrantRow = {
41
+ /** Stable React key. */
42
+ id: string;
43
+ kind: Grant["kind"];
44
+ /** Display label, e.g. "Monthly credit". */
45
+ label: string;
46
+ /** Primary value, e.g. "$50/month", "14-day trial". */
47
+ value: string;
48
+ /** Secondary line, or null. */
49
+ detail: string | null;
50
+ };
51
+ /** The effective grant list for a plan: the declared `grants[]` plus a
52
+ * synthesized `{kind:"trial"}` entry when `trialDays > 0` (de-duped). Mirrors
53
+ * the shared `getEffectiveGrants`. */
54
+ export declare function effectiveGrants(plan: Plan): Grant[];
55
+ /** Build render-ready grant rows for a plan, sorted by a stable display order. */
56
+ export declare function buildGrantRows(plan: Plan): GrantRow[];
57
+ /** Monthly included quota for a dimension on a plan, or null when none. */
58
+ export declare function quotaForDimension(plan: Plan | null, dimension: string): number | null;
59
+ /** Progress-bar percentage + a coarse status color name for `used / total`. */
60
+ export declare function computeProgressBar(used: number, total: number): {
61
+ pct: number;
62
+ status: "ok" | "warning" | "error";
63
+ };
64
+ export type MeterUsageRow = {
65
+ key: string;
66
+ label: string;
67
+ used: number;
68
+ quota: number | null;
69
+ unit?: string;
70
+ };
71
+ /**
72
+ * Combine a plan's monthly quotas with a usage summary into render-ready rows —
73
+ * one per meter that has a quota OR non-zero usage. Sorted: quota'd meters
74
+ * first (by label), then no-quota meters by usage descending. Mirrors the SSR
75
+ * portal's `buildAllMeterUsage`.
76
+ *
77
+ * `meters` is the product-level meter catalog (for labels/units); when empty,
78
+ * the summary keys are used directly.
79
+ */
80
+ export declare function buildMeterUsageRows(plan: Plan | null, summary: Record<string, number>, meters: Meter[]): MeterUsageRow[];
81
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAa,SAAS,EAAE,MAAM,YAAY,CAAC;AAS3E,MAAM,MAAM,QAAQ,GAChB,MAAM,GACN,eAAe,GACf,cAAc,GACd,QAAQ,GACR,SAAS,GACT,OAAO,GACP,QAAQ,CAAC;AAUb;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,QAAQ,CAQjD;AAED;8EAC8E;AAC9E,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAO9C;AAED,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAEpD;AAID;;;2CAG2C;AAC3C,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAO7E;AAED,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGjD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAQ5D;AAED,2DAA2D;AAC3D,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAE9D;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAI5D;AAID;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAoB3D;AAID,MAAM,MAAM,QAAQ,GAAG;IACrB,wBAAwB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACpB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,+BAA+B;IAC/B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAsBF;;uCAEuC;AACvC,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,KAAK,EAAE,CAMnD;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,QAAQ,EAAE,CAMrD;AAwED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,IAAI,GAAG,IAAI,EACjB,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAKf;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACZ;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,IAAI,GAAG,SAAS,GAAG,OAAO,CAAA;CAAE,CAIrD;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,GAAG,IAAI,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,MAAM,EAAE,KAAK,EAAE,GACd,aAAa,EAAE,CA+BjB"}
@@ -0,0 +1,281 @@
1
+ // Pure catalog/pricing display helpers — the SDK-local port of the SSR portal's
2
+ // plan classification + pricing/grant/quota formatting (dev-portal's
3
+ // classification.ts / pricing-display.ts / grant-display.ts / plan-display.ts).
4
+ //
5
+ // These keep a generated/managed frontend from re-deriving "is this Free vs
6
+ // pay-as-you-go vs $N/mo", "$0.001 / request", "$50/month credit", or "used / N
7
+ // included" by hand. All pure functions over the SDK's `Plan` type — safe to
8
+ // call from render paths.
9
+ /** The named-window name for a limit rule, or null for custom windows. */
10
+ function windowName(limit) {
11
+ return limit.window.type === "named" ? limit.window.name : null;
12
+ }
13
+ /** First one-time grant amount (cents) in the plan's grants, or 0. */
14
+ function oneTimeGrantCents(plan) {
15
+ for (const g of plan.grants) {
16
+ if (g.kind === "one_time")
17
+ return g.amount_cents;
18
+ }
19
+ return 0;
20
+ }
21
+ /**
22
+ * Classify a plan into a single canonical kind — mirrors the shared
23
+ * `classifyPlan` decision tree so this surface labels a plan identically to the
24
+ * SSR portal / web-ui / fs-admin.
25
+ */
26
+ export function classifyPlan(plan) {
27
+ if (plan.trialDays > 0)
28
+ return "trial";
29
+ if (plan.recurringFeeCents > 0) {
30
+ return plan.meters.length > 0 ? "hybrid" : "subscription";
31
+ }
32
+ if (oneTimeGrantCents(plan) > 0)
33
+ return "prepaid";
34
+ if (plan.meters.length > 0)
35
+ return "pay_as_you_go";
36
+ return "free";
37
+ }
38
+ /** True iff the plan charges nothing at all (no fee, no meters, no prepaid, no
39
+ * trial). Free plans skip Stripe — `subscribe()` activates them directly. */
40
+ export function isFreePlan(plan) {
41
+ return (plan.recurringFeeCents === 0 &&
42
+ plan.meters.length === 0 &&
43
+ oneTimeGrantCents(plan) === 0 &&
44
+ plan.trialDays === 0);
45
+ }
46
+ /** True iff the plan is purely usage-based (no recurring fee, ≥1 meter). */
47
+ export function isPayAsYouGoPlan(plan) {
48
+ return plan.recurringFeeCents === 0 && plan.meters.length > 0;
49
+ }
50
+ // ── Price formatting ─────────────────────────────────────────────────────────
51
+ /** Render-ready headline price for a plan card: `{ price, period }`.
52
+ * - free → `{ "$0", "forever" }`
53
+ * - pay-as-you-go → `{ "$0", "+ usage" }`
54
+ * - otherwise → `{ "$N", "/mo" }` */
55
+ export function formatPlanPrice(plan) {
56
+ if (isFreePlan(plan))
57
+ return { price: "$0", period: "forever" };
58
+ if (isPayAsYouGoPlan(plan))
59
+ return { price: "$0", period: "+ usage" };
60
+ return {
61
+ price: `$${(plan.recurringFeeCents / 100).toFixed(0)}`,
62
+ period: "/mo",
63
+ };
64
+ }
65
+ /** Format an integer-cents value as "$X" (or "$X.YY" when fractional). */
66
+ export function formatCents(cents) {
67
+ if (cents % 100 === 0)
68
+ return `$${(cents / 100).toFixed(0)}`;
69
+ return `$${(cents / 100).toFixed(2)}`;
70
+ }
71
+ /**
72
+ * Format a micros-per-unit value as a dollar price string (mirrors the SSR
73
+ * portal's `formatPriceFromMicros`):
74
+ * 1_000_000 → "$1" 100_000 → "$0.10" 1_000 → "$0.001" 1 → "$0.000001"
75
+ */
76
+ export function formatPriceFromMicros(micros) {
77
+ if (micros === 0)
78
+ return "Free";
79
+ const dollars = micros / 1_000_000;
80
+ if (dollars >= 1)
81
+ return `$${dollars.toFixed(2).replace(/\.00$/, "")}`;
82
+ const raw = dollars.toFixed(8);
83
+ const trimmed = raw.replace(/(\.\d*?[1-9])0+$/, "$1");
84
+ const padded = /\.\d$/.test(trimmed) ? `${trimmed}0` : trimmed;
85
+ return `$${padded}`;
86
+ }
87
+ /** Per-meter pricing headline, e.g. "$0.001 / request". */
88
+ export function summarizeMeterPricing(meter) {
89
+ return `${formatPriceFromMicros(meter.price_per_unit_micros)} / ${meter.dimension}`;
90
+ }
91
+ /** One-line minimum-spend floor summary, or null when the plan has no floor. */
92
+ export function formatMinimumSpend(plan) {
93
+ const floor = plan.minMonthlySpendCents;
94
+ if (!floor || floor <= 0)
95
+ return null;
96
+ return `${formatCents(floor)}/month minimum`;
97
+ }
98
+ // ── Feature synthesis ────────────────────────────────────────────────────────
99
+ /**
100
+ * Synthesize a short feature-bullet list for a plan card. Uses the builder's
101
+ * `planDetails` verbatim when present, else derives from the plan's monthly
102
+ * quotas + kind — mirrors the SSR portal's `synthesizeFeatures`.
103
+ */
104
+ export function synthesizePlanFeatures(plan) {
105
+ if (plan.planDetails.length > 0)
106
+ return plan.planDetails;
107
+ const features = [];
108
+ for (const rule of plan.limits) {
109
+ if (windowName(rule) === "month") {
110
+ features.push(`${rule.capacity.toLocaleString()} ${rule.dimension} / month`);
111
+ }
112
+ }
113
+ features.push("Full API access");
114
+ if (isFreePlan(plan)) {
115
+ features.push("Community support");
116
+ }
117
+ else if (isPayAsYouGoPlan(plan)) {
118
+ features.push("Usage-based billing for overages");
119
+ features.push("Dedicated support");
120
+ }
121
+ else {
122
+ features.push("Priority support");
123
+ }
124
+ return features;
125
+ }
126
+ const GRANT_KIND_LABEL = {
127
+ recurring: "Monthly credit",
128
+ one_time: "Prepaid credit",
129
+ promotional: "Promotional credit",
130
+ trial: "Free trial",
131
+ rollover: "Unused balance rollover",
132
+ top_up: "Top-up pack",
133
+ auto_recharge: "Auto-recharge",
134
+ };
135
+ const GRANT_KIND_ORDER = {
136
+ recurring: 0,
137
+ one_time: 1,
138
+ promotional: 2,
139
+ trial: 3,
140
+ rollover: 4,
141
+ top_up: 5,
142
+ auto_recharge: 6,
143
+ };
144
+ /** The effective grant list for a plan: the declared `grants[]` plus a
145
+ * synthesized `{kind:"trial"}` entry when `trialDays > 0` (de-duped). Mirrors
146
+ * the shared `getEffectiveGrants`. */
147
+ export function effectiveGrants(plan) {
148
+ const out = [...plan.grants];
149
+ if (plan.trialDays > 0 && !out.some((g) => g.kind === "trial")) {
150
+ out.push({ kind: "trial" });
151
+ }
152
+ return out;
153
+ }
154
+ /** Build render-ready grant rows for a plan, sorted by a stable display order. */
155
+ export function buildGrantRows(plan) {
156
+ const rows = effectiveGrants(plan).map((grant, idx) => formatGrantRow(grant, plan, idx));
157
+ rows.sort((a, b) => GRANT_KIND_ORDER[a.kind] - GRANT_KIND_ORDER[b.kind]);
158
+ return rows;
159
+ }
160
+ function formatGrantRow(grant, plan, idx) {
161
+ switch (grant.kind) {
162
+ case "recurring":
163
+ return {
164
+ id: `recurring-${idx}`,
165
+ kind: "recurring",
166
+ label: GRANT_KIND_LABEL.recurring,
167
+ value: `${formatCents(grant.amount_cents)}/month`,
168
+ detail: "Resets every billing period",
169
+ };
170
+ case "one_time":
171
+ return {
172
+ id: `one_time-${idx}`,
173
+ kind: "one_time",
174
+ label: GRANT_KIND_LABEL.one_time,
175
+ value: formatCents(grant.amount_cents),
176
+ detail: "One-time grant at signup",
177
+ };
178
+ case "promotional":
179
+ return {
180
+ id: `promotional-${grant.label}`,
181
+ kind: "promotional",
182
+ label: grant.label,
183
+ value: formatCents(grant.amount_cents),
184
+ detail: grant.expires_after_days
185
+ ? `Expires ${grant.expires_after_days} days after issue`
186
+ : null,
187
+ };
188
+ case "trial":
189
+ return {
190
+ id: `trial-${idx}`,
191
+ kind: "trial",
192
+ label: GRANT_KIND_LABEL.trial,
193
+ value: plan.trialDays > 0 ? `${plan.trialDays}-day trial` : "Included",
194
+ detail: null,
195
+ };
196
+ case "rollover":
197
+ return {
198
+ id: `rollover-${idx}`,
199
+ kind: "rollover",
200
+ label: GRANT_KIND_LABEL.rollover,
201
+ value: `${grant.percent}% carries over`,
202
+ detail: grant.percent === 0
203
+ ? "Unused credit expires at period end"
204
+ : grant.percent === 100
205
+ ? "Unused credit fully carries to next period"
206
+ : null,
207
+ };
208
+ case "top_up":
209
+ return {
210
+ id: `top_up-${grant.sku}`,
211
+ kind: "top_up",
212
+ label: grant.label,
213
+ value: `${formatCents(grant.price_cents)} → ${formatCents(grant.credit_cents)} credit`,
214
+ detail: `SKU: ${grant.sku}`,
215
+ };
216
+ case "auto_recharge":
217
+ return {
218
+ id: `auto_recharge-${idx}`,
219
+ kind: "auto_recharge",
220
+ label: GRANT_KIND_LABEL.auto_recharge,
221
+ value: `+${formatCents(grant.refill_cents)} per refill`,
222
+ detail: `Triggers when balance drops below ${formatCents(grant.threshold_cents)}`,
223
+ };
224
+ }
225
+ }
226
+ // ── Usage / quota rows (progress) ────────────────────────────────────────────
227
+ /** Monthly included quota for a dimension on a plan, or null when none. */
228
+ export function quotaForDimension(plan, dimension) {
229
+ const rule = plan?.limits.find((r) => r.dimension === dimension && windowName(r) === "month");
230
+ return rule?.capacity ?? null;
231
+ }
232
+ /** Progress-bar percentage + a coarse status color name for `used / total`. */
233
+ export function computeProgressBar(used, total) {
234
+ const pct = total > 0 ? Math.min(100, Math.round((used / total) * 100)) : 0;
235
+ const status = pct >= 90 ? "error" : pct >= 70 ? "warning" : "ok";
236
+ return { pct, status };
237
+ }
238
+ /**
239
+ * Combine a plan's monthly quotas with a usage summary into render-ready rows —
240
+ * one per meter that has a quota OR non-zero usage. Sorted: quota'd meters
241
+ * first (by label), then no-quota meters by usage descending. Mirrors the SSR
242
+ * portal's `buildAllMeterUsage`.
243
+ *
244
+ * `meters` is the product-level meter catalog (for labels/units); when empty,
245
+ * the summary keys are used directly.
246
+ */
247
+ export function buildMeterUsageRows(plan, summary, meters) {
248
+ // Union of catalog dimensions and any dimension present in the summary.
249
+ const byKey = new Map();
250
+ for (const m of meters)
251
+ byKey.set(m.key, m);
252
+ for (const key of Object.keys(summary)) {
253
+ if (!byKey.has(key))
254
+ byKey.set(key, { key, display: key });
255
+ }
256
+ const rows = [];
257
+ for (const meter of byKey.values()) {
258
+ const used = summary[meter.key] ?? 0;
259
+ const quota = quotaForDimension(plan, meter.key);
260
+ if (quota != null || used > 0) {
261
+ rows.push({
262
+ key: meter.key,
263
+ label: meter.display,
264
+ used,
265
+ quota,
266
+ ...(meter.unit ? { unit: meter.unit } : {}),
267
+ });
268
+ }
269
+ }
270
+ rows.sort((a, b) => {
271
+ const aHasQuota = a.quota != null && a.quota > 0 ? 0 : 1;
272
+ const bHasQuota = b.quota != null && b.quota > 0 ? 0 : 1;
273
+ if (aHasQuota !== bHasQuota)
274
+ return aHasQuota - bHasQuota;
275
+ if (aHasQuota === 0)
276
+ return a.label.localeCompare(b.label);
277
+ return b.used - a.used;
278
+ });
279
+ return rows;
280
+ }
281
+ //# sourceMappingURL=catalog.js.map