@comfyorg/account-core 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.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/dist/core/billing/balanceWatch.d.ts +35 -0
  3. package/dist/core/billing/balanceWatch.js +83 -0
  4. package/dist/core/billing/billingContracts.d.ts +123 -0
  5. package/dist/core/billing/billingContracts.js +11 -0
  6. package/dist/core/billing/billingErrorBody.d.ts +8 -0
  7. package/dist/core/billing/billingErrorBody.js +11 -0
  8. package/dist/core/billing/billingScope.d.ts +39 -0
  9. package/dist/core/billing/billingScope.js +45 -0
  10. package/dist/core/billing/capabilities.d.ts +70 -0
  11. package/dist/core/billing/capabilities.js +137 -0
  12. package/dist/core/billing/capabilityDenials.d.ts +26 -0
  13. package/dist/core/billing/capabilityDenials.js +43 -0
  14. package/dist/core/billing/challengeDriver.d.ts +22 -0
  15. package/dist/core/billing/challengeDriver.js +38 -0
  16. package/dist/core/billing/credentialedTransport.d.ts +35 -0
  17. package/dist/core/billing/credentialedTransport.js +53 -0
  18. package/dist/core/billing/credits.d.ts +43 -0
  19. package/dist/core/billing/credits.js +29 -0
  20. package/dist/core/billing/httpStatus.d.ts +7 -0
  21. package/dist/core/billing/httpStatus.js +25 -0
  22. package/dist/core/billing/index.d.ts +52 -0
  23. package/dist/core/billing/index.js +23 -0
  24. package/dist/core/billing/operationLifecycle.d.ts +77 -0
  25. package/dist/core/billing/operationLifecycle.js +547 -0
  26. package/dist/core/billing/operationPointer.d.ts +48 -0
  27. package/dist/core/billing/operationPointer.js +89 -0
  28. package/dist/core/billing/operationPolicy.d.ts +26 -0
  29. package/dist/core/billing/operationPolicy.js +51 -0
  30. package/dist/core/billing/operationState.d.ts +128 -0
  31. package/dist/core/billing/operationState.js +187 -0
  32. package/dist/core/billing/paymentCopy.d.ts +18 -0
  33. package/dist/core/billing/paymentCopy.js +47 -0
  34. package/dist/core/billing/paymentMethods.d.ts +40 -0
  35. package/dist/core/billing/paymentMethods.js +37 -0
  36. package/dist/core/billing/paymentProjection.d.ts +34 -0
  37. package/dist/core/billing/paymentProjection.js +56 -0
  38. package/dist/core/billing/plans.d.ts +44 -0
  39. package/dist/core/billing/plans.js +29 -0
  40. package/dist/core/billing/presentation.d.ts +20 -0
  41. package/dist/core/billing/presentation.js +10 -0
  42. package/dist/core/billing/scopedReader.d.ts +61 -0
  43. package/dist/core/billing/scopedReader.js +108 -0
  44. package/dist/core/billing/sharedRead.d.ts +33 -0
  45. package/dist/core/billing/sharedRead.js +59 -0
  46. package/dist/core/billing/status.d.ts +27 -0
  47. package/dist/core/billing/status.js +21 -0
  48. package/dist/core/billing/subscriptionCommands.d.ts +367 -0
  49. package/dist/core/billing/subscriptionCommands.js +263 -0
  50. package/dist/core/billing/topup.d.ts +113 -0
  51. package/dist/core/billing/topup.js +209 -0
  52. package/dist/core/billing/transport.d.ts +14 -0
  53. package/dist/core/billing/transport.js +106 -0
  54. package/dist/core/billing/transportExchange.d.ts +24 -0
  55. package/dist/core/billing/transportExchange.js +79 -0
  56. package/dist/core/boundedOperation.d.ts +16 -0
  57. package/dist/core/boundedOperation.js +12 -0
  58. package/dist/core/credentialCache.d.ts +41 -0
  59. package/dist/core/credentialCache.js +97 -0
  60. package/dist/core/customerRecovery.d.ts +39 -0
  61. package/dist/core/customerRecovery.js +79 -0
  62. package/dist/core/exchange.d.ts +50 -0
  63. package/dist/core/exchange.js +137 -0
  64. package/dist/core/identity.d.ts +21 -0
  65. package/dist/core/identity.js +16 -0
  66. package/dist/core/mintCoordinator.d.ts +18 -0
  67. package/dist/core/mintCoordinator.js +31 -0
  68. package/dist/core/refreshScheduler.d.ts +46 -0
  69. package/dist/core/refreshScheduler.js +262 -0
  70. package/dist/core/session.d.ts +146 -0
  71. package/dist/core/session.js +312 -0
  72. package/dist/core/sessionContracts.d.ts +135 -0
  73. package/dist/core/sessionContracts.js +9 -0
  74. package/dist/core/sessionState.d.ts +118 -0
  75. package/dist/core/sessionState.js +235 -0
  76. package/dist/firebase/index.d.ts +53 -0
  77. package/dist/firebase/index.js +74 -0
  78. package/dist/firebaseAuthError.d.ts +52 -0
  79. package/dist/firebaseAuthError.js +88 -0
  80. package/dist/loadExternalScript.d.ts +7 -0
  81. package/dist/loadExternalScript.js +88 -0
  82. package/dist/provisioning.d.ts +53 -0
  83. package/dist/provisioning.js +76 -0
  84. package/dist/redirect.d.ts +13 -0
  85. package/dist/redirect.js +38 -0
  86. package/dist/signInSchemas.d.ts +88 -0
  87. package/dist/signInSchemas.js +71 -0
  88. package/dist/telemetry.d.ts +39 -0
  89. package/dist/telemetry.js +26 -0
  90. package/dist/testing.d.ts +7 -0
  91. package/dist/testing.js +3 -0
  92. package/dist/turnstile.d.ts +14 -0
  93. package/dist/turnstile.js +17 -0
  94. package/dist/turnstileScript.d.ts +18 -0
  95. package/dist/turnstileScript.js +6 -0
  96. package/dist/web/crossTabRefresh.d.ts +18 -0
  97. package/dist/web/crossTabRefresh.js +121 -0
  98. package/dist/webviewDetection.d.ts +5 -0
  99. package/dist/webviewDetection.js +62 -0
  100. package/package.json +114 -0
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The plans read: the catalog a workspace may subscribe to, cached per scope.
3
+ *
4
+ * Availability, seat costs, and the credit-stop ladder are all resolved by the
5
+ * server for the actor that asked, so the response is cached under the scope it
6
+ * was resolved for and no plan is filtered, ranked, or priced here.
7
+ */
8
+ import { zGetBillingPlansResponse } from '@comfyorg/ingest-types/zod';
9
+ import type { z } from 'zod';
10
+ import type { BillingResult, BillingTransport } from './billingContracts.js';
11
+ import type { BillingScope, BillingScopeSource } from './billingScope.js';
12
+ export declare const PLANS_ROUTE = "/billing/plans";
13
+ export type BillingPlansData = z.infer<typeof zGetBillingPlansResponse>;
14
+ export type PlansScope = BillingScope;
15
+ export interface PlansSnapshot {
16
+ readonly scope: PlansScope;
17
+ readonly data: BillingPlansData;
18
+ /** ms since epoch at which this read settled. */
19
+ readonly readAt: number;
20
+ }
21
+ export interface PlansReadOptions {
22
+ readonly signal?: AbortSignal;
23
+ /** Bounds the request, when this caller is the one that issues it. */
24
+ readonly timeoutMs?: number;
25
+ }
26
+ export interface PlansReader {
27
+ /**
28
+ * Always requests. The catalog changes rarely, but nothing in the response
29
+ * declares how long it stays valid, so the host decides when to re-read
30
+ * rather than the core inventing a freshness window nobody published.
31
+ * Concurrent callers for one scope still share a single request.
32
+ */
33
+ read: (options?: PlansReadOptions) => Promise<BillingResult<PlansSnapshot>>;
34
+ /** The last catalog published for the current scope. */
35
+ getSnapshot: () => PlansSnapshot | undefined;
36
+ dispose: () => void;
37
+ }
38
+ export interface PlansReaderOptions {
39
+ readonly transport: BillingTransport;
40
+ /** Where the core learns which user, workspace, and role it runs as. */
41
+ readonly scopeSource: BillingScopeSource;
42
+ readonly now?: () => number;
43
+ }
44
+ export declare function createPlansReader(options: PlansReaderOptions): PlansReader;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The plans read: the catalog a workspace may subscribe to, cached per scope.
3
+ *
4
+ * Availability, seat costs, and the credit-stop ladder are all resolved by the
5
+ * server for the actor that asked, so the response is cached under the scope it
6
+ * was resolved for and no plan is filtered, ranked, or priced here.
7
+ */
8
+ import { zGetBillingPlansResponse } from '@comfyorg/ingest-types/zod';
9
+ import { createScopedReader } from './scopedReader.js';
10
+ export const PLANS_ROUTE = '/billing/plans';
11
+ export function createPlansReader(options) {
12
+ const { transport, scopeSource, now = Date.now } = options;
13
+ const reader = createScopedReader({
14
+ transport,
15
+ scopeSource,
16
+ route: PLANS_ROUTE,
17
+ parse: (body) => zGetBillingPlansResponse.safeParse(body),
18
+ timeoutMs: (readOptions) => readOptions?.timeoutMs,
19
+ project: ({ data }, scope) => ({
20
+ status: 'ok',
21
+ value: { scope, data, readAt: now() }
22
+ })
23
+ });
24
+ return {
25
+ read: reader.read,
26
+ getSnapshot: reader.getSnapshot,
27
+ dispose: reader.dispose
28
+ };
29
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The one place a billing operation is routed to the embedded challenge or
3
+ * the hosted page. Hosts report whether they can drive an embedded challenge
4
+ * at all; they never choose the route.
5
+ *
6
+ * Every rule fails toward hosted, which works on every rail and needs
7
+ * nothing from the host but a way to open a URL.
8
+ */
9
+ import type { BillingPresentation } from './operationState.js';
10
+ import type { BillingStatusData } from './status.js';
11
+ export interface PresentationRoutingInput {
12
+ /** The host can drive an in-page challenge: its provider adapter and key are present. */
13
+ readonly embeddedCheckoutAvailable: boolean;
14
+ readonly billingRail: BillingStatusData['billing_rail'];
15
+ /** A hosted continuation the command response already carried. */
16
+ readonly hostedUrl?: string;
17
+ /** An in-page challenge the command response already carried. */
18
+ readonly clientSecret?: string;
19
+ }
20
+ export declare function selectBillingPresentation(input: PresentationRoutingInput): BillingPresentation;
@@ -0,0 +1,10 @@
1
+ export function selectBillingPresentation(input) {
2
+ // The backend answered with a page and no challenge: it has decided.
3
+ if (input.hostedUrl !== undefined && input.clientSecret === undefined) {
4
+ return 'hosted';
5
+ }
6
+ if (!input.embeddedCheckoutAvailable)
7
+ return 'hosted';
8
+ // Only the workspace Stripe rail exposes payment intents to the client.
9
+ return input.billingRail === 'stripe' ? 'embedded' : 'hosted';
10
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The one shape every billing read has: capture the scope, issue a validated
3
+ * request, and refuse to attribute the answer to a scope that has since taken
4
+ * over. Only the route, the decoder, and the projection into a snapshot differ
5
+ * between reads, so only those live in the readers.
6
+ *
7
+ * What the skeleton owns is what a reader therefore cannot get wrong on its
8
+ * own: one shared request per scope, a caller's signal releasing only that
9
+ * caller, the publish fence that reports a late answer as `SUPERSEDED`, and
10
+ * the denial that drops a published snapshot unless it predates a change the
11
+ * host reported.
12
+ */
13
+ import type { BillingResult, BillingTransport } from './billingContracts.js';
14
+ import type { BillingScope, BillingScopeSource } from './billingScope.js';
15
+ import type { ParsedBillingBody, ValidatedBillingResponse } from './sharedRead.js';
16
+ /** Every reader's read options carry at least the caller's release signal. */
17
+ export interface ScopedReadOptions {
18
+ readonly signal?: AbortSignal;
19
+ }
20
+ export interface ScopedReaderDefinition<TData, TSnapshot, TOptions extends ScopedReadOptions> {
21
+ readonly transport: BillingTransport;
22
+ /** Where the core learns which user, workspace, and role it runs as. */
23
+ readonly scopeSource: BillingScopeSource;
24
+ readonly route: string;
25
+ readonly parse: (body: unknown) => ParsedBillingBody<TData>;
26
+ /**
27
+ * The reader's own reading of a validated response. Returning a failure is
28
+ * how a reader refuses an answer its decoder alone cannot reject.
29
+ */
30
+ readonly project: (response: ValidatedBillingResponse<TData>, scope: BillingScope) => BillingResult<TSnapshot>;
31
+ /** The request budget, for a read that bounds itself or lets its caller. */
32
+ readonly timeoutMs?: (options: TOptions | undefined) => number | undefined;
33
+ /**
34
+ * The published snapshot this call may be served from instead of asking.
35
+ * Absent for a reader that always requests.
36
+ */
37
+ readonly cached?: (snapshot: TSnapshot | undefined, scope: BillingScope, options: TOptions | undefined) => TSnapshot | undefined;
38
+ /**
39
+ * What to publish once the read settles in scope, given whether the host
40
+ * fenced the request while it was in flight. `undefined` publishes nothing,
41
+ * and the callers waiting on the request are still served what it read.
42
+ */
43
+ readonly publish?: (value: TSnapshot, fenced: boolean) => TSnapshot | undefined;
44
+ }
45
+ export interface ScopedReader<TSnapshot, TOptions extends ScopedReadOptions> {
46
+ readonly read: (options?: TOptions) => Promise<BillingResult<TSnapshot>>;
47
+ readonly getSnapshot: () => TSnapshot | undefined;
48
+ /** Replaces what `getSnapshot` publishes; `undefined` drops it. */
49
+ readonly setSnapshot: (snapshot: TSnapshot | undefined) => void;
50
+ /**
51
+ * Marks a request already in flight as predating a change the host is
52
+ * reporting, leaving `publish` to decide what its answer is still worth.
53
+ * `detach` also frees the shared slot, so the read that follows issues its
54
+ * own request rather than joining the one that predates the change.
55
+ */
56
+ readonly fenceInFlight: (options?: {
57
+ readonly detach?: boolean;
58
+ }) => void;
59
+ readonly dispose: () => void;
60
+ }
61
+ export declare function createScopedReader<TData, TSnapshot, TOptions extends ScopedReadOptions>(definition: ScopedReaderDefinition<TData, TSnapshot, TOptions>): ScopedReader<TSnapshot, TOptions>;
@@ -0,0 +1,108 @@
1
+ import { createBillingScopeTracker } from './billingScope.js';
2
+ import { matchesScopedRead, readValidatedBillingResponse, releaseOnAbort } from './sharedRead.js';
3
+ function publishAsRead(value) {
4
+ return value;
5
+ }
6
+ export function createScopedReader(definition) {
7
+ const { transport, scopeSource, route, parse, project, timeoutMs, cached, publish = publishAsRead } = definition;
8
+ let snapshot;
9
+ let inFlight;
10
+ const lifetime = { disposed: false };
11
+ const scopeTracker = createBillingScopeTracker(scopeSource, () => {
12
+ snapshot = undefined;
13
+ inFlight = undefined;
14
+ });
15
+ async function request(scope, options) {
16
+ const budget = timeoutMs?.(options);
17
+ const billingRequest = {
18
+ method: 'GET',
19
+ route,
20
+ ...(budget === undefined ? {} : { timeoutMs: budget })
21
+ };
22
+ const response = await readValidatedBillingResponse(transport, billingRequest, parse);
23
+ if (response.status === 'error')
24
+ return response;
25
+ return project(response.value, scope);
26
+ }
27
+ function publishSettled(value, fenced) {
28
+ const published = publish(value, fenced);
29
+ if (published !== undefined)
30
+ snapshot = published;
31
+ return { status: 'ok', value: published ?? value };
32
+ }
33
+ async function settle(context, pending, options) {
34
+ const result = await request(context.scope, options);
35
+ // The publish guard. Between issuing the request and settling it the host
36
+ // may have changed workspace or signed out, so an answer cached now would
37
+ // be attributed to whoever is signed in next, and a failure says nothing
38
+ // about the scope that has since taken over.
39
+ if (lifetime.disposed || !scopeTracker.isCurrent(context)) {
40
+ return { status: 'error', code: 'SUPERSEDED' };
41
+ }
42
+ if (result.status !== 'ok') {
43
+ // A denial that predates a fence is no evidence about what a later read
44
+ // published, and clearing on it would drop a fresh answer for a stale
45
+ // refusal.
46
+ if (result.code === 'ACCESS_DENIED' && !pending.fenced) {
47
+ snapshot = undefined;
48
+ }
49
+ return result;
50
+ }
51
+ return publishSettled(result.value, pending.fenced);
52
+ }
53
+ async function read(options) {
54
+ if (lifetime.disposed)
55
+ return { status: 'error', code: 'SUPERSEDED' };
56
+ const context = scopeTracker.capture();
57
+ if (context === undefined) {
58
+ return { status: 'error', code: 'NOT_AUTHENTICATED' };
59
+ }
60
+ const served = cached?.(snapshot, context.scope, options);
61
+ if (served !== undefined)
62
+ return { status: 'ok', value: served };
63
+ // One read per scope. A caller arriving mid-flight for the same scope
64
+ // joins rather than issuing a second identical request; a caller for a
65
+ // different scope starts its own, and the older one can no longer publish.
66
+ if (matchesScopedRead(inFlight, context)) {
67
+ return releaseOnAbort(inFlight.promise, options?.signal);
68
+ }
69
+ // No caller signal reaches the shared request: it is bounded by its own
70
+ // timeout, and one caller walking away must not fail the readers still
71
+ // waiting on it. Each caller's signal releases only that caller, below.
72
+ const pending = { fenced: false };
73
+ const attempt = {
74
+ context,
75
+ promise: settle(context, pending, options),
76
+ pending
77
+ };
78
+ inFlight = attempt;
79
+ // The slot is released when the request settles, not when this caller
80
+ // stops waiting, so an abandoned read still serves whoever joined it.
81
+ const release = () => {
82
+ if (inFlight === attempt)
83
+ inFlight = undefined;
84
+ };
85
+ attempt.promise.then(release, release);
86
+ return releaseOnAbort(attempt.promise, options?.signal);
87
+ }
88
+ return {
89
+ read,
90
+ getSnapshot: () => snapshot,
91
+ setSnapshot: (next) => {
92
+ snapshot = next;
93
+ },
94
+ fenceInFlight: (fenceOptions) => {
95
+ if (inFlight === undefined)
96
+ return;
97
+ inFlight.pending.fenced = true;
98
+ if (fenceOptions?.detach === true)
99
+ inFlight = undefined;
100
+ },
101
+ dispose: () => {
102
+ lifetime.disposed = true;
103
+ snapshot = undefined;
104
+ inFlight = undefined;
105
+ scopeTracker.dispose();
106
+ }
107
+ };
108
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Joining a caller to a shared in-flight read without letting either one
3
+ * decide the other's fate.
4
+ *
5
+ * Deduplication and cancellation pull in opposite directions: if the request
6
+ * carries the first caller's signal, that caller aborting fails every reader
7
+ * that joined it, and a joiner aborting does nothing at all. So the shared
8
+ * request carries no caller signal — it is bounded by its own timeout — and
9
+ * each caller's signal releases only that caller's wait.
10
+ *
11
+ * This is the semantic `SessionClient.ensureFresh` already documents for its
12
+ * shared mint: concurrent callers share one request, and a later caller's
13
+ * signal releases that caller with a transient failure without cancelling the
14
+ * work the others are still waiting on.
15
+ */
16
+ import type { BillingRequest, BillingResult, BillingTransport } from './billingContracts.js';
17
+ import type { BillingScopeContext } from './billingScope.js';
18
+ export type ParsedBillingBody<T> = {
19
+ readonly success: true;
20
+ readonly data: T;
21
+ } | {
22
+ readonly success: false;
23
+ };
24
+ export interface ValidatedBillingResponse<T> {
25
+ readonly data: T;
26
+ readonly body: unknown;
27
+ readonly httpStatus: number;
28
+ }
29
+ export declare function matchesScopedRead<T extends {
30
+ readonly context: BillingScopeContext;
31
+ }>(read: T | undefined, context: BillingScopeContext): read is T;
32
+ export declare function readValidatedBillingResponse<T>(transport: BillingTransport, request: BillingRequest, parse: (body: unknown) => ParsedBillingBody<T>): Promise<BillingResult<ValidatedBillingResponse<T>>>;
33
+ export declare function releaseOnAbort<T>(shared: Promise<BillingResult<T>>, signal: AbortSignal | undefined): Promise<BillingResult<T>>;
@@ -0,0 +1,59 @@
1
+ import { sameBillingScope } from './billingScope.js';
2
+ import { readBillingErrorCode } from './billingErrorBody.js';
3
+ import { codeForHttpStatus } from './httpStatus.js';
4
+ /**
5
+ * An abandoned wait is transient, the same bucket the transport reports an
6
+ * abort under: the caller stopped waiting, which says nothing about whether
7
+ * the read would have succeeded.
8
+ */
9
+ const ABANDONED = {
10
+ status: 'error',
11
+ code: 'REQUEST_FAILED'
12
+ };
13
+ export function matchesScopedRead(read, context) {
14
+ return (read !== undefined &&
15
+ read.context.generation === context.generation &&
16
+ sameBillingScope(read.context.scope, context.scope));
17
+ }
18
+ export async function readValidatedBillingResponse(transport, request, parse) {
19
+ const response = await transport(request);
20
+ if (response.status === 'error')
21
+ return response;
22
+ const { httpStatus, body } = response.value;
23
+ if (httpStatus < 200 || httpStatus >= 300) {
24
+ const serverCode = readBillingErrorCode(body);
25
+ return {
26
+ status: 'error',
27
+ code: codeForHttpStatus(response.value),
28
+ httpStatus,
29
+ ...(serverCode === undefined ? {} : { serverCode })
30
+ };
31
+ }
32
+ const parsed = parse(body);
33
+ if (!parsed.success) {
34
+ return { status: 'error', code: 'MALFORMED_RESPONSE', httpStatus };
35
+ }
36
+ return { status: 'ok', value: { data: parsed.data, body, httpStatus } };
37
+ }
38
+ export function releaseOnAbort(shared, signal) {
39
+ if (signal === undefined)
40
+ return shared.catch(() => ABANDONED);
41
+ // An 'abort' listener never fires for a signal that is already aborted, so
42
+ // a caller who cancelled before joining would otherwise wait for the full
43
+ // shared read.
44
+ if (signal.aborted) {
45
+ void shared.catch(() => { });
46
+ return Promise.resolve(ABANDONED);
47
+ }
48
+ return new Promise((resolve) => {
49
+ const onAbort = () => resolve(ABANDONED);
50
+ signal.addEventListener('abort', onAbort, { once: true });
51
+ shared.then((value) => {
52
+ signal.removeEventListener('abort', onAbort);
53
+ resolve(value);
54
+ }, () => {
55
+ signal.removeEventListener('abort', onAbort);
56
+ resolve(ABANDONED);
57
+ });
58
+ });
59
+ }
@@ -0,0 +1,27 @@
1
+ import { zBillingStatusResponse } from '@comfyorg/ingest-types/zod';
2
+ import type { z } from 'zod';
3
+ import type { BillingResult, BillingTransport } from './billingContracts.js';
4
+ import type { BillingScope, BillingScopeSource } from './billingScope.js';
5
+ export declare const BILLING_STATUS_ROUTE = "/billing/status";
6
+ export type BillingStatusData = z.infer<typeof zBillingStatusResponse>;
7
+ export type BillingStatusScope = BillingScope;
8
+ export interface BillingStatusSnapshot {
9
+ readonly status: BillingStatusData;
10
+ readonly scope: BillingStatusScope;
11
+ readonly readAt: number;
12
+ }
13
+ export interface BillingStatusReadOptions {
14
+ readonly signal?: AbortSignal;
15
+ }
16
+ export interface BillingStatusReader {
17
+ read: (options?: BillingStatusReadOptions) => Promise<BillingResult<BillingStatusSnapshot>>;
18
+ getSnapshot: () => BillingStatusSnapshot | undefined;
19
+ dispose: () => void;
20
+ }
21
+ export interface BillingStatusReaderOptions {
22
+ readonly transport: BillingTransport;
23
+ /** Where the core learns which user, workspace, and role it runs as. */
24
+ readonly scopeSource: BillingScopeSource;
25
+ readonly now?: () => number;
26
+ }
27
+ export declare function createBillingStatusReader(options: BillingStatusReaderOptions): BillingStatusReader;
@@ -0,0 +1,21 @@
1
+ import { zBillingStatusResponse } from '@comfyorg/ingest-types/zod';
2
+ import { createScopedReader } from './scopedReader.js';
3
+ export const BILLING_STATUS_ROUTE = '/billing/status';
4
+ export function createBillingStatusReader(options) {
5
+ const { transport, scopeSource, now = Date.now } = options;
6
+ const reader = createScopedReader({
7
+ transport,
8
+ scopeSource,
9
+ route: BILLING_STATUS_ROUTE,
10
+ parse: (body) => zBillingStatusResponse.safeParse(body),
11
+ project: ({ data }, scope) => ({
12
+ status: 'ok',
13
+ value: { status: data, scope, readAt: now() }
14
+ })
15
+ });
16
+ return {
17
+ read: reader.read,
18
+ getSnapshot: reader.getSnapshot,
19
+ dispose: reader.dispose
20
+ };
21
+ }