@undefineds.co/linx 0.3.20 → 0.3.23

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 (99) hide show
  1. package/dist/generated/version.js +1 -1
  2. package/dist/index.js +6 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/auto-mode/pod-persistence.js +53 -3
  5. package/dist/lib/auto-mode/pod-persistence.js.map +1 -1
  6. package/dist/lib/auto-mode/secretary.js +2 -2
  7. package/dist/lib/auto-mode/secretary.js.map +1 -1
  8. package/dist/lib/chat-api.js +23 -61
  9. package/dist/lib/chat-api.js.map +1 -1
  10. package/dist/lib/codex-plugin/index.js +1 -0
  11. package/dist/lib/codex-plugin/index.js.map +1 -1
  12. package/dist/lib/codex-plugin/symphony-mcp.js +335 -0
  13. package/dist/lib/codex-plugin/symphony-mcp.js.map +1 -0
  14. package/dist/lib/linx-cloud-errors.js +0 -5
  15. package/dist/lib/linx-cloud-errors.js.map +1 -1
  16. package/dist/lib/linx-status-line.js +1 -8
  17. package/dist/lib/linx-status-line.js.map +1 -1
  18. package/dist/lib/linx-tui-contract.js +2 -1
  19. package/dist/lib/linx-tui-contract.js.map +1 -1
  20. package/dist/lib/models.js +3 -2
  21. package/dist/lib/models.js.map +1 -1
  22. package/dist/lib/pi-adapter/auth.js +68 -0
  23. package/dist/lib/pi-adapter/auth.js.map +1 -0
  24. package/dist/lib/pi-adapter/branding.js +67 -110
  25. package/dist/lib/pi-adapter/branding.js.map +1 -1
  26. package/dist/lib/pi-adapter/interactive.js +341 -101
  27. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  28. package/dist/lib/pi-adapter/pod-mirror.js +38 -107
  29. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -1
  30. package/dist/lib/pi-adapter/pod-native.js +2 -0
  31. package/dist/lib/pi-adapter/pod-native.js.map +1 -1
  32. package/dist/lib/pi-adapter/pod-tools.js +140 -0
  33. package/dist/lib/pi-adapter/pod-tools.js.map +1 -0
  34. package/dist/lib/pi-adapter/runtime.js +2 -12
  35. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  36. package/dist/lib/pi-adapter/session.js +13 -17
  37. package/dist/lib/pi-adapter/session.js.map +1 -1
  38. package/dist/lib/pi-adapter/stream.js +2 -20
  39. package/dist/lib/pi-adapter/stream.js.map +1 -1
  40. package/dist/lib/pod-chat-store.js +53 -4
  41. package/dist/lib/pod-chat-store.js.map +1 -1
  42. package/dist/lib/resource-identity.js +2 -0
  43. package/dist/lib/resource-identity.js.map +1 -0
  44. package/dist/lib/status-line-command.js +2 -2
  45. package/dist/lib/status-line-command.js.map +1 -1
  46. package/dist/lib/symphony/archive.js +15 -37
  47. package/dist/lib/symphony/archive.js.map +1 -1
  48. package/dist/lib/symphony/pod-projection.js +189 -1346
  49. package/dist/lib/symphony/pod-projection.js.map +1 -1
  50. package/dist/lib/symphony-command.js +209 -109
  51. package/dist/lib/symphony-command.js.map +1 -1
  52. package/dist/plugins/linx-symphony-codex/.codex-plugin/plugin.json +38 -0
  53. package/dist/plugins/linx-symphony-codex/.mcp.json +10 -0
  54. package/dist/plugins/linx-symphony-codex/README.md +9 -0
  55. package/dist/plugins/linx-symphony-codex/hooks.json +60 -0
  56. package/dist/plugins/linx-symphony-codex/scripts/symphony-hook-events.mjs +119 -0
  57. package/dist/plugins/linx-symphony-codex/scripts/symphony-mcp.mjs +335 -0
  58. package/dist/plugins/linx-symphony-codex/skills/symphony/SKILL.md +791 -0
  59. package/dist/skills/symphony/SKILL.md +7 -0
  60. package/dist/skills/xpod-cli/SKILL.md +2 -13
  61. package/package.json +4 -4
  62. package/vendor/agent-runtime/dist/chat-reconciler.d.ts +33 -0
  63. package/vendor/agent-runtime/dist/chat-reconciler.js +108 -0
  64. package/vendor/agent-runtime/dist/index.d.ts +4 -1
  65. package/vendor/agent-runtime/dist/index.js +4 -1
  66. package/vendor/agent-runtime/dist/matrix-client.d.ts +149 -0
  67. package/vendor/agent-runtime/dist/matrix-client.js +220 -0
  68. package/vendor/agent-runtime/dist/pod-resource-identity.d.ts +17 -0
  69. package/vendor/agent-runtime/dist/pod-resource-identity.js +54 -0
  70. package/vendor/agent-runtime/dist/reconciler.d.ts +0 -11
  71. package/vendor/agent-runtime/dist/reconciler.js +5 -43
  72. package/vendor/agent-runtime/dist/symphony.d.ts +272 -27
  73. package/vendor/agent-runtime/dist/symphony.js +1268 -21
  74. package/vendor/agent-runtime/dist/workspace.d.ts +61 -0
  75. package/vendor/agent-runtime/dist/workspace.js +81 -0
  76. package/vendor/agent-runtime/package.json +5 -1
  77. package/vendor/stores/dist/current-pod-base.d.ts +2 -0
  78. package/vendor/stores/dist/current-pod-base.js +14 -0
  79. package/vendor/stores/dist/exact-records.d.ts +7 -0
  80. package/vendor/stores/dist/exact-records.js +87 -0
  81. package/vendor/stores/dist/index.d.ts +1 -0
  82. package/vendor/stores/dist/index.js +1 -0
  83. package/vendor/stores/dist/login.d.ts +51 -0
  84. package/vendor/stores/dist/login.js +195 -0
  85. package/vendor/stores/dist/pod-collection.d.ts +28 -0
  86. package/vendor/stores/dist/pod-collection.js +194 -0
  87. package/vendor/stores/dist/pod-write-guard.d.ts +5 -0
  88. package/vendor/stores/dist/pod-write-guard.js +133 -0
  89. package/vendor/stores/dist/symphony-control.d.ts +245 -0
  90. package/vendor/stores/dist/symphony-control.js +2175 -0
  91. package/vendor/stores/package.json +14 -0
  92. package/dist/lib/capture/persistence.js +0 -377
  93. package/dist/lib/capture/persistence.js.map +0 -1
  94. package/dist/lib/capture/tool.js +0 -242
  95. package/dist/lib/capture/tool.js.map +0 -1
  96. package/dist/skills/basic/SKILL.md +0 -46
  97. package/dist/skills/capture/SKILL.md +0 -165
  98. package/vendor/agent-runtime/dist/coordination.d.ts +0 -93
  99. package/vendor/agent-runtime/dist/coordination.js +0 -145
@@ -0,0 +1,61 @@
1
+ export type RuntimeWorkspaceKind = 'local-folder' | 'local-worktree' | 'pod-container';
2
+ export type AgentWorkspaceKind = 'folder' | 'git' | 'worktree';
3
+ export interface AgentWorkspaceEnvironment {
4
+ kind: 'local-shell' | 'remote-container' | 'cloud-runner' | 'backend-runtime' | 'unknown';
5
+ id?: string;
6
+ label?: string;
7
+ runtime?: string;
8
+ }
9
+ export interface AgentWorkspace {
10
+ /**
11
+ * Stable container identity.
12
+ *
13
+ * - Solid Pod containers should use their dereferenceable HTTP(S) LDP IRI.
14
+ * - Device-local containers should use a device-scoped URI such as
15
+ * `linx://device-0000/Users/alice/repo`.
16
+ *
17
+ * Runtime adapters may derive a process-local path from this value, but the
18
+ * path is not the shared identity.
19
+ */
20
+ container?: string;
21
+ path?: string;
22
+ kind: AgentWorkspaceKind;
23
+ repository?: string;
24
+ branch?: string;
25
+ worktree?: string;
26
+ baseRevision?: string;
27
+ environment?: AgentWorkspaceEnvironment;
28
+ }
29
+ export interface RuntimeWorkspaceInput {
30
+ workspaceKind?: unknown;
31
+ container?: string | null;
32
+ repoPath?: string | null;
33
+ folderPath?: string | null;
34
+ baseRef?: string | null;
35
+ branch?: string | null;
36
+ }
37
+ export interface NormalizedRuntimeWorkspaceInput {
38
+ workspaceKind: RuntimeWorkspaceKind;
39
+ container?: string;
40
+ repoPath?: string;
41
+ folderPath?: string;
42
+ baseRef: string;
43
+ branch?: string;
44
+ }
45
+ export interface NormalizeRuntimeWorkspaceOptions {
46
+ normalizeLocalPath?: (value?: string | null) => string;
47
+ defaultBaseRef?: string;
48
+ }
49
+ export interface RuntimeWorkspaceSessionLike {
50
+ cwd?: string | null;
51
+ repoPath?: string | null;
52
+ folderPath?: string | null;
53
+ container?: string | null;
54
+ }
55
+ export declare function isRuntimeWorkspaceKind(value: unknown): value is RuntimeWorkspaceKind;
56
+ export declare function isHttpContainerRef(value?: string | null): boolean;
57
+ export declare const isHttpWorkspaceRef: typeof isHttpContainerRef;
58
+ export declare function inferRuntimeWorkspaceKind(input: RuntimeWorkspaceInput, options?: NormalizeRuntimeWorkspaceOptions): RuntimeWorkspaceKind;
59
+ export declare function normalizeRuntimeWorkspaceInput(input: RuntimeWorkspaceInput, options?: NormalizeRuntimeWorkspaceOptions): NormalizedRuntimeWorkspaceInput;
60
+ export declare function isRuntimeSessionInWorkspace(session: RuntimeWorkspaceSessionLike, workspacePath: string, options?: NormalizeRuntimeWorkspaceOptions): boolean;
61
+ export declare function filterRuntimeSessionsForWorkspace<T extends RuntimeWorkspaceSessionLike>(sessions: T[], workspacePath: string, options?: NormalizeRuntimeWorkspaceOptions): T[];
@@ -0,0 +1,81 @@
1
+ function trim(value) {
2
+ return value?.trim() || '';
3
+ }
4
+ function normalizeLocalPath(value) {
5
+ return trim(value);
6
+ }
7
+ export function isRuntimeWorkspaceKind(value) {
8
+ return value === 'local-folder' || value === 'local-worktree' || value === 'pod-container';
9
+ }
10
+ export function isHttpContainerRef(value) {
11
+ const candidate = trim(value);
12
+ if (!candidate)
13
+ return false;
14
+ try {
15
+ const url = new URL(candidate);
16
+ return url.protocol === 'http:' || url.protocol === 'https:';
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ export const isHttpWorkspaceRef = isHttpContainerRef;
23
+ export function inferRuntimeWorkspaceKind(input, options = {}) {
24
+ if (isRuntimeWorkspaceKind(input.workspaceKind)) {
25
+ return input.workspaceKind;
26
+ }
27
+ const normalize = options.normalizeLocalPath ?? normalizeLocalPath;
28
+ const repoPath = normalize(input.repoPath);
29
+ if (isHttpContainerRef(input.container) && !repoPath) {
30
+ return 'pod-container';
31
+ }
32
+ const folderPath = normalize(input.folderPath);
33
+ return repoPath && folderPath && folderPath !== repoPath ? 'local-worktree' : 'local-folder';
34
+ }
35
+ export function normalizeRuntimeWorkspaceInput(input, options = {}) {
36
+ const normalize = options.normalizeLocalPath ?? normalizeLocalPath;
37
+ const workspaceKind = inferRuntimeWorkspaceKind(input, options);
38
+ const container = trim(input.container) || undefined;
39
+ const repoPath = normalize(input.repoPath) || undefined;
40
+ const folderPathInput = normalize(input.folderPath) || undefined;
41
+ const baseRef = trim(input.baseRef) || options.defaultBaseRef || 'HEAD';
42
+ const branch = trim(input.branch) || undefined;
43
+ if (workspaceKind === 'pod-container') {
44
+ if (!container || !isHttpContainerRef(container)) {
45
+ throw new Error('Pod workspace session requires an http(s) container.');
46
+ }
47
+ return {
48
+ workspaceKind,
49
+ container,
50
+ repoPath,
51
+ folderPath: folderPathInput,
52
+ baseRef,
53
+ branch,
54
+ };
55
+ }
56
+ if (!repoPath) {
57
+ throw new Error('Local runtime session requires repoPath.');
58
+ }
59
+ const folderPath = folderPathInput || repoPath;
60
+ return {
61
+ workspaceKind: folderPath !== repoPath ? 'local-worktree' : 'local-folder',
62
+ container,
63
+ repoPath,
64
+ folderPath,
65
+ baseRef,
66
+ branch,
67
+ };
68
+ }
69
+ export function isRuntimeSessionInWorkspace(session, workspacePath, options = {}) {
70
+ const normalize = options.normalizeLocalPath ?? normalizeLocalPath;
71
+ const expected = normalize(workspacePath);
72
+ if (!expected)
73
+ return false;
74
+ const candidates = [session.cwd, session.folderPath, session.repoPath]
75
+ .map((value) => normalize(value))
76
+ .filter(Boolean);
77
+ return candidates.some((candidate) => candidate === expected);
78
+ }
79
+ export function filterRuntimeSessionsForWorkspace(sessions, workspacePath, options = {}) {
80
+ return sessions.filter((session) => isRuntimeSessionInWorkspace(session, workspacePath, options));
81
+ }
@@ -6,15 +6,19 @@
6
6
  ".": "./dist/index.js",
7
7
  "./acp": "./dist/acp.js",
8
8
  "./auto-mode": "./dist/auto-mode.js",
9
+ "./chat-reconciler": "./dist/chat-reconciler.js",
9
10
  "./companion-model": "./dist/companion-model.js",
10
11
  "./control-plane": "./dist/control-plane.js",
11
12
  "./file-sync": "./dist/file-sync.js",
13
+ "./matrix-client": "./dist/matrix-client.js",
14
+ "./pod-resource-identity": "./dist/pod-resource-identity.js",
12
15
  "./reconciler": "./dist/reconciler.js",
13
16
  "./runtime": "./dist/runtime.js",
14
17
  "./symphony": "./dist/symphony.js",
15
18
  "./sync": "./dist/sync.js",
16
19
  "./thread-reconciler-controller": "./dist/thread-reconciler-controller.js",
17
20
  "./turn-controller": "./dist/turn-controller.js",
18
- "./wake-scheduler": "./dist/wake-scheduler.js"
21
+ "./wake-scheduler": "./dist/wake-scheduler.js",
22
+ "./workspace": "./dist/workspace.js"
19
23
  }
20
24
  }
@@ -0,0 +1,2 @@
1
+ import type { SolidDatabase } from '@undefineds.co/models';
2
+ export declare function resolveCurrentPodBaseUrl(db: SolidDatabase): string | null;
@@ -0,0 +1,14 @@
1
+ export function resolveCurrentPodBaseUrl(db) {
2
+ const podUrl = db.getDialect?.()?.getPodUrl?.() ?? db.getPodUrl?.();
3
+ return normalizePodBaseUrl(podUrl);
4
+ }
5
+ function normalizePodBaseUrl(url) {
6
+ if (typeof url !== 'string') {
7
+ return null;
8
+ }
9
+ const trimmed = url.trim();
10
+ if (!trimmed) {
11
+ return null;
12
+ }
13
+ return trimmed.replace(/\/+$/, '');
14
+ }
@@ -0,0 +1,7 @@
1
+ import type { PodResource, SolidDatabase } from '@undefineds.co/drizzle-solid';
2
+ type ExactRecordTarget = string | Record<string, unknown> | null | undefined;
3
+ type ExactPodResource = PodResource<any>;
4
+ export declare function findExactRecord<T>(db: SolidDatabase, resource: ExactPodResource, target: ExactRecordTarget): Promise<T | null>;
5
+ export declare function updateExactRecord(db: SolidDatabase, resource: ExactPodResource, target: ExactRecordTarget, updates: Record<string, unknown>): Promise<void>;
6
+ export declare function deleteExactRecord(db: SolidDatabase, resource: ExactPodResource, target: ExactRecordTarget): Promise<void>;
7
+ export {};
@@ -0,0 +1,87 @@
1
+ import { asBaseRelativeResourceId, requireRowResourceId } from '../../agent-runtime/dist/pod-resource-identity.js';
2
+ import { assertCurrentPodBaseUrl, assertIriBelongsToCurrentPod, assertUpdateValuesBelongToCurrentPod } from './pod-write-guard.js';
3
+ const ABSOLUTE_IRI = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
4
+ const INTERNAL_FIELDS = new Set(['id', '@id', 'subject', 'uri', 'source']);
5
+ export async function findExactRecord(db, resource, target) {
6
+ const locatorDb = db;
7
+ const id = resolveRecordId(target);
8
+ if (id && typeof locatorDb.findById === 'function') {
9
+ return locatorDb.findById(resource, id);
10
+ }
11
+ const iri = resolveRecordIri(target);
12
+ if (iri && typeof locatorDb.findByIri === 'function') {
13
+ return locatorDb.findByIri(resource, iri);
14
+ }
15
+ return null;
16
+ }
17
+ export async function updateExactRecord(db, resource, target, updates) {
18
+ const locatorDb = db;
19
+ const payload = sanitizeUpdatePayload(updates);
20
+ assertCurrentPodBaseUrl(db, 'update');
21
+ assertUpdateValuesBelongToCurrentPod(db, payload);
22
+ const id = resolveRecordId(target);
23
+ if (id && typeof locatorDb.updateById === 'function') {
24
+ await locatorDb.updateById(resource, id, payload);
25
+ return;
26
+ }
27
+ const iri = resolveRecordIri(target);
28
+ if (iri) {
29
+ assertIriBelongsToCurrentPod(db, iri, 'update');
30
+ }
31
+ if (iri && typeof locatorDb.updateByIri === 'function') {
32
+ await locatorDb.updateByIri(resource, iri, payload);
33
+ return;
34
+ }
35
+ throw new Error('Cannot update exact record without updateById/updateByIri support.');
36
+ }
37
+ export async function deleteExactRecord(db, resource, target) {
38
+ const locatorDb = db;
39
+ assertCurrentPodBaseUrl(db, 'delete');
40
+ const id = resolveRecordId(target);
41
+ if (id && typeof locatorDb.deleteById === 'function') {
42
+ await locatorDb.deleteById(resource, id);
43
+ return;
44
+ }
45
+ const iri = resolveRecordIri(target);
46
+ if (iri) {
47
+ assertIriBelongsToCurrentPod(db, iri, 'delete');
48
+ }
49
+ if (iri && typeof locatorDb.deleteByIri === 'function') {
50
+ await locatorDb.deleteByIri(resource, iri);
51
+ return;
52
+ }
53
+ throw new Error('Cannot delete exact record without deleteById/deleteByIri support.');
54
+ }
55
+ function resolveRecordIri(target) {
56
+ if (typeof target === 'string') {
57
+ return ABSOLUTE_IRI.test(target) ? target : null;
58
+ }
59
+ if (!target)
60
+ return null;
61
+ for (const key of ['@id', 'uri', 'subject', 'source']) {
62
+ const value = target[key];
63
+ if (typeof value === 'string' && ABSOLUTE_IRI.test(value)) {
64
+ return value;
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+ function resolveRecordId(target) {
70
+ if (typeof target === 'string') {
71
+ return ABSOLUTE_IRI.test(target)
72
+ ? null
73
+ : asBaseRelativeResourceId(target, 'record target');
74
+ }
75
+ if (!target)
76
+ return null;
77
+ return requireRowResourceId(target, 'record target');
78
+ }
79
+ function sanitizeUpdatePayload(updates) {
80
+ const payload = {};
81
+ for (const [key, value] of Object.entries(updates)) {
82
+ if (!INTERNAL_FIELDS.has(key) && value !== undefined) {
83
+ payload[key] = value;
84
+ }
85
+ }
86
+ return payload;
87
+ }
@@ -0,0 +1 @@
1
+ export * from './login';
@@ -0,0 +1 @@
1
+ export * from './login.js';
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 登录状态机
3
+ *
4
+ * restoring → idle → connecting → authenticated
5
+ */
6
+ export type LoginState = 'restoring' | 'idle' | 'connecting' | 'authenticated';
7
+ export interface StoredAccount {
8
+ displayName: string;
9
+ avatarUrl?: string;
10
+ issuerUrl: string;
11
+ issuerLabel?: string;
12
+ storageProviderUrl?: string;
13
+ storageProviderLabel?: string;
14
+ webId?: string;
15
+ }
16
+ export interface ProviderOption {
17
+ id: string;
18
+ url: string;
19
+ label: string;
20
+ logoUrl?: string;
21
+ isDefault?: boolean;
22
+ }
23
+ export declare function getRememberedAccount(): StoredAccount | null;
24
+ export interface LoginStore {
25
+ state: LoginState;
26
+ error: string | null;
27
+ storedAccount: StoredAccount | null;
28
+ customProviders: ProviderOption[];
29
+ setState: (state: LoginState) => void;
30
+ setError: (error: string | null) => void;
31
+ setStoredAccount: (account: StoredAccount | null) => void;
32
+ addCustomProvider: (provider: ProviderOption) => void;
33
+ removeCustomProvider: (url: string) => void;
34
+ loginSuccess: (account: StoredAccount) => void;
35
+ reset: () => void;
36
+ }
37
+ export declare const useLoginStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<LoginStore>, "setState" | "persist"> & {
38
+ setState(partial: LoginStore | Partial<LoginStore> | ((state: LoginStore) => LoginStore | Partial<LoginStore>), replace?: false | undefined): unknown;
39
+ setState(state: LoginStore | ((state: LoginStore) => LoginStore), replace: true): unknown;
40
+ persist: {
41
+ setOptions: (options: Partial<import("zustand/middleware").PersistOptions<LoginStore, unknown, unknown>>) => void;
42
+ clearStorage: () => void;
43
+ rehydrate: () => Promise<void> | void;
44
+ hasHydrated: () => boolean;
45
+ onHydrate: (fn: (state: LoginStore) => void) => () => void;
46
+ onFinishHydration: (fn: (state: LoginStore) => void) => () => void;
47
+ getOptions: () => Partial<import("zustand/middleware").PersistOptions<LoginStore, unknown, unknown>>;
48
+ };
49
+ }>;
50
+ export declare const DEFAULT_PROVIDERS: ProviderOption[];
51
+ export declare function getAllProviders(customProviders: ProviderOption[]): ProviderOption[];
@@ -0,0 +1,195 @@
1
+ import { create } from 'zustand';
2
+ import { persist, createJSONStorage } from 'zustand/middleware';
3
+ const REMEMBERED_ACCOUNT_KEY = 'linx-remembered-account';
4
+ const LINX_CLOUD_IDENTITY_ORIGIN = 'https://id.undefineds.co';
5
+ function getBrowserStorage() {
6
+ if (typeof window === 'undefined')
7
+ return null;
8
+ return window.localStorage ?? null;
9
+ }
10
+ function persistRememberedAccount(account) {
11
+ const storage = getBrowserStorage();
12
+ if (!storage)
13
+ return;
14
+ if (!account) {
15
+ storage.removeItem(REMEMBERED_ACCOUNT_KEY);
16
+ return;
17
+ }
18
+ storage.setItem(REMEMBERED_ACCOUNT_KEY, JSON.stringify(account));
19
+ }
20
+ export function getRememberedAccount() {
21
+ const storage = getBrowserStorage();
22
+ if (!storage)
23
+ return null;
24
+ const raw = storage.getItem(REMEMBERED_ACCOUNT_KEY);
25
+ if (!raw)
26
+ return null;
27
+ try {
28
+ const parsed = JSON.parse(raw);
29
+ const storageProviderLabel = resolveStorageProviderLabel(parsed);
30
+ const issuerUrl = resolveStoredAccountIssuerUrl(parsed, storageProviderLabel);
31
+ if (typeof parsed.displayName !== 'string' || !issuerUrl) {
32
+ return null;
33
+ }
34
+ return {
35
+ displayName: parsed.displayName,
36
+ avatarUrl: typeof parsed.avatarUrl === 'string' ? parsed.avatarUrl : undefined,
37
+ issuerUrl,
38
+ issuerLabel: typeof parsed.issuerLabel === 'string' ? parsed.issuerLabel : undefined,
39
+ storageProviderUrl: typeof parsed.storageProviderUrl === 'string'
40
+ ? parsed.storageProviderUrl
41
+ : normalizeStoredUrl(parsed.providerUrl) ?? undefined,
42
+ storageProviderLabel,
43
+ webId: typeof parsed.webId === 'string' ? parsed.webId : undefined,
44
+ };
45
+ }
46
+ catch {
47
+ storage.removeItem(REMEMBERED_ACCOUNT_KEY);
48
+ return null;
49
+ }
50
+ }
51
+ function migrateStoredAccount(value) {
52
+ if (!value || typeof value !== 'object')
53
+ return null;
54
+ const parsed = value;
55
+ const storageProviderLabel = resolveStorageProviderLabel(parsed);
56
+ const issuerUrl = resolveStoredAccountIssuerUrl(parsed, storageProviderLabel);
57
+ const storageProviderUrl = normalizeStoredUrl(parsed.storageProviderUrl)
58
+ ?? normalizeStoredUrl(parsed.providerUrl);
59
+ if (typeof parsed.displayName !== 'string' || !issuerUrl) {
60
+ return null;
61
+ }
62
+ return {
63
+ displayName: parsed.displayName,
64
+ avatarUrl: typeof parsed.avatarUrl === 'string' ? parsed.avatarUrl : undefined,
65
+ issuerUrl,
66
+ issuerLabel: typeof parsed.issuerLabel === 'string' ? parsed.issuerLabel : undefined,
67
+ storageProviderUrl: storageProviderUrl ?? undefined,
68
+ storageProviderLabel,
69
+ webId: typeof parsed.webId === 'string' ? parsed.webId : undefined,
70
+ };
71
+ }
72
+ function resolveStoredAccountIssuerUrl(parsed, storageProviderLabel) {
73
+ const explicitIssuerUrl = normalizeStoredUrl(parsed.issuerUrl);
74
+ if (explicitIssuerUrl) {
75
+ return explicitIssuerUrl;
76
+ }
77
+ if (isLegacyCloudBackedLocalAccount(parsed, storageProviderLabel)) {
78
+ return LINX_CLOUD_IDENTITY_ORIGIN;
79
+ }
80
+ return normalizeStoredUrl(parsed.providerUrl);
81
+ }
82
+ function resolveStorageProviderLabel(parsed) {
83
+ if (typeof parsed.storageProviderLabel === 'string') {
84
+ return parsed.storageProviderLabel;
85
+ }
86
+ if (typeof parsed.providerLabel === 'string') {
87
+ return parsed.providerLabel;
88
+ }
89
+ return undefined;
90
+ }
91
+ function isLegacyCloudBackedLocalAccount(parsed, storageProviderLabel) {
92
+ return storageProviderLabel?.trim().toLowerCase() === 'local'
93
+ && typeof parsed.webId === 'string'
94
+ && normalizeStoredUrl(parsed.webId)?.startsWith(`${LINX_CLOUD_IDENTITY_ORIGIN}/`) === true;
95
+ }
96
+ // ============================================================================
97
+ // Store 实现
98
+ // ============================================================================
99
+ export const useLoginStore = create()(persist((set) => ({
100
+ // 初始状态
101
+ state: 'restoring',
102
+ error: null,
103
+ storedAccount: null,
104
+ customProviders: [],
105
+ // 基础 Actions
106
+ setState: (state) => set((current) => current.state === state ? current : { state }),
107
+ setError: (error) => set((current) => current.error === error ? current : { error }),
108
+ setStoredAccount: (account) => set((current) => {
109
+ if (current.storedAccount === account)
110
+ return current;
111
+ persistRememberedAccount(account);
112
+ return { storedAccount: account };
113
+ }),
114
+ addCustomProvider: (provider) => set((s) => ({
115
+ customProviders: [
116
+ provider,
117
+ ...s.customProviders.filter(p => p.url !== provider.url)
118
+ ].slice(0, 10)
119
+ })),
120
+ removeCustomProvider: (url) => set((s) => ({
121
+ customProviders: s.customProviders.filter(p => p.url !== url)
122
+ })),
123
+ // 复合 Actions
124
+ loginSuccess: (account) => set(() => {
125
+ persistRememberedAccount(account);
126
+ return {
127
+ state: 'authenticated',
128
+ error: null,
129
+ storedAccount: account,
130
+ };
131
+ }),
132
+ reset: () => set((current) => {
133
+ persistRememberedAccount(current.storedAccount);
134
+ return {
135
+ state: 'idle',
136
+ error: null,
137
+ };
138
+ }),
139
+ }), {
140
+ name: 'linx-login',
141
+ version: 1,
142
+ migrate: (persistedState) => {
143
+ const state = persistedState;
144
+ return {
145
+ storedAccount: migrateStoredAccount(state.storedAccount),
146
+ customProviders: Array.isArray(state.customProviders) ? state.customProviders : [],
147
+ };
148
+ },
149
+ storage: createJSONStorage(() => {
150
+ if (typeof window !== 'undefined' && window.localStorage) {
151
+ return window.localStorage;
152
+ }
153
+ // SSR fallback
154
+ const memory = {};
155
+ return {
156
+ getItem: (key) => memory[key] ?? null,
157
+ setItem: (key, value) => { memory[key] = value; },
158
+ removeItem: (key) => { delete memory[key]; },
159
+ };
160
+ }),
161
+ // 只持久化这些字段
162
+ partialize: (state) => ({
163
+ storedAccount: state.storedAccount,
164
+ customProviders: state.customProviders,
165
+ }),
166
+ }));
167
+ // ============================================================================
168
+ // 默认 Providers
169
+ // ============================================================================
170
+ export const DEFAULT_PROVIDERS = [
171
+ {
172
+ id: 'linx-cloud',
173
+ url: 'https://id.undefineds.co',
174
+ label: 'Cloud',
175
+ logoUrl: '/linx-logo.png',
176
+ isDefault: true,
177
+ },
178
+ ];
179
+ // ============================================================================
180
+ // 辅助函数
181
+ // ============================================================================
182
+ export function getAllProviders(customProviders) {
183
+ const map = new Map();
184
+ // 先添加默认的
185
+ DEFAULT_PROVIDERS.forEach(p => map.set(p.url, p));
186
+ // 再添加自定义的(会覆盖同 URL 的默认项)
187
+ customProviders.forEach(p => map.set(p.url, { ...p, isDefault: false }));
188
+ return Array.from(map.values());
189
+ }
190
+ function normalizeStoredUrl(url) {
191
+ if (typeof url !== 'string')
192
+ return null;
193
+ const trimmed = url.trim();
194
+ return trimmed.length > 0 ? trimmed : null;
195
+ }
@@ -0,0 +1,28 @@
1
+ import { QueryClient } from '@tanstack/react-query';
2
+ import type { SolidDatabase } from '@undefineds.co/models';
3
+ import type { PodResource as PodResourceSchema } from '@undefineds.co/drizzle-solid';
4
+ interface PodCollectionOptions<TResource, TData> {
5
+ resource: TResource;
6
+ queryKey: string[];
7
+ queryClient: QueryClient;
8
+ getDb: () => SolidDatabase<any> | null;
9
+ columns?: (keyof TData)[];
10
+ orderBy?: {
11
+ column: string;
12
+ direction?: 'asc' | 'desc';
13
+ };
14
+ getKey?: (item: TData) => string;
15
+ seed?: TData[] | (() => TData[]);
16
+ }
17
+ /**
18
+ * Creates a TanStack DB Collection synchronized with a Solid Pod resource.
19
+ * Includes support for real-time subscriptions via db.subscribe().
20
+ */
21
+ export declare function createPodCollection<TResource extends PodResourceSchema<any>, TData extends {
22
+ id?: string;
23
+ }, _TInsert = any>(options: PodCollectionOptions<TResource, TData>): import("@tanstack/db").Collection<TData, string, import("@tanstack/db").UtilsRecord, never, TData> & import("@tanstack/db").NonSingleResult & {
24
+ insert: (item: TData) => import("@tanstack/db").Transaction<Record<string, unknown>>;
25
+ subscribeToPod: (db: SolidDatabase<any>) => Promise<() => void>;
26
+ fetch: () => Promise<TData[]>;
27
+ };
28
+ export {};