@comfyorg/account-core 1.0.0-alpha.0 → 1.0.0-alpha.2

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 (64) hide show
  1. package/dist/core/billing/billingContracts.d.ts +29 -14
  2. package/dist/core/billing/billingErrorBody.d.ts +8 -4
  3. package/dist/core/billing/billingErrorBody.js +14 -4
  4. package/dist/core/billing/credentialedTransport.d.ts +23 -7
  5. package/dist/core/billing/credentialedTransport.js +72 -8
  6. package/dist/core/billing/events.d.ts +47 -0
  7. package/dist/core/billing/events.js +43 -0
  8. package/dist/core/billing/index.d.ts +16 -11
  9. package/dist/core/billing/index.js +8 -6
  10. package/dist/core/billing/operationLifecycle.d.ts +34 -1
  11. package/dist/core/billing/operationLifecycle.js +146 -67
  12. package/dist/core/billing/operationPointer.d.ts +27 -2
  13. package/dist/core/billing/operationPointer.js +36 -25
  14. package/dist/core/billing/operationPolicy.d.ts +44 -7
  15. package/dist/core/billing/operationPolicy.js +64 -3
  16. package/dist/core/billing/operationState.d.ts +225 -19
  17. package/dist/core/billing/operationState.js +93 -8
  18. package/dist/core/billing/paymentCopy.d.ts +6 -1
  19. package/dist/core/billing/paymentCopy.js +34 -1
  20. package/dist/core/billing/paymentProjection.d.ts +14 -2
  21. package/dist/core/billing/paymentProjection.js +37 -9
  22. package/dist/core/billing/scopedReader.d.ts +11 -5
  23. package/dist/core/billing/scopedReader.js +20 -9
  24. package/dist/core/billing/sharedRead.js +4 -2
  25. package/dist/core/billing/subscriptionCommands.d.ts +63 -9
  26. package/dist/core/billing/subscriptionCommands.js +46 -30
  27. package/dist/core/billing/topup.d.ts +26 -0
  28. package/dist/core/billing/topup.js +36 -2
  29. package/dist/core/billing/wireCents.d.ts +8 -0
  30. package/dist/core/billing/wireCents.js +8 -0
  31. package/dist/core/billing/workspaceInvites.d.ts +12 -0
  32. package/dist/core/billing/workspaceInvites.js +26 -0
  33. package/dist/core/identity.d.ts +6 -6
  34. package/dist/core/identity.js +4 -10
  35. package/dist/core/lazyIdentity.d.ts +31 -0
  36. package/dist/core/lazyIdentity.js +96 -0
  37. package/dist/core/requestAuth.d.ts +30 -0
  38. package/dist/core/requestAuth.js +41 -0
  39. package/dist/core/requestTimeout.d.ts +7 -0
  40. package/dist/core/requestTimeout.js +13 -0
  41. package/dist/core/session.d.ts +12 -18
  42. package/dist/core/session.js +16 -26
  43. package/dist/core/sessionContracts.d.ts +39 -2
  44. package/dist/core/sessionTokenMint.d.ts +35 -0
  45. package/dist/core/sessionTokenMint.js +222 -0
  46. package/dist/core/webSession.d.ts +27 -0
  47. package/dist/core/webSession.js +144 -0
  48. package/dist/core/webSessionFlag.d.ts +19 -0
  49. package/dist/core/webSessionFlag.js +61 -0
  50. package/dist/core/webSessionIdentity.d.ts +297 -0
  51. package/dist/core/webSessionIdentity.js +509 -0
  52. package/dist/firebase/configSource.d.ts +54 -0
  53. package/dist/firebase/configSource.js +111 -0
  54. package/dist/firebase/index.d.ts +71 -8
  55. package/dist/firebase/index.js +236 -23
  56. package/dist/firebase/popupWatch.d.ts +42 -0
  57. package/dist/firebase/popupWatch.js +140 -0
  58. package/dist/testing.d.ts +38 -0
  59. package/dist/testing.js +116 -0
  60. package/dist/web/crossTabRefresh.d.ts +4 -2
  61. package/dist/web/crossTabRefresh.js +13 -0
  62. package/dist/workspaceLink.d.ts +45 -0
  63. package/dist/workspaceLink.js +78 -0
  64. package/package.json +30 -6
package/dist/testing.js CHANGED
@@ -1,3 +1,119 @@
1
1
  export { brandIdentity as createTestIdentity } from './core/identity.js';
2
2
  // Enumerating wording (CWE-204) each host asserts its copy never matches; shared so the two suites can't drift.
3
3
  export const ENUMERATION_ORACLE = /\bexists?\b|already (?:registered|in use|have|exists)|wrong password|no account|not found|is registered|different (?:sign-in method|credential)/i;
4
+ export function fakeWebSessionUser(overrides = {}) {
5
+ return {
6
+ id: 'user-1',
7
+ email: 'user-1@example.com',
8
+ name: 'Test User',
9
+ emailVerified: true,
10
+ signInProvider: 'google.com',
11
+ ...overrides
12
+ };
13
+ }
14
+ const FAKE_CSRF_TOKEN = 'fake-csrf-token';
15
+ const DAY_MS = 24 * 60 * 60 * 1000;
16
+ function jsonResponse(status, body) {
17
+ return new Response(JSON.stringify(body), {
18
+ status,
19
+ headers: { 'Content-Type': 'application/json' }
20
+ });
21
+ }
22
+ function errorResponse(status, code) {
23
+ return jsonResponse(status, { code, message: code });
24
+ }
25
+ function sessionBody(user, now) {
26
+ return {
27
+ user: {
28
+ id: user.id,
29
+ email: user.email,
30
+ name: user.name,
31
+ email_verified: user.emailVerified,
32
+ sign_in_provider: user.signInProvider
33
+ },
34
+ csrf_token: FAKE_CSRF_TOKEN,
35
+ expires_at: new Date(now() + DAY_MS).toISOString(),
36
+ absolute_expires_at: new Date(now() + 7 * DAY_MS).toISOString()
37
+ };
38
+ }
39
+ function routesFor(signInUser, now) {
40
+ const revoke = (endpoint) => {
41
+ endpoint.state = { kind: 'dead', code: 'session_revoked' };
42
+ };
43
+ return new Map([
44
+ [
45
+ 'POST /api/auth/session',
46
+ (endpoint, headers) => {
47
+ if (!headers.get('authorization')?.startsWith('Bearer ')) {
48
+ return errorResponse(401, 'no_session');
49
+ }
50
+ endpoint.state = { kind: 'live', user: signInUser };
51
+ return jsonResponse(200, { success: true, expiresIn: DAY_MS / 1000 });
52
+ }
53
+ ],
54
+ [
55
+ 'DELETE /api/auth/session',
56
+ (endpoint) => {
57
+ revoke(endpoint);
58
+ return jsonResponse(200, { success: true });
59
+ }
60
+ ],
61
+ [
62
+ 'GET /api/auth/session',
63
+ ({ state }) => state.kind === 'live'
64
+ ? jsonResponse(200, sessionBody(state.user, now))
65
+ : errorResponse(401, state.kind === 'dead' ? state.code : 'no_session')
66
+ ],
67
+ [
68
+ 'POST /api/auth/sessions/revoke-all',
69
+ (endpoint, headers) => {
70
+ if (!headers.get('authorization')?.startsWith('Bearer ')) {
71
+ return errorResponse(401, 'no_session');
72
+ }
73
+ if (endpoint.state.kind === 'dead') {
74
+ return errorResponse(401, endpoint.state.code);
75
+ }
76
+ if (headers.get('x-csrf-token') !== FAKE_CSRF_TOKEN) {
77
+ return errorResponse(403, 'csrf_invalid');
78
+ }
79
+ revoke(endpoint);
80
+ return jsonResponse(200, { revoked: 1 });
81
+ }
82
+ ]
83
+ ]);
84
+ }
85
+ /**
86
+ * A `fetch` serving ingest's session routes under `/api/auth`. POST with a
87
+ * bearer proof signs `signInUser` in; DELETE and revoke-all leave the cookie
88
+ * revoked, as the real endpoint does.
89
+ */
90
+ export function createFakeWebSessionEndpoint({ state, signInUser = fakeWebSessionUser(), now = Date.now }) {
91
+ const requests = [];
92
+ const routes = routesFor(signInUser, now);
93
+ const endpoint = {
94
+ state,
95
+ requests,
96
+ fetch: async (input, init = {}) => {
97
+ const method = (init.method ?? 'GET').toUpperCase();
98
+ const { pathname } = new URL(input instanceof Request ? input.url : String(input));
99
+ const headers = new Headers(init.headers);
100
+ requests.push({
101
+ method,
102
+ path: pathname,
103
+ credentials: init.credentials,
104
+ cache: init.cache,
105
+ headers: Object.fromEntries(headers)
106
+ });
107
+ const current = endpoint.state;
108
+ if (current.kind === 'network_error') {
109
+ throw new TypeError('Failed to fetch');
110
+ }
111
+ if (current.kind === 'unavailable') {
112
+ return errorResponse(current.status, 'unavailable');
113
+ }
114
+ const route = routes.get(`${method} ${pathname}`);
115
+ return route ? route(endpoint, headers) : errorResponse(404, 'not_found');
116
+ }
117
+ };
118
+ return endpoint;
119
+ }
@@ -1,4 +1,5 @@
1
- import type { CrossTabRefreshPort } from '../core/session.js';
1
+ import type { AccountCredential, CrossTabRefreshPort } from '../core/session.js';
2
+ import type { VisibilityPort } from '../core/webSessionIdentity.js';
2
3
  /**
3
4
  * Web Locks + BroadcastChannel implementation of the cross-tab refresh port.
4
5
  * The coordination key names both the lock and the channel. Resolves to
@@ -15,4 +16,5 @@ import type { CrossTabRefreshPort } from '../core/session.js';
15
16
  * can do to siblings (hand them a token), not what it can reach. Hosts that
16
17
  * load third-party script into the realm accept that when they opt in.
17
18
  */
18
- export declare function createWebCrossTabRefreshPort(): CrossTabRefreshPort | undefined;
19
+ export declare function createWebCrossTabRefreshPort<Message = AccountCredential>(): CrossTabRefreshPort<Message> | undefined;
20
+ export declare function createWebVisibilityPort(): VisibilityPort | undefined;
@@ -119,3 +119,16 @@ export function createWebCrossTabRefreshPort() {
119
119
  }
120
120
  };
121
121
  }
122
+ export function createWebVisibilityPort() {
123
+ if (typeof document === 'undefined')
124
+ return undefined;
125
+ const isVisible = () => document.visibilityState === 'visible';
126
+ return {
127
+ isVisible,
128
+ onChange: (listener) => {
129
+ const handler = () => listener(isVisible());
130
+ document.addEventListener('visibilitychange', handler);
131
+ return () => document.removeEventListener('visibilitychange', handler);
132
+ }
133
+ };
134
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The workspace deep-link contract: the one query parameter, `workspace`,
3
+ * that every surface — the cloud app, comfy.org, and the billing SDK's entry
4
+ * URL — reads and writes to say which workspace a link is for.
5
+ *
6
+ * It is the URL parameter only, never the token API field. `POST
7
+ * /api/auth/token` still takes `{ workspace_id }` in its request body; that
8
+ * name does not change here and this module does not touch it.
9
+ *
10
+ * A destination applies the link before it loads workspace-scoped content:
11
+ * it mints a token for the named id and lets the server decide membership,
12
+ * the same way any other mint does. A payment or billing destination fails
13
+ * closed on a link it cannot honor and never acts on a workspace other than
14
+ * the one named — silently falling back to the caller's own active workspace
15
+ * would bill or expose the wrong one. A navigation destination may fall back
16
+ * to its own active workspace when the link is absent or invalid, but must
17
+ * show which workspace it landed on rather than switch silently. The
18
+ * parameter is carried through the sign-in redirect like any other query
19
+ * parameter on the path being returned to.
20
+ */
21
+ export declare const WORKSPACE_LINK_PARAM = "workspace";
22
+ export type WorkspaceLinkRead = {
23
+ readonly status: 'absent';
24
+ } | {
25
+ readonly status: 'ok';
26
+ readonly workspaceId: string;
27
+ } | {
28
+ readonly status: 'invalid';
29
+ };
30
+ export declare function isWorkspaceId(value: string): boolean;
31
+ /**
32
+ * Reads the link out of a URL, a `URLSearchParams`, or a string (a full URL
33
+ * or the path-and-query form a router hands over). A missing parameter is
34
+ * `absent`; a present-but-empty value is `invalid`, the same as any other
35
+ * value outside the shared charset — an empty `workspace=` is a producer
36
+ * bug, not "no link," and a payment destination has to fail closed on it
37
+ * rather than treat it as though the link were never there. A repeated
38
+ * parameter is also `invalid` rather than picking one, so a caller cannot be
39
+ * pointed at two different workspaces by ambiguity.
40
+ */
41
+ export declare function readWorkspaceLink(source: URL | URLSearchParams | string): WorkspaceLinkRead;
42
+ /** Returns a new `URL` carrying the link; never mutates `url`. Throws `RangeError` on an id outside the shared charset. */
43
+ export declare function withWorkspaceLink(url: URL, workspaceId: string): URL;
44
+ /** Returns a new `URL` with the link removed; never mutates `url`. */
45
+ export declare function withoutWorkspaceLink(url: URL): URL;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The workspace deep-link contract: the one query parameter, `workspace`,
3
+ * that every surface — the cloud app, comfy.org, and the billing SDK's entry
4
+ * URL — reads and writes to say which workspace a link is for.
5
+ *
6
+ * It is the URL parameter only, never the token API field. `POST
7
+ * /api/auth/token` still takes `{ workspace_id }` in its request body; that
8
+ * name does not change here and this module does not touch it.
9
+ *
10
+ * A destination applies the link before it loads workspace-scoped content:
11
+ * it mints a token for the named id and lets the server decide membership,
12
+ * the same way any other mint does. A payment or billing destination fails
13
+ * closed on a link it cannot honor and never acts on a workspace other than
14
+ * the one named — silently falling back to the caller's own active workspace
15
+ * would bill or expose the wrong one. A navigation destination may fall back
16
+ * to its own active workspace when the link is absent or invalid, but must
17
+ * show which workspace it landed on rather than switch silently. The
18
+ * parameter is carried through the sign-in redirect like any other query
19
+ * parameter on the path being returned to.
20
+ */
21
+ export const WORKSPACE_LINK_PARAM = 'workspace';
22
+ /** Same charset as `@comfyorg/billing-contract`'s identifiers: no path separator, query delimiter, or percent escape survives it. */
23
+ const WORKSPACE_ID = /^[A-Za-z0-9_-]{1,128}$/;
24
+ export function isWorkspaceId(value) {
25
+ return WORKSPACE_ID.test(value);
26
+ }
27
+ /** Reserved by RFC 2606; only used to parse a path+query string that carries no origin of its own. */
28
+ const PARSE_BASE = 'https://workspace-link.invalid/';
29
+ function searchParamsOf(source) {
30
+ if (source instanceof URLSearchParams)
31
+ return source;
32
+ if (source instanceof URL)
33
+ return source.searchParams;
34
+ try {
35
+ return new URL(source, PARSE_BASE).searchParams;
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ /**
42
+ * Reads the link out of a URL, a `URLSearchParams`, or a string (a full URL
43
+ * or the path-and-query form a router hands over). A missing parameter is
44
+ * `absent`; a present-but-empty value is `invalid`, the same as any other
45
+ * value outside the shared charset — an empty `workspace=` is a producer
46
+ * bug, not "no link," and a payment destination has to fail closed on it
47
+ * rather than treat it as though the link were never there. A repeated
48
+ * parameter is also `invalid` rather than picking one, so a caller cannot be
49
+ * pointed at two different workspaces by ambiguity.
50
+ */
51
+ export function readWorkspaceLink(source) {
52
+ const params = searchParamsOf(source);
53
+ if (!params)
54
+ return { status: 'absent' };
55
+ const values = params.getAll(WORKSPACE_LINK_PARAM);
56
+ if (values.length === 0)
57
+ return { status: 'absent' };
58
+ if (values.length > 1)
59
+ return { status: 'invalid' };
60
+ const [value] = values;
61
+ if (!isWorkspaceId(value))
62
+ return { status: 'invalid' };
63
+ return { status: 'ok', workspaceId: value };
64
+ }
65
+ /** Returns a new `URL` carrying the link; never mutates `url`. Throws `RangeError` on an id outside the shared charset. */
66
+ export function withWorkspaceLink(url, workspaceId) {
67
+ if (!isWorkspaceId(workspaceId))
68
+ throw new RangeError(`not a workspace id: ${workspaceId}`);
69
+ const next = new URL(url.href);
70
+ next.searchParams.set(WORKSPACE_LINK_PARAM, workspaceId);
71
+ return next;
72
+ }
73
+ /** Returns a new `URL` with the link removed; never mutates `url`. */
74
+ export function withoutWorkspaceLink(url) {
75
+ const next = new URL(url.href);
76
+ next.searchParams.delete(WORKSPACE_LINK_PARAM);
77
+ return next;
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comfyorg/account-core",
3
- "version": "1.0.0-alpha.0",
3
+ "version": "1.0.0-alpha.2",
4
4
  "description": "Shared account layer: a framework-free session core that exchanges a signed-in identity for the workspace-scoped Cloud JWT with valid-on-read freshness, plus the package-owned Firebase identity entry. It has no view layer and no Vue dependency; the composables and unstyled components ship from @comfyorg/account-ui",
5
5
  "keywords": [],
6
6
  "homepage": "https://comfy.org",
@@ -29,6 +29,26 @@
29
29
  "types": "./dist/core/session.d.ts",
30
30
  "import": "./dist/core/session.js"
31
31
  },
32
+ "./webSession": {
33
+ "types": "./dist/core/webSession.d.ts",
34
+ "import": "./dist/core/webSession.js"
35
+ },
36
+ "./webSessionFlag": {
37
+ "types": "./dist/core/webSessionFlag.d.ts",
38
+ "import": "./dist/core/webSessionFlag.js"
39
+ },
40
+ "./requestAuth": {
41
+ "types": "./dist/core/requestAuth.d.ts",
42
+ "import": "./dist/core/requestAuth.js"
43
+ },
44
+ "./sessionTokenMint": {
45
+ "types": "./dist/core/sessionTokenMint.d.ts",
46
+ "import": "./dist/core/sessionTokenMint.js"
47
+ },
48
+ "./webSessionIdentity": {
49
+ "types": "./dist/core/webSessionIdentity.d.ts",
50
+ "import": "./dist/core/webSessionIdentity.js"
51
+ },
32
52
  "./billing": {
33
53
  "types": "./dist/core/billing/index.d.ts",
34
54
  "import": "./dist/core/billing/index.js"
@@ -73,6 +93,10 @@
73
93
  "types": "./dist/redirect.d.ts",
74
94
  "import": "./dist/redirect.js"
75
95
  },
96
+ "./workspaceLink": {
97
+ "types": "./dist/workspaceLink.d.ts",
98
+ "import": "./dist/workspaceLink.js"
99
+ },
76
100
  "./telemetry": {
77
101
  "types": "./dist/telemetry.d.ts",
78
102
  "import": "./dist/telemetry.js"
@@ -87,15 +111,15 @@
87
111
  "access": "public"
88
112
  },
89
113
  "dependencies": {
90
- "@comfyorg/ingest-types": "^1.0.0"
114
+ "@comfyorg/ingest-types": "^1.1.0"
91
115
  },
92
116
  "devDependencies": {
93
117
  "@types/node": "^25.9.5",
94
- "@vitest/coverage-v8": "^4.1.11",
118
+ "@vitest/coverage-v8": "^5.0.2",
95
119
  "firebase": "^11.6.0",
96
- "happy-dom": "20.9.0",
120
+ "happy-dom": "20.14.5",
97
121
  "typescript": "^6.0.3",
98
- "vitest": "^4.1.11",
122
+ "vitest": "^5.0.2",
99
123
  "zod": "^3.23.8"
100
124
  },
101
125
  "peerDependencies": {
@@ -109,6 +133,6 @@
109
133
  "lint": "oxlint src --type-aware && eslint src",
110
134
  "smoke:pack": "tsx scripts/smokePack.ts",
111
135
  "test": "vitest run",
112
- "typecheck": "tsc --noEmit && tsc -p scripts/tsconfig.json"
136
+ "typecheck": "tsx ../../scripts/vue-tsc-native.ts --noEmit && tsx ../../scripts/vue-tsc-native.ts -p scripts/tsconfig.json"
113
137
  }
114
138
  }