@comfyorg/billing-contract 1.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Comfy Org
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The versioned URL contract every Comfy product uses to hand a customer to
3
+ * the hosted billing app, and that the app reads back on arrival.
4
+ *
5
+ * The entry URL is routing input and nothing more. It never carries a token,
6
+ * a JWT, a payment-provider secret, an authoritative price, or a caller-chosen
7
+ * return URL: a product states which billing screen it wants, which product is
8
+ * asking, and which registered destination the customer should land on
9
+ * afterwards. Billing resolves everything else against its own environment and
10
+ * the signed-in session, so a workspace id here is a requested scope and never
11
+ * an authorization.
12
+ *
13
+ * Route and parameter names are provisional while the hosted app is behind a
14
+ * flag. `BILLING_CONTRACT_VERSION` is what lets them change without stranding
15
+ * a product that is already deployed against the older shape.
16
+ *
17
+ * Whether a payment renders embedded or hosted is not decided here — that is
18
+ * the SDK core's `hostedDestination` port. This package stays dependency-free
19
+ * and framework-free so a product, the billing app, and a future non-Vue host
20
+ * can share exactly one definition of the route surface.
21
+ */
22
+ export declare const BILLING_CONTRACT_VERSION = "v1";
23
+ export declare const BILLING_INTENTS: readonly ["pricing", "checkout", "subscription", "payment-methods", "invoices", "result"];
24
+ export type BillingIntent = (typeof BILLING_INTENTS)[number];
25
+ export declare function isBillingIntent(value: string | null | undefined): value is BillingIntent;
26
+ /** The route an intent occupies, e.g. `/v1/checkout`. */
27
+ export declare function billingIntentPath(intent: BillingIntent): string;
28
+ /** The surfaces allowed to originate a billing trip. */
29
+ export declare const BILLING_PRODUCTS: readonly ["comfyui", "platform", "workshop", "models"];
30
+ export type BillingProduct = (typeof BILLING_PRODUCTS)[number];
31
+ export declare function isBillingProduct(value: string | null | undefined): value is BillingProduct;
32
+ /**
33
+ * Which deployment family a return destination belongs to. The backends run
34
+ * three of them, not two, and pair each origin with exactly one family, so a
35
+ * product resolves its return target in the same family its session was
36
+ * minted against.
37
+ *
38
+ * A host picks its family by origin, never by the label on its own build: the
39
+ * cloud app's "staging" distribution points at `testcloud.comfy.org`
40
+ * (`STAGING_CLOUD_BASE_URL` in `src/config/comfyApi.ts`) and therefore maps to
41
+ * `test` here, while `staging` means `stagingcloud.comfy.org`.
42
+ */
43
+ export type BillingEnvironment = 'production' | 'staging' | 'test';
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The versioned URL contract every Comfy product uses to hand a customer to
3
+ * the hosted billing app, and that the app reads back on arrival.
4
+ *
5
+ * The entry URL is routing input and nothing more. It never carries a token,
6
+ * a JWT, a payment-provider secret, an authoritative price, or a caller-chosen
7
+ * return URL: a product states which billing screen it wants, which product is
8
+ * asking, and which registered destination the customer should land on
9
+ * afterwards. Billing resolves everything else against its own environment and
10
+ * the signed-in session, so a workspace id here is a requested scope and never
11
+ * an authorization.
12
+ *
13
+ * Route and parameter names are provisional while the hosted app is behind a
14
+ * flag. `BILLING_CONTRACT_VERSION` is what lets them change without stranding
15
+ * a product that is already deployed against the older shape.
16
+ *
17
+ * Whether a payment renders embedded or hosted is not decided here — that is
18
+ * the SDK core's `hostedDestination` port. This package stays dependency-free
19
+ * and framework-free so a product, the billing app, and a future non-Vue host
20
+ * can share exactly one definition of the route surface.
21
+ */
22
+ export const BILLING_CONTRACT_VERSION = 'v1';
23
+ export const BILLING_INTENTS = [
24
+ 'pricing',
25
+ 'checkout',
26
+ 'subscription',
27
+ 'payment-methods',
28
+ 'invoices',
29
+ 'result'
30
+ ];
31
+ export function isBillingIntent(value) {
32
+ return BILLING_INTENTS.some((intent) => intent === value);
33
+ }
34
+ /** The route an intent occupies, e.g. `/v1/checkout`. */
35
+ export function billingIntentPath(intent) {
36
+ return `/${BILLING_CONTRACT_VERSION}/${intent}`;
37
+ }
38
+ /** The surfaces allowed to originate a billing trip. */
39
+ export const BILLING_PRODUCTS = [
40
+ 'comfyui',
41
+ 'platform',
42
+ 'workshop',
43
+ 'models'
44
+ ];
45
+ export function isBillingProduct(value) {
46
+ return BILLING_PRODUCTS.some((product) => product === value);
47
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The query shape of an entry URL, in one table so the builder and the parser
3
+ * cannot drift apart: a field is written and read under the same name, and a
4
+ * value that fails the shared charset reports the same code on both sides.
5
+ */
6
+ /** Names are provisional; see `contract.ts`. */
7
+ export declare const ENTRY_PARAM_PRODUCT = "product";
8
+ export declare const ENTRY_PARAM_RETURN_TO = "return_to";
9
+ export type OptionalEntryKey = 'plan' | 'correlationId' | 'workspaceId';
10
+ export type InvalidIdentifierCode = 'INVALID_PLAN' | 'INVALID_CORRELATION_ID' | 'INVALID_WORKSPACE_ID';
11
+ interface OptionalEntryField {
12
+ readonly key: OptionalEntryKey;
13
+ readonly param: string;
14
+ readonly code: InvalidIdentifierCode;
15
+ }
16
+ export declare const OPTIONAL_ENTRY_FIELDS: readonly OptionalEntryField[];
17
+ export type OptionalEntryValues = {
18
+ readonly [K in OptionalEntryKey]?: string;
19
+ };
20
+ export {};
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The query shape of an entry URL, in one table so the builder and the parser
3
+ * cannot drift apart: a field is written and read under the same name, and a
4
+ * value that fails the shared charset reports the same code on both sides.
5
+ */
6
+ /** Names are provisional; see `contract.ts`. */
7
+ export const ENTRY_PARAM_PRODUCT = 'product';
8
+ export const ENTRY_PARAM_RETURN_TO = 'return_to';
9
+ export const OPTIONAL_ENTRY_FIELDS = [
10
+ { key: 'plan', param: 'plan', code: 'INVALID_PLAN' },
11
+ {
12
+ key: 'correlationId',
13
+ param: 'correlation_id',
14
+ code: 'INVALID_CORRELATION_ID'
15
+ },
16
+ { key: 'workspaceId', param: 'workspace_id', code: 'INVALID_WORKSPACE_ID' }
17
+ ];
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The billing-web side of the contract: read an arriving URL back into the
3
+ * request a product meant to make. Unknown query parameters are ignored, so a
4
+ * product may add analytics noise without breaking the app; everything the
5
+ * contract names is validated, because this is untrusted input.
6
+ */
7
+ import type { BillingIntent, BillingProduct } from './contract.js';
8
+ import { BILLING_CONTRACT_VERSION } from './contract.js';
9
+ import type { InvalidIdentifierCode, OptionalEntryValues } from './entryFields.js';
10
+ import type { ReturnTarget } from './returnTargets.js';
11
+ export interface BillingEntry extends OptionalEntryValues {
12
+ readonly version: typeof BILLING_CONTRACT_VERSION;
13
+ readonly intent: BillingIntent;
14
+ readonly product: BillingProduct;
15
+ readonly returnTo: ReturnTarget;
16
+ }
17
+ export type BillingEntryErrorCode = 'UNSUPPORTED_VERSION' | 'UNKNOWN_INTENT' | 'UNKNOWN_PRODUCT' | 'UNKNOWN_RETURN_TARGET' | InvalidIdentifierCode;
18
+ export type BillingEntryResult = {
19
+ readonly status: 'ok';
20
+ readonly entry: BillingEntry;
21
+ } | {
22
+ readonly status: 'error';
23
+ readonly code: BillingEntryErrorCode;
24
+ };
25
+ /**
26
+ * Accepts a full href or the path-and-query form a router hands over. Input
27
+ * that is not a `/v1` billing route at all — a malformed string included —
28
+ * reports `UNSUPPORTED_VERSION`, because from the app's side it is a URL the
29
+ * contract does not describe.
30
+ */
31
+ export declare function parseBillingEntry(url: string | URL): BillingEntryResult;
@@ -0,0 +1,58 @@
1
+ import { BILLING_CONTRACT_VERSION, isBillingIntent, isBillingProduct } from './contract.js';
2
+ import { ENTRY_PARAM_PRODUCT, ENTRY_PARAM_RETURN_TO, OPTIONAL_ENTRY_FIELDS } from './entryFields.js';
3
+ import { isContractIdentifier } from './identifiers.js';
4
+ import { isReturnTarget } from './returnTargets.js';
5
+ import { CONTRACT_PARSE_BASE, parseUrl } from './url.js';
6
+ function parseRoute(pathname) {
7
+ const [version, intent, ...rest] = pathname.split('/').filter(Boolean);
8
+ if (version !== BILLING_CONTRACT_VERSION)
9
+ return { status: 'error', code: 'UNSUPPORTED_VERSION' };
10
+ if (rest.length > 0 || !isBillingIntent(intent))
11
+ return { status: 'error', code: 'UNKNOWN_INTENT' };
12
+ return { status: 'ok', intent };
13
+ }
14
+ function parseOptionalFields(params) {
15
+ const values = {};
16
+ for (const field of OPTIONAL_ENTRY_FIELDS) {
17
+ const raw = params.get(field.param);
18
+ if (raw === null)
19
+ continue;
20
+ if (!isContractIdentifier(raw))
21
+ return { status: 'error', code: field.code };
22
+ values[field.key] = raw;
23
+ }
24
+ return { status: 'ok', values };
25
+ }
26
+ /**
27
+ * Accepts a full href or the path-and-query form a router hands over. Input
28
+ * that is not a `/v1` billing route at all — a malformed string included —
29
+ * reports `UNSUPPORTED_VERSION`, because from the app's side it is a URL the
30
+ * contract does not describe.
31
+ */
32
+ export function parseBillingEntry(url) {
33
+ const parsed = parseUrl(url, CONTRACT_PARSE_BASE);
34
+ if (!parsed)
35
+ return { status: 'error', code: 'UNSUPPORTED_VERSION' };
36
+ const route = parseRoute(parsed.pathname);
37
+ if (route.status === 'error')
38
+ return route;
39
+ const product = parsed.searchParams.get(ENTRY_PARAM_PRODUCT);
40
+ if (!isBillingProduct(product))
41
+ return { status: 'error', code: 'UNKNOWN_PRODUCT' };
42
+ const returnTo = parsed.searchParams.get(ENTRY_PARAM_RETURN_TO);
43
+ if (returnTo === null || !isReturnTarget(returnTo))
44
+ return { status: 'error', code: 'UNKNOWN_RETURN_TARGET' };
45
+ const optional = parseOptionalFields(parsed.searchParams);
46
+ if (optional.status === 'error')
47
+ return optional;
48
+ return {
49
+ status: 'ok',
50
+ entry: {
51
+ version: BILLING_CONTRACT_VERSION,
52
+ intent: route.intent,
53
+ product,
54
+ returnTo,
55
+ ...optional.values
56
+ }
57
+ };
58
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The product side of the contract: turn a billing request into the URL to
3
+ * navigate to. Failures are coded results in the style of the billing core's
4
+ * `BillingResult` — the builder never throws, and a code carries no server or
5
+ * caller text a host could render by accident.
6
+ */
7
+ import type { BillingIntent, BillingProduct } from './contract.js';
8
+ import type { InvalidIdentifierCode, OptionalEntryValues } from './entryFields.js';
9
+ export interface BillingEntryInput extends OptionalEntryValues {
10
+ /** Only the origin is used; any path or query on it is discarded. */
11
+ readonly billingOrigin: string | URL;
12
+ readonly intent: BillingIntent;
13
+ readonly product: BillingProduct;
14
+ /**
15
+ * A `ReturnTarget`. Typed wide because the registry lookup is the real gate:
16
+ * a JavaScript host, or a generic that widened the literal, still has to
17
+ * name a registered target.
18
+ */
19
+ readonly returnTo: string;
20
+ }
21
+ export type BillingEntryUrlErrorCode = 'INVALID_ORIGIN' | 'UNKNOWN_RETURN_TARGET' | InvalidIdentifierCode;
22
+ export type BillingEntryUrlResult = {
23
+ readonly status: 'ok';
24
+ readonly url: URL;
25
+ } | {
26
+ readonly status: 'error';
27
+ readonly code: BillingEntryUrlErrorCode;
28
+ };
29
+ export declare function buildBillingEntryUrl(input: BillingEntryInput): BillingEntryUrlResult;
@@ -0,0 +1,43 @@
1
+ import { billingIntentPath } from './contract.js';
2
+ import { ENTRY_PARAM_PRODUCT, ENTRY_PARAM_RETURN_TO, OPTIONAL_ENTRY_FIELDS } from './entryFields.js';
3
+ import { isContractIdentifier } from './identifiers.js';
4
+ import { isReturnTarget } from './returnTargets.js';
5
+ import { parseUrl } from './url.js';
6
+ const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]']);
7
+ function billingOriginHref(origin) {
8
+ const url = parseUrl(origin);
9
+ if (!url || url.username || url.password)
10
+ return undefined;
11
+ if (url.protocol === 'https:')
12
+ return url.origin;
13
+ if (url.protocol === 'http:' && LOCAL_HOSTNAMES.has(url.hostname))
14
+ return url.origin;
15
+ return undefined;
16
+ }
17
+ function invalidIdentifierCode(values) {
18
+ for (const field of OPTIONAL_ENTRY_FIELDS) {
19
+ const value = values[field.key];
20
+ if (value !== undefined && !isContractIdentifier(value))
21
+ return field.code;
22
+ }
23
+ return undefined;
24
+ }
25
+ export function buildBillingEntryUrl(input) {
26
+ const origin = billingOriginHref(input.billingOrigin);
27
+ if (origin === undefined)
28
+ return { status: 'error', code: 'INVALID_ORIGIN' };
29
+ if (!isReturnTarget(input.returnTo))
30
+ return { status: 'error', code: 'UNKNOWN_RETURN_TARGET' };
31
+ const invalid = invalidIdentifierCode(input);
32
+ if (invalid !== undefined)
33
+ return { status: 'error', code: invalid };
34
+ const url = new URL(billingIntentPath(input.intent), origin);
35
+ url.searchParams.set(ENTRY_PARAM_PRODUCT, input.product);
36
+ url.searchParams.set(ENTRY_PARAM_RETURN_TO, input.returnTo);
37
+ for (const field of OPTIONAL_ENTRY_FIELDS) {
38
+ const value = input[field.key];
39
+ if (value !== undefined)
40
+ url.searchParams.set(field.param, value);
41
+ }
42
+ return { status: 'ok', url };
43
+ }
@@ -0,0 +1 @@
1
+ export declare function isContractIdentifier(value: string): boolean;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The charset every free-form identifier in the contract shares: plan slugs,
3
+ * correlation ids, workspace ids, and the reference billing hands back. It is
4
+ * deliberately narrower than what a URL permits, so nothing that survives it
5
+ * can carry a path separator, a query delimiter, or a percent escape into a
6
+ * downstream URL.
7
+ */
8
+ const CONTRACT_IDENTIFIER = /^[A-Za-z0-9_-]{1,128}$/;
9
+ export function isContractIdentifier(value) {
10
+ return CONTRACT_IDENTIFIER.test(value);
11
+ }
@@ -0,0 +1,10 @@
1
+ export type { BillingEnvironment, BillingIntent, BillingProduct } from './contract.js';
2
+ export { BILLING_CONTRACT_VERSION, BILLING_INTENTS, BILLING_PRODUCTS, billingIntentPath, isBillingIntent, isBillingProduct } from './contract.js';
3
+ export type { BillingEntry, BillingEntryErrorCode, BillingEntryResult } from './entryParser.js';
4
+ export { parseBillingEntry } from './entryParser.js';
5
+ export type { BillingEntryInput, BillingEntryUrlErrorCode, BillingEntryUrlResult } from './entryUrl.js';
6
+ export { buildBillingEntryUrl } from './entryUrl.js';
7
+ export type { ReturnTarget } from './returnTargets.js';
8
+ export { RETURN_TARGETS, isReturnTarget, resolveReturnTarget } from './returnTargets.js';
9
+ export type { BillingOutcome, BillingReturn, ReturnUrlInput } from './returnUrl.js';
10
+ export { buildReturnUrl, parseReturnResult } from './returnUrl.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { BILLING_CONTRACT_VERSION, BILLING_INTENTS, BILLING_PRODUCTS, billingIntentPath, isBillingIntent, isBillingProduct } from './contract.js';
2
+ export { parseBillingEntry } from './entryParser.js';
3
+ export { buildBillingEntryUrl } from './entryUrl.js';
4
+ export { RETURN_TARGETS, isReturnTarget, resolveReturnTarget } from './returnTargets.js';
5
+ export { buildReturnUrl, parseReturnResult } from './returnUrl.js';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Where billing is allowed to send a customer back to. A closed registry, not
3
+ * a caller-supplied URL: `return_to=<identifier>` names a target, and the
4
+ * absolute destination is chosen here per environment, so a crafted entry URL
5
+ * cannot turn the hosted app into an open redirect.
6
+ *
7
+ * Every destination below is one this repository already states. A family a
8
+ * target has no confirmed origin for stays absent rather than being derived
9
+ * from a sibling's hostname, because an unresolvable target fails closed while
10
+ * a guessed one sends a customer somewhere nobody serves.
11
+ *
12
+ * Localhost and desktop return targets are out of scope: a developer running a
13
+ * product locally returns to the deployed destination of whichever environment
14
+ * their backend belongs to.
15
+ */
16
+ import type { BillingEnvironment } from './contract.js';
17
+ export declare const RETURN_TARGETS: readonly ["comfyui_workspace", "comfyui_credits", "platform_account"];
18
+ export type ReturnTarget = (typeof RETURN_TARGETS)[number];
19
+ export declare function isReturnTarget(value: string): value is ReturnTarget;
20
+ /**
21
+ * Undefined for an identifier outside the registry, and for a registered
22
+ * target that has no destination in the requested environment. Each call
23
+ * returns a fresh `URL` so a caller can append to it without editing the
24
+ * registry.
25
+ */
26
+ export declare function resolveReturnTarget(target: string, environment: BillingEnvironment): URL | undefined;
@@ -0,0 +1,41 @@
1
+ import { parseUrl } from './url.js';
2
+ export const RETURN_TARGETS = [
3
+ 'comfyui_workspace',
4
+ 'comfyui_credits',
5
+ 'platform_account'
6
+ ];
7
+ const RETURN_TARGET_DESTINATIONS = {
8
+ comfyui_workspace: {
9
+ production: 'https://cloud.comfy.org/',
10
+ staging: 'https://stagingcloud.comfy.org/',
11
+ test: 'https://testcloud.comfy.org/'
12
+ },
13
+ // The cloud app's credits settings page, the same URL the Workshop builds as
14
+ // `WORKSHOP_CREDITS_URL`.
15
+ comfyui_credits: {
16
+ production: 'https://cloud.comfy.org/?settings=plan-credits',
17
+ staging: 'https://stagingcloud.comfy.org/?settings=plan-credits',
18
+ test: 'https://testcloud.comfy.org/?settings=plan-credits'
19
+ },
20
+ // Platform lives in its own repository and names no account route here, so
21
+ // its origin root is the destination until that app names one.
22
+ platform_account: {
23
+ production: 'https://platform.comfy.org/',
24
+ staging: 'https://stagingplatform.comfy.org/'
25
+ }
26
+ };
27
+ export function isReturnTarget(value) {
28
+ return RETURN_TARGETS.some((target) => target === value);
29
+ }
30
+ /**
31
+ * Undefined for an identifier outside the registry, and for a registered
32
+ * target that has no destination in the requested environment. Each call
33
+ * returns a fresh `URL` so a caller can append to it without editing the
34
+ * registry.
35
+ */
36
+ export function resolveReturnTarget(target, environment) {
37
+ if (!isReturnTarget(target))
38
+ return undefined;
39
+ const destination = RETURN_TARGET_DESTINATIONS[target][environment];
40
+ return destination === undefined ? undefined : parseUrl(destination);
41
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The trip back. Billing resolves a registered target to its environment's
3
+ * destination and may append a coarse outcome plus an opaque reference; a
4
+ * consumer reads both back and must work when neither is there, because a
5
+ * customer can close the tab or arrive by bookmark at any point.
6
+ */
7
+ import type { BillingEnvironment } from './contract.js';
8
+ declare const BILLING_OUTCOMES: readonly ["success", "cancelled", "pending"];
9
+ /**
10
+ * Deliberately coarse. A consumer routes on it; the authoritative state of a
11
+ * subscription is whatever the consumer's own next read of billing says.
12
+ */
13
+ export type BillingOutcome = (typeof BILLING_OUTCOMES)[number];
14
+ export interface ReturnUrlInput {
15
+ /** A `ReturnTarget`; see `BillingEntryInput.returnTo` for why it is wide. */
16
+ readonly target: string;
17
+ readonly environment: BillingEnvironment;
18
+ readonly result?: BillingOutcome;
19
+ /** An operation or correlation reference. Dropped when it is not opaque. */
20
+ readonly reference?: string;
21
+ }
22
+ export declare function buildReturnUrl(input: ReturnUrlInput): URL | undefined;
23
+ export interface BillingReturn {
24
+ readonly result?: BillingOutcome;
25
+ readonly reference?: string;
26
+ }
27
+ export declare function parseReturnResult(url: string | URL): BillingReturn;
28
+ export {};
@@ -0,0 +1,33 @@
1
+ import { isContractIdentifier } from './identifiers.js';
2
+ import { resolveReturnTarget } from './returnTargets.js';
3
+ import { CONTRACT_PARSE_BASE, parseUrl } from './url.js';
4
+ /** Names are provisional; see `contract.ts`. */
5
+ const RETURN_PARAM_OUTCOME = 'billing_result';
6
+ const RETURN_PARAM_REFERENCE = 'billing_ref';
7
+ const BILLING_OUTCOMES = ['success', 'cancelled', 'pending'];
8
+ function isBillingOutcome(value) {
9
+ return BILLING_OUTCOMES.some((outcome) => outcome === value);
10
+ }
11
+ export function buildReturnUrl(input) {
12
+ const url = resolveReturnTarget(input.target, input.environment);
13
+ if (!url)
14
+ return undefined;
15
+ if (input.result !== undefined)
16
+ url.searchParams.set(RETURN_PARAM_OUTCOME, input.result);
17
+ if (input.reference !== undefined && isContractIdentifier(input.reference))
18
+ url.searchParams.set(RETURN_PARAM_REFERENCE, input.reference);
19
+ return url;
20
+ }
21
+ export function parseReturnResult(url) {
22
+ const params = parseUrl(url, CONTRACT_PARSE_BASE)?.searchParams;
23
+ if (!params)
24
+ return {};
25
+ const result = params.get(RETURN_PARAM_OUTCOME);
26
+ const reference = params.get(RETURN_PARAM_REFERENCE);
27
+ return {
28
+ ...(isBillingOutcome(result) ? { result } : {}),
29
+ ...(reference !== null && isContractIdentifier(reference)
30
+ ? { reference }
31
+ : {})
32
+ };
33
+ }
package/dist/url.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The one place a string becomes a `URL` in this package. Every entry point
3
+ * here takes input that arrived over a URL bar, so a parse failure is a value
4
+ * to branch on rather than an exception each caller has to remember to catch.
5
+ */
6
+ export declare function parseUrl(value: string | URL, base?: string): URL | undefined;
7
+ /**
8
+ * Lets the parsers accept the path-and-query form a router hands them
9
+ * (`/v1/checkout?...`) as readily as a full href. It is never read back: only
10
+ * the pathname and the query survive parsing, and `.invalid` is reserved by
11
+ * RFC 2606 so a bug that leaked it could not reach a real host.
12
+ */
13
+ export declare const CONTRACT_PARSE_BASE = "https://billing.invalid/";
package/dist/url.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The one place a string becomes a `URL` in this package. Every entry point
3
+ * here takes input that arrived over a URL bar, so a parse failure is a value
4
+ * to branch on rather than an exception each caller has to remember to catch.
5
+ */
6
+ export function parseUrl(value, base) {
7
+ try {
8
+ return new URL(value instanceof URL ? value.href : value, base);
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ }
14
+ /**
15
+ * Lets the parsers accept the path-and-query form a router hands them
16
+ * (`/v1/checkout?...`) as readily as a full href. It is never read back: only
17
+ * the pathname and the query survive parsing, and `.invalid` is reserved by
18
+ * RFC 2606 so a bug that leaked it could not reach a real host.
19
+ */
20
+ export const CONTRACT_PARSE_BASE = 'https://billing.invalid/';
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@comfyorg/billing-contract",
3
+ "version": "1.0.0-alpha.0",
4
+ "description": "The URL contract every Comfy product shares with the hosted billing app: the versioned route builder, the entry parser billing-web reads on arrival, and the closed return_to registry that keeps the trip back off open redirects",
5
+ "keywords": [],
6
+ "homepage": "https://comfy.org",
7
+ "license": "MIT",
8
+ "author": {
9
+ "name": "Comfy Org",
10
+ "email": "support@comfy.org",
11
+ "url": "https://www.comfy.org"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/Comfy-Org/ComfyUI_frontend.git",
16
+ "directory": "packages/billing-contract"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "type": "module",
22
+ "sideEffects": false,
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^25.9.5",
35
+ "@vitest/coverage-v8": "^4.1.11",
36
+ "typescript": "^6.0.3",
37
+ "vitest": "^4.1.11"
38
+ },
39
+ "scripts": {
40
+ "build": "pnpm run clean && tsc -p tsconfig.build.json",
41
+ "clean": "node --input-type=module -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\"",
42
+ "lint": "oxlint src --type-aware && eslint src",
43
+ "test": "vitest run",
44
+ "typecheck": "tsc --noEmit"
45
+ }
46
+ }