@tangle-network/agent-app 0.16.1 → 0.19.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/dist/{mcp-eZCmkgCF.d.ts → auth-BlS9GWfL.d.ts} +1 -51
- package/dist/chunk-KOG473C4.js +132 -0
- package/dist/chunk-KOG473C4.js.map +1 -0
- package/dist/design-canvas/index.d.ts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +100 -100
- package/dist/mcp-BShTlESm.d.ts +54 -0
- package/dist/model-resolution/index.d.ts +83 -0
- package/dist/model-resolution/index.js +123 -0
- package/dist/model-resolution/index.js.map +1 -0
- package/dist/platform/index.d.ts +71 -1
- package/dist/platform/index.js +55 -0
- package/dist/platform/index.js.map +1 -1
- package/dist/profile/index.d.ts +145 -0
- package/dist/profile/index.js +63 -0
- package/dist/profile/index.js.map +1 -0
- package/dist/prompt/index.d.ts +77 -0
- package/dist/prompt/index.js +22 -0
- package/dist/prompt/index.js.map +1 -0
- package/dist/run/index.d.ts +64 -0
- package/dist/run/index.js +30 -0
- package/dist/run/index.js.map +1 -0
- package/dist/runtime/index.d.ts +2 -1
- package/dist/sandbox/index.d.ts +146 -0
- package/dist/sandbox/index.js +304 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sequences/index.d.ts +2 -1
- package/dist/skills/index.d.ts +136 -0
- package/dist/skills/index.js +15 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/tools/index.d.ts +3 -2
- package/dist/web-react/index.d.ts +37 -1
- package/dist/web-react/index.js +199 -122
- package/dist/web-react/index.js.map +1 -1
- package/package.json +37 -2
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// src/model-resolution/index.ts
|
|
2
|
+
function canonicalModelId(model) {
|
|
3
|
+
if (model.id.includes("/")) return model.id;
|
|
4
|
+
const provider = model._provider ?? model.provider;
|
|
5
|
+
return provider ? `${provider}/${model.id}` : model.id;
|
|
6
|
+
}
|
|
7
|
+
function createChatModelResolution(defaults) {
|
|
8
|
+
const DEFAULT_ROUTER_MODEL = defaults.routerModel;
|
|
9
|
+
const DEFAULT_SANDBOX_OPENAI_MODEL = defaults.sandboxOpenaiModel;
|
|
10
|
+
const DEFAULT_ROUTER_BASE_URL = defaults.routerBaseUrl;
|
|
11
|
+
const allowlist = new Set(
|
|
12
|
+
[
|
|
13
|
+
DEFAULT_ROUTER_MODEL,
|
|
14
|
+
DEFAULT_SANDBOX_OPENAI_MODEL,
|
|
15
|
+
...defaults.extraAllowlist ?? []
|
|
16
|
+
].filter((model) => typeof model === "string" && model.length > 0)
|
|
17
|
+
);
|
|
18
|
+
function resolveChatModel({
|
|
19
|
+
requestedModel,
|
|
20
|
+
backend,
|
|
21
|
+
env = process.env
|
|
22
|
+
}) {
|
|
23
|
+
const selectedModel = cleanModelId(requestedModel);
|
|
24
|
+
if (selectedModel) return { backend, model: selectedModel, source: "request" };
|
|
25
|
+
if (backend === "router") {
|
|
26
|
+
const routerModel = cleanModelId(env.MODEL_NAME);
|
|
27
|
+
return {
|
|
28
|
+
backend,
|
|
29
|
+
model: routerModel ?? DEFAULT_ROUTER_MODEL,
|
|
30
|
+
source: routerModel ? "env:MODEL_NAME" : "default"
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const sandboxModel = cleanModelId(env.MODEL_NAME);
|
|
34
|
+
if (sandboxModel) return { backend, model: sandboxModel, source: "env:MODEL_NAME" };
|
|
35
|
+
const modelProvider = env.MODEL_PROVIDER ?? (env.TANGLE_API_KEY ? "openai-compat" : env.OPENAI_API_KEY ? "openai" : void 0);
|
|
36
|
+
if (modelProvider === "openai" || modelProvider === "openai-compat") {
|
|
37
|
+
return { backend, model: DEFAULT_SANDBOX_OPENAI_MODEL, source: "default" };
|
|
38
|
+
}
|
|
39
|
+
return { backend, source: "sandbox-default" };
|
|
40
|
+
}
|
|
41
|
+
async function validateChatModelId(modelId, {
|
|
42
|
+
routerBaseUrl = DEFAULT_ROUTER_BASE_URL,
|
|
43
|
+
loadModels
|
|
44
|
+
}) {
|
|
45
|
+
const cleaned = cleanModelId(modelId);
|
|
46
|
+
if (!cleaned) {
|
|
47
|
+
return { succeeded: false, error: "Model id must be a non-empty string." };
|
|
48
|
+
}
|
|
49
|
+
if (!isWellFormedModelId(cleaned)) {
|
|
50
|
+
return { succeeded: false, error: `Model id is malformed: ${cleaned}` };
|
|
51
|
+
}
|
|
52
|
+
if (allowlist.has(cleaned)) {
|
|
53
|
+
return { succeeded: true, value: cleaned };
|
|
54
|
+
}
|
|
55
|
+
if (typeof routerBaseUrl !== "string" || routerBaseUrl.length === 0) {
|
|
56
|
+
return { succeeded: false, error: "Router base URL is required to validate against the catalog." };
|
|
57
|
+
}
|
|
58
|
+
let catalog;
|
|
59
|
+
try {
|
|
60
|
+
catalog = await loadModels(routerBaseUrl);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
63
|
+
return { succeeded: false, error: `Could not validate model catalog: ${message}` };
|
|
64
|
+
}
|
|
65
|
+
const ids = new Set(catalog.flatMap(catalogIdsForModel));
|
|
66
|
+
if (ids.has(cleaned)) {
|
|
67
|
+
return { succeeded: true, value: cleaned };
|
|
68
|
+
}
|
|
69
|
+
if (!cleaned.includes("/")) {
|
|
70
|
+
const canonicalBySuffix = /* @__PURE__ */ new Map();
|
|
71
|
+
for (const model of catalog) {
|
|
72
|
+
const canonical = canonicalModelIdOrUndefined(model);
|
|
73
|
+
if (!canonical || !canonical.includes("/")) continue;
|
|
74
|
+
const suffix = canonical.split("/").slice(1).join("/");
|
|
75
|
+
const entries = canonicalBySuffix.get(suffix);
|
|
76
|
+
if (entries) entries.push(canonical);
|
|
77
|
+
else canonicalBySuffix.set(suffix, [canonical]);
|
|
78
|
+
}
|
|
79
|
+
const matches = canonicalBySuffix.get(cleaned);
|
|
80
|
+
const only = matches && matches.length === 1 ? matches[0] : void 0;
|
|
81
|
+
if (only) {
|
|
82
|
+
return { succeeded: true, value: only };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { succeeded: false, error: `Model is not available: ${cleaned}` };
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
resolveChatModel,
|
|
89
|
+
validateChatModelId,
|
|
90
|
+
DEFAULT_ROUTER_MODEL,
|
|
91
|
+
DEFAULT_SANDBOX_OPENAI_MODEL,
|
|
92
|
+
DEFAULT_ROUTER_BASE_URL
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function cleanModelId(value) {
|
|
96
|
+
if (typeof value !== "string") return void 0;
|
|
97
|
+
const trimmed = value.trim();
|
|
98
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
99
|
+
}
|
|
100
|
+
function isWellFormedModelId(modelId) {
|
|
101
|
+
if (modelId.length > 200) return false;
|
|
102
|
+
return /^[A-Za-z0-9._/@:-]+$/.test(modelId);
|
|
103
|
+
}
|
|
104
|
+
function catalogIdsForModel(model) {
|
|
105
|
+
const ids = /* @__PURE__ */ new Set();
|
|
106
|
+
if (typeof model.id === "string" && model.id.trim()) ids.add(model.id.trim());
|
|
107
|
+
if (typeof model.id === "string" && model.id.trim() && !model.id.includes("/")) {
|
|
108
|
+
const canonical = canonicalModelId(model);
|
|
109
|
+
if (canonical.includes("/")) ids.add(canonical);
|
|
110
|
+
}
|
|
111
|
+
return [...ids];
|
|
112
|
+
}
|
|
113
|
+
function canonicalModelIdOrUndefined(model) {
|
|
114
|
+
if (typeof model.id !== "string" || !model.id.trim()) return void 0;
|
|
115
|
+
return canonicalModelId(model);
|
|
116
|
+
}
|
|
117
|
+
export {
|
|
118
|
+
catalogIdsForModel,
|
|
119
|
+
cleanModelId,
|
|
120
|
+
createChatModelResolution,
|
|
121
|
+
isWellFormedModelId
|
|
122
|
+
};
|
|
123
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/model-resolution/index.ts"],"sourcesContent":["/**\n * Chat-time model resolution: a precedence resolver and a fail-closed catalog\n * validator that sit on top of a product's boot-time model config.\n *\n * `resolveChatModel` picks the model id for a chat turn by precedence:\n * request id > env MODEL_NAME > provider default > sandbox default.\n *\n * `validateChatModelId` is the fail-closed gate: it returns a typed outcome and\n * accepts an id only if it is in the constructed allowlist OR served by the live\n * router catalog (loaded through an injected boundary). A bare id with no\n * provider prefix resolves to its canonical id only when the suffix is unique\n * across the catalog, so an ambiguous suffix is rejected rather than silently\n * assigned a provider.\n *\n * The product injects one value — `modelDefaults` — and supplies the catalog\n * loader per call. `ModelInfo` is the router /v1/models wire shape and\n * `canonicalModelId` the bare->prefixed id helper, both defined locally so this\n * engine module carries no UI-package coupling.\n */\n\n/** The router /v1/models entry shape this module reads. Minimal on purpose. */\nexport interface ModelInfo {\n id: string\n name?: string\n _provider?: string\n provider?: string\n}\n\n/** Canonical (provider-prefixed) id for a catalog entry: pass through an id that\n * already carries a provider, else prefix the entry's provider when present. */\nfunction canonicalModelId(model: ModelInfo): string {\n if (model.id.includes('/')) return model.id\n const provider = model._provider ?? model.provider\n return provider ? `${provider}/${model.id}` : model.id\n}\n\n/** Which execution path the chat turn runs on. Product-supplied per turn. */\nexport type ChatBackend = 'router' | 'sandbox'\n\nexport type ChatModelSource =\n | 'request'\n | 'env:MODEL_NAME'\n | 'default'\n | 'sandbox-default'\n\nexport interface ResolvedChatModel {\n backend: ChatBackend\n model?: string\n source: ChatModelSource\n}\n\nexport interface ChatModelValidationSuccess {\n succeeded: true\n value: string\n}\n\nexport interface ChatModelValidationFailure {\n succeeded: false\n error: string\n}\n\nexport type ChatModelValidationResult = ChatModelValidationSuccess | ChatModelValidationFailure\n\n/** The catalog-fetch boundary: maps a router base URL to the raw model list. */\nexport type LoadModels = (routerBaseUrl: string) => Promise<ModelInfo[]>\n\n/**\n * The single product-injected seam.\n *\n * - `routerModel` / `sandboxOpenaiModel`: the two `DEFAULT_*` ids used by the\n * precedence ladder and seeded into the allowlist.\n * - `routerBaseUrl`: catalog endpoint base; overridable per validate call.\n * - `extraAllowlist`: additional ids accepted without a catalog round-trip.\n */\nexport interface ChatModelDefaults {\n routerModel: string\n sandboxOpenaiModel: string\n routerBaseUrl?: string\n extraAllowlist?: string[]\n}\n\nexport interface ResolveChatModelOptions {\n requestedModel?: string\n backend: ChatBackend\n /** Env to read (defaults to process.env). Inject for non-node runtimes. */\n env?: Record<string, string | undefined>\n}\n\nexport interface ValidateChatModelIdOptions {\n routerBaseUrl?: string\n /** Catalog loader. No default body is baked in; the consumer supplies it. */\n loadModels: LoadModels\n}\n\nexport interface ChatModelResolution {\n resolveChatModel: (options: ResolveChatModelOptions) => ResolvedChatModel\n validateChatModelId: (\n modelId: unknown,\n options: ValidateChatModelIdOptions,\n ) => Promise<ChatModelValidationResult>\n DEFAULT_ROUTER_MODEL: string\n DEFAULT_SANDBOX_OPENAI_MODEL: string\n DEFAULT_ROUTER_BASE_URL?: string\n}\n\nexport function createChatModelResolution(defaults: ChatModelDefaults): ChatModelResolution {\n const DEFAULT_ROUTER_MODEL = defaults.routerModel\n const DEFAULT_SANDBOX_OPENAI_MODEL = defaults.sandboxOpenaiModel\n const DEFAULT_ROUTER_BASE_URL = defaults.routerBaseUrl\n\n const allowlist = new Set(\n [\n DEFAULT_ROUTER_MODEL,\n DEFAULT_SANDBOX_OPENAI_MODEL,\n ...(defaults.extraAllowlist ?? []),\n ].filter((model): model is string => typeof model === 'string' && model.length > 0),\n )\n\n function resolveChatModel({\n requestedModel,\n backend,\n env = process.env,\n }: ResolveChatModelOptions): ResolvedChatModel {\n const selectedModel = cleanModelId(requestedModel)\n if (selectedModel) return { backend, model: selectedModel, source: 'request' }\n\n if (backend === 'router') {\n const routerModel = cleanModelId(env.MODEL_NAME)\n return {\n backend,\n model: routerModel ?? DEFAULT_ROUTER_MODEL,\n source: routerModel ? 'env:MODEL_NAME' : 'default',\n }\n }\n\n const sandboxModel = cleanModelId(env.MODEL_NAME)\n if (sandboxModel) return { backend, model: sandboxModel, source: 'env:MODEL_NAME' }\n\n const modelProvider = env.MODEL_PROVIDER\n ?? (env.TANGLE_API_KEY ? 'openai-compat' : env.OPENAI_API_KEY ? 'openai' : undefined)\n if (modelProvider === 'openai' || modelProvider === 'openai-compat') {\n return { backend, model: DEFAULT_SANDBOX_OPENAI_MODEL, source: 'default' }\n }\n\n return { backend, source: 'sandbox-default' }\n }\n\n async function validateChatModelId(\n modelId: unknown,\n {\n routerBaseUrl = DEFAULT_ROUTER_BASE_URL,\n loadModels,\n }: ValidateChatModelIdOptions,\n ): Promise<ChatModelValidationResult> {\n const cleaned = cleanModelId(modelId)\n if (!cleaned) {\n return { succeeded: false, error: 'Model id must be a non-empty string.' }\n }\n if (!isWellFormedModelId(cleaned)) {\n return { succeeded: false, error: `Model id is malformed: ${cleaned}` }\n }\n if (allowlist.has(cleaned)) {\n return { succeeded: true, value: cleaned }\n }\n if (typeof routerBaseUrl !== 'string' || routerBaseUrl.length === 0) {\n return { succeeded: false, error: 'Router base URL is required to validate against the catalog.' }\n }\n\n let catalog: ModelInfo[]\n try {\n catalog = await loadModels(routerBaseUrl)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n return { succeeded: false, error: `Could not validate model catalog: ${message}` }\n }\n\n // Exact match against any id the catalog serves (canonical or bare).\n const ids = new Set(catalog.flatMap(catalogIdsForModel))\n if (ids.has(cleaned)) {\n return { succeeded: true, value: cleaned }\n }\n\n // A bare request id (no provider prefix) may name a model the catalog only\n // serves under a provider-prefixed id (e.g. request \"gpt-5\" -> catalog\n // \"openai/gpt-5\"). Resolve it to the canonical id the router serves, but only\n // when the bare suffix is unique across the catalog -- an ambiguous suffix\n // (e.g. \"openai/x\" vs \"vertex/x\") stays rejected so we never silently pick a\n // provider for the caller.\n if (!cleaned.includes('/')) {\n const canonicalBySuffix = new Map<string, string[]>()\n for (const model of catalog) {\n const canonical = canonicalModelIdOrUndefined(model)\n if (!canonical || !canonical.includes('/')) continue\n const suffix = canonical.split('/').slice(1).join('/')\n const entries = canonicalBySuffix.get(suffix)\n if (entries) entries.push(canonical)\n else canonicalBySuffix.set(suffix, [canonical])\n }\n const matches = canonicalBySuffix.get(cleaned)\n const only = matches && matches.length === 1 ? matches[0] : undefined\n if (only) {\n return { succeeded: true, value: only }\n }\n }\n\n return { succeeded: false, error: `Model is not available: ${cleaned}` }\n }\n\n return {\n resolveChatModel,\n validateChatModelId,\n DEFAULT_ROUTER_MODEL,\n DEFAULT_SANDBOX_OPENAI_MODEL,\n DEFAULT_ROUTER_BASE_URL,\n }\n}\n\nexport function cleanModelId(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nexport function isWellFormedModelId(modelId: string): boolean {\n if (modelId.length > 200) return false\n return /^[A-Za-z0-9._/@:-]+$/.test(modelId)\n}\n\nexport function catalogIdsForModel(model: ModelInfo): string[] {\n const ids = new Set<string>()\n if (typeof model.id === 'string' && model.id.trim()) ids.add(model.id.trim())\n\n if (typeof model.id === 'string' && model.id.trim() && !model.id.includes('/')) {\n const canonical = canonicalModelId(model)\n if (canonical.includes('/')) ids.add(canonical)\n }\n\n // The bare suffix of a provider-prefixed id (e.g. \"openai/gpt-5\" -> \"gpt-5\")\n // is NOT added here: a bare request id resolves to its canonical id only\n // through the uniqueness-gated path in validateChatModelId, so an ambiguous\n // suffix never slips through as an exact match.\n return [...ids]\n}\n\n/** The canonical id for a catalog entry, or undefined when the entry has no id. */\nfunction canonicalModelIdOrUndefined(model: ModelInfo): string | undefined {\n if (typeof model.id !== 'string' || !model.id.trim()) return undefined\n return canonicalModelId(model)\n}\n"],"mappings":"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","names":[]}
|
package/dist/platform/index.d.ts
CHANGED
|
@@ -325,6 +325,30 @@ interface PlatformUsageProductRow {
|
|
|
325
325
|
totalSpent: number;
|
|
326
326
|
count: number;
|
|
327
327
|
}
|
|
328
|
+
/** Lifecycle of a per-product seat subscription, mirroring the Stripe states
|
|
329
|
+
* the platform persists. 'none' = the user has never held this seat. */
|
|
330
|
+
type SeatStatus = 'none' | 'active' | 'trialing' | 'past_due' | 'canceled';
|
|
331
|
+
/**
|
|
332
|
+
* Per-product entitlement snapshot from the platform — the single read that
|
|
333
|
+
* tells a product whether to show its workspace or the seat paywall. Shape
|
|
334
|
+
* matches `GET /v1/billing/product-entitlement?product=<id>`.
|
|
335
|
+
*
|
|
336
|
+
* `hasSeat` and `onFreeTier` are computed platform-side from the raw seat row
|
|
337
|
+
* + cumulative spend so the gate is identical across all five products:
|
|
338
|
+
* - `hasSeat` — an active/trialing seat whose period has not lapsed.
|
|
339
|
+
* - `onFreeTier` — no active seat AND cumulative spend below the free cap
|
|
340
|
+
* ($2 / 200¢ lifetime). Keys off lifetime spend, not wallet
|
|
341
|
+
* balance, so a router top-up never re-opens free access.
|
|
342
|
+
*/
|
|
343
|
+
interface ProductEntitlement {
|
|
344
|
+
seatStatus: SeatStatus;
|
|
345
|
+
/** ISO timestamp the active seat's paid period runs until; null when none. */
|
|
346
|
+
currentPeriodEnd: string | null;
|
|
347
|
+
/** Cumulative inference spend across the whole suite, in dollars. */
|
|
348
|
+
lifetimeSpentUsd: number;
|
|
349
|
+
hasSeat: boolean;
|
|
350
|
+
onFreeTier: boolean;
|
|
351
|
+
}
|
|
328
352
|
interface PlatformBillingHttp {
|
|
329
353
|
/** GET /v1/plans/current (user bearer). */
|
|
330
354
|
getSubscription(userApiKey: string): Promise<PlatformSubscriptionInfo>;
|
|
@@ -332,6 +356,8 @@ interface PlatformBillingHttp {
|
|
|
332
356
|
getBalance(userApiKey: string): Promise<PlatformBalanceSnapshot>;
|
|
333
357
|
/** GET /v1/billing/usage (user bearer). */
|
|
334
358
|
getUsageByProduct(userApiKey: string): Promise<PlatformUsageProductRow[]>;
|
|
359
|
+
/** GET /v1/billing/product-entitlement?product=<id> (user bearer). */
|
|
360
|
+
getProductEntitlement(userApiKey: string, productId: string): Promise<ProductEntitlement>;
|
|
335
361
|
/** POST /v1/billing/deduct (service token). */
|
|
336
362
|
deduct(input: {
|
|
337
363
|
platformUserId: string;
|
|
@@ -342,8 +368,17 @@ interface PlatformBillingHttp {
|
|
|
342
368
|
}): Promise<void>;
|
|
343
369
|
/** Absolute URL of the platform's billing-management surface. */
|
|
344
370
|
billingUrl(): string;
|
|
371
|
+
/** Absolute URL of the $100/mo seat checkout for `productId`. */
|
|
372
|
+
seatCheckoutUrl(productId: string): string;
|
|
345
373
|
}
|
|
346
374
|
declare function createPlatformBillingHttp(opts: PlatformBillingHttpOptions): PlatformBillingHttp;
|
|
375
|
+
/**
|
|
376
|
+
* Platform Stripe checkout URL for a product's $100/mo seat. One shared price
|
|
377
|
+
* carries `metadata.productId`; the platform distinguishes the product from
|
|
378
|
+
* the `product` query param (not five distinct prices). Mirrors the
|
|
379
|
+
* `billingUrl()` shape — a deterministic platform-rooted URL, no network call.
|
|
380
|
+
*/
|
|
381
|
+
declare function seatCheckoutUrl(baseUrl: string, productId: string): string;
|
|
347
382
|
interface TangleTierPolicy {
|
|
348
383
|
concurrency: number;
|
|
349
384
|
overageAllowed: boolean;
|
|
@@ -364,6 +399,41 @@ interface TangleTierState {
|
|
|
364
399
|
* on the billable path choose their posture explicitly.
|
|
365
400
|
*/
|
|
366
401
|
declare function readTangleTierState(http: PlatformBillingHttp, userApiKey: string | null | undefined, policy?: Record<TanglePlanTier, TangleTierPolicy>): Promise<TangleTierState>;
|
|
402
|
+
/** Lifetime free-tier cap: $2 (200¢) cumulative inference spend, expressed in
|
|
403
|
+
* dollars. Free product access ends once cumulative spend crosses this. */
|
|
404
|
+
declare const FREE_TIER_SPEND_CAP_USD = 2;
|
|
405
|
+
/**
|
|
406
|
+
* Default name of the per-app feature flag gating seat billing. While OFF the
|
|
407
|
+
* entitlement read is skipped and access fails OPEN (entitled) so nothing
|
|
408
|
+
* changes live until a product flips the flag.
|
|
409
|
+
*/
|
|
410
|
+
declare const DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR = "SEAT_BILLING_ENABLED";
|
|
411
|
+
interface SeatBillingFlagOptions {
|
|
412
|
+
env?: Record<string, string | undefined>;
|
|
413
|
+
/** Override the flag name; default {@link DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR}. */
|
|
414
|
+
flagEnvVar?: string;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Seat billing is OFF unless the flag is explicitly truthy ('true'/'1'/'on'/
|
|
418
|
+
* 'enabled'). Default OFF — pre-rollout, the paywall never engages. Returns
|
|
419
|
+
* false when no env is available (browser bundles) so the client stays
|
|
420
|
+
* fail-open there too.
|
|
421
|
+
*/
|
|
422
|
+
declare function isSeatBillingEnabled(opts?: SeatBillingFlagOptions): boolean;
|
|
423
|
+
/**
|
|
424
|
+
* Read a user's entitlement for one product. Fails OPEN: an absent key,
|
|
425
|
+
* disabled flag, or unreachable seat endpoint all return a permissive snapshot
|
|
426
|
+
* (`hasSeat: true`) so consumers never break pre-rollout. The platform owns the
|
|
427
|
+
* `hasSeat`/`onFreeTier` computation; this client only transports + degrades
|
|
428
|
+
* safely.
|
|
429
|
+
*
|
|
430
|
+
* @param flag — pass {@link isSeatBillingEnabled} (or your own boolean) so the
|
|
431
|
+
* product owns when the gate engages. When false, no network call is made.
|
|
432
|
+
*/
|
|
433
|
+
declare function getProductEntitlement(http: Pick<PlatformBillingHttp, 'getProductEntitlement'>, userApiKey: string | null | undefined, productId: string, flag?: boolean): Promise<ProductEntitlement>;
|
|
434
|
+
/** Entitled = holds an active seat OR is still inside the free tier. The one
|
|
435
|
+
* predicate all five products gate on. */
|
|
436
|
+
declare function isProductEntitled(ent: ProductEntitlement): boolean;
|
|
367
437
|
interface PlatformIdentityStore {
|
|
368
438
|
resolveIdentity(userId: string): Promise<PlatformIdentity | null>;
|
|
369
439
|
}
|
|
@@ -429,4 +499,4 @@ interface AssertBillableBalanceOptions {
|
|
|
429
499
|
*/
|
|
430
500
|
declare function assertBillableBalance(state: BillableBalanceState, opts?: AssertBillableBalanceOptions): void;
|
|
431
501
|
|
|
432
|
-
export { type AdminGuardOptions, type AssertBillableBalanceOptions, type AuthGuard, type AuthGuardOptions, type BetterAuthSessionCookieMinterOptions, type BetterAuthSessionCookieSource, type BillableBalanceState, DEFAULT_TANGLE_TIER_POLICY, type HubClientLike, type HubProxyContext, type HubProxyRouteArgs, type HubProxyRoutes, type PlatformBalanceSnapshot, type PlatformBillingHttp, PlatformBillingHttpError, type PlatformBillingHttpOptions, type PlatformIdentityStore, type PlatformSubscriptionInfo, type PlatformUsageProductRow, type SsoStateConfig, TangleBearerMissingError, type TanglePlanTier, type TangleSsoAccountStore, type TangleSsoAuthClient, type TangleSsoExchangeResult, type TangleSsoHandlerOptions, type TangleSsoHandlers, type TangleSsoSessionCookieArgs, TangleSsoUserCreateError, type TangleTierPolicy, type TangleTierState, assertBillableBalance, createAdminGuard, createAuthGuard, createBetterAuthSessionCookieMinter, createHubProxyRoutes, createPlatformBillingHttp, createSignedSsoState, createTanglePlatformBillingClient, createTangleSsoHandlers, isPlatformBillingHttpError, isPlatformHubErrorLike, isTangleBearerMissingError, normalizeTanglePlanTier, parseAdminEmails, readTangleTierState, signSessionCookieValue, verifySignedSsoState };
|
|
502
|
+
export { type AdminGuardOptions, type AssertBillableBalanceOptions, type AuthGuard, type AuthGuardOptions, type BetterAuthSessionCookieMinterOptions, type BetterAuthSessionCookieSource, type BillableBalanceState, DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR, DEFAULT_TANGLE_TIER_POLICY, FREE_TIER_SPEND_CAP_USD, type HubClientLike, type HubProxyContext, type HubProxyRouteArgs, type HubProxyRoutes, type PlatformBalanceSnapshot, type PlatformBillingHttp, PlatformBillingHttpError, type PlatformBillingHttpOptions, type PlatformIdentityStore, type PlatformSubscriptionInfo, type PlatformUsageProductRow, type ProductEntitlement, type SeatBillingFlagOptions, type SeatStatus, type SsoStateConfig, TangleBearerMissingError, type TanglePlanTier, type TangleSsoAccountStore, type TangleSsoAuthClient, type TangleSsoExchangeResult, type TangleSsoHandlerOptions, type TangleSsoHandlers, type TangleSsoSessionCookieArgs, TangleSsoUserCreateError, type TangleTierPolicy, type TangleTierState, assertBillableBalance, createAdminGuard, createAuthGuard, createBetterAuthSessionCookieMinter, createHubProxyRoutes, createPlatformBillingHttp, createSignedSsoState, createTanglePlatformBillingClient, createTangleSsoHandlers, getProductEntitlement, isPlatformBillingHttpError, isPlatformHubErrorLike, isProductEntitled, isSeatBillingEnabled, isTangleBearerMissingError, normalizeTanglePlanTier, parseAdminEmails, readTangleTierState, seatCheckoutUrl, signSessionCookieValue, verifySignedSsoState };
|
package/dist/platform/index.js
CHANGED
|
@@ -376,6 +376,20 @@ function createPlatformBillingHttp(opts) {
|
|
|
376
376
|
count: row.count ?? 0
|
|
377
377
|
}));
|
|
378
378
|
},
|
|
379
|
+
async getProductEntitlement(userApiKey, productId) {
|
|
380
|
+
const slug = encodeURIComponent(productId);
|
|
381
|
+
const body = await userRead(userApiKey, `/v1/billing/product-entitlement?product=${slug}`);
|
|
382
|
+
const data = body.data ?? {};
|
|
383
|
+
const hasSeat = data.hasSeat === true;
|
|
384
|
+
return {
|
|
385
|
+
seatStatus: data.seatStatus ?? "none",
|
|
386
|
+
currentPeriodEnd: data.currentPeriodEnd ?? null,
|
|
387
|
+
lifetimeSpentUsd: data.lifetimeSpentUsd ?? 0,
|
|
388
|
+
hasSeat,
|
|
389
|
+
// Free access only when there is no seat AND the platform says so.
|
|
390
|
+
onFreeTier: !hasSeat && data.onFreeTier === true
|
|
391
|
+
};
|
|
392
|
+
},
|
|
379
393
|
async deduct(input) {
|
|
380
394
|
const headers = new Headers();
|
|
381
395
|
headers.set("Authorization", `Bearer ${resolveServiceToken()}`);
|
|
@@ -395,9 +409,16 @@ function createPlatformBillingHttp(opts) {
|
|
|
395
409
|
},
|
|
396
410
|
billingUrl() {
|
|
397
411
|
return `${baseUrl}/app/billing`;
|
|
412
|
+
},
|
|
413
|
+
seatCheckoutUrl(productId) {
|
|
414
|
+
return seatCheckoutUrl(baseUrl, productId);
|
|
398
415
|
}
|
|
399
416
|
};
|
|
400
417
|
}
|
|
418
|
+
function seatCheckoutUrl(baseUrl, productId) {
|
|
419
|
+
const root = baseUrl.replace(/\/+$/, "");
|
|
420
|
+
return `${root}/app/billing/seat/checkout?product=${encodeURIComponent(productId)}`;
|
|
421
|
+
}
|
|
401
422
|
var DEFAULT_TANGLE_TIER_POLICY = {
|
|
402
423
|
free: { concurrency: 1, overageAllowed: false },
|
|
403
424
|
pro: { concurrency: Number.POSITIVE_INFINITY, overageAllowed: true },
|
|
@@ -425,6 +446,34 @@ async function readTangleTierState(http, userApiKey, policy = DEFAULT_TANGLE_TIE
|
|
|
425
446
|
...policy[subscription.tier]
|
|
426
447
|
};
|
|
427
448
|
}
|
|
449
|
+
var FREE_TIER_SPEND_CAP_USD = 2;
|
|
450
|
+
var DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR = "SEAT_BILLING_ENABLED";
|
|
451
|
+
function isSeatBillingEnabled(opts = {}) {
|
|
452
|
+
const env = opts.env ?? (typeof process !== "undefined" ? process.env : void 0);
|
|
453
|
+
if (!env) return false;
|
|
454
|
+
const flag = env[opts.flagEnvVar ?? DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR]?.trim().toLowerCase();
|
|
455
|
+
return flag === "true" || flag === "1" || flag === "on" || flag === "enabled";
|
|
456
|
+
}
|
|
457
|
+
async function getProductEntitlement(http, userApiKey, productId, flag = true) {
|
|
458
|
+
if (!flag || !userApiKey) return failOpenEntitlement();
|
|
459
|
+
try {
|
|
460
|
+
return await http.getProductEntitlement(userApiKey, productId);
|
|
461
|
+
} catch {
|
|
462
|
+
return failOpenEntitlement();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function failOpenEntitlement() {
|
|
466
|
+
return {
|
|
467
|
+
seatStatus: "active",
|
|
468
|
+
currentPeriodEnd: null,
|
|
469
|
+
lifetimeSpentUsd: 0,
|
|
470
|
+
hasSeat: true,
|
|
471
|
+
onFreeTier: false
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function isProductEntitled(ent) {
|
|
475
|
+
return ent.hasSeat || ent.onFreeTier;
|
|
476
|
+
}
|
|
428
477
|
function createTanglePlatformBillingClient(http, identity) {
|
|
429
478
|
return {
|
|
430
479
|
resolveIdentity: (userId) => identity.resolveIdentity(userId),
|
|
@@ -484,7 +533,9 @@ function assertBillableBalance(state, opts = {}) {
|
|
|
484
533
|
);
|
|
485
534
|
}
|
|
486
535
|
export {
|
|
536
|
+
DEFAULT_SEAT_BILLING_ENABLED_ENV_VAR,
|
|
487
537
|
DEFAULT_TANGLE_TIER_POLICY,
|
|
538
|
+
FREE_TIER_SPEND_CAP_USD,
|
|
488
539
|
PlatformBillingHttpError,
|
|
489
540
|
TangleBearerMissingError,
|
|
490
541
|
TangleSsoUserCreateError,
|
|
@@ -497,12 +548,16 @@ export {
|
|
|
497
548
|
createSignedSsoState,
|
|
498
549
|
createTanglePlatformBillingClient,
|
|
499
550
|
createTangleSsoHandlers,
|
|
551
|
+
getProductEntitlement,
|
|
500
552
|
isPlatformBillingHttpError,
|
|
501
553
|
isPlatformHubErrorLike,
|
|
554
|
+
isProductEntitled,
|
|
555
|
+
isSeatBillingEnabled,
|
|
502
556
|
isTangleBearerMissingError,
|
|
503
557
|
normalizeTanglePlanTier,
|
|
504
558
|
parseAdminEmails,
|
|
505
559
|
readTangleTierState,
|
|
560
|
+
seatCheckoutUrl,
|
|
506
561
|
signSessionCookieValue,
|
|
507
562
|
verifySignedSsoState
|
|
508
563
|
};
|