@openfairygui/backend 0.2.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/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@openfairygui/backend",
3
+ "version": "0.2.0-alpha.0",
4
+ "description": "FairyGUI Headless Authoring SDK — stateful backend runtime and session services.",
5
+ "author": "OpenFairyGUI Contributors",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/OpenFairyGUI/OpenFairyGUI.git",
10
+ "directory": "packages/backend"
11
+ },
12
+ "homepage": "https://github.com/OpenFairyGUI/OpenFairyGUI#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/OpenFairyGUI/OpenFairYGUI/issues"
15
+ },
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "main": "./dist/index.cjs",
19
+ "module": "./dist/index.mjs",
20
+ "types": "./dist/index.d.mts",
21
+ "exports": {
22
+ "require": {
23
+ "types": "./dist/index.d.cts",
24
+ "default": "./dist/index.cjs"
25
+ },
26
+ "default": {
27
+ "types": "./dist/index.d.mts",
28
+ "default": "./dist/index.mjs"
29
+ }
30
+ },
31
+ "scripts": {
32
+ "build": "tsdown --format esm,cjs --platform node --external node:fs --external node:fs/promises --external node:path --env.PACKAGE_VERSION=$npm_package_version",
33
+ "build:watch": "tsdown --watch --format esm,cjs --platform node --env.PACKAGE_VERSION=$npm_package_version"
34
+ },
35
+ "files": [
36
+ "dist/",
37
+ "src/"
38
+ ],
39
+ "keywords": [
40
+ "fairygui",
41
+ "backend",
42
+ "session",
43
+ "authoring",
44
+ "runtime"
45
+ ],
46
+ "dependencies": {
47
+ "@openfairygui/core": "workspace:*",
48
+ "@openfairygui/functions": "workspace:*"
49
+ },
50
+ "devDependencies": {
51
+ "ava": "^7.0.0",
52
+ "tsx": "^4.0.0"
53
+ }
54
+ }
@@ -0,0 +1,32 @@
1
+ export const BACKEND_CONTRACT_VERSION = '1.1.0-p2' as const;
2
+ export const BACKEND_CAPABILITY_SCHEMA_VERSION = 2 as const;
3
+ export const BACKEND_COMPATIBILITY_POLICY = {
4
+ incompatibleChange: 'requires contractVersion bump',
5
+ capabilitySchemaChange: 'requires capabilitySchemaVersion bump',
6
+ additiveChange: 'allowed without breaking existing consumers',
7
+ } as const;
8
+
9
+ export type BackendStage = 'read' | 'authoring' | 'runtime';
10
+
11
+ export interface BackendMessage {
12
+ code: string;
13
+ message: string;
14
+ }
15
+
16
+ export interface BackendDiagnostic {
17
+ code: string;
18
+ message: string;
19
+ severity: 'info' | 'warning' | 'error';
20
+ }
21
+
22
+ export interface BackendResponseMeta {
23
+ requestId: string;
24
+ sessionId?: string;
25
+ revision?: number;
26
+ durationMs: number;
27
+ warnings: BackendMessage[];
28
+ diagnostics: BackendDiagnostic[];
29
+ stage: BackendStage;
30
+ contractVersion: typeof BACKEND_CONTRACT_VERSION;
31
+ capabilitySchemaVersion: typeof BACKEND_CAPABILITY_SCHEMA_VERSION;
32
+ }
package/src/index.ts ADDED
@@ -0,0 +1,50 @@
1
+ export {
2
+ BackendRuntime,
3
+ createNodeBackendFileSystem,
4
+ type AdvisoryLockConflictError,
5
+ type ApplySessionTransactionInput,
6
+ type BackendCacheEntry,
7
+ type BackendCacheSnapshot,
8
+ type BackendCapabilities,
9
+ type BackendError,
10
+ type BackendEvent,
11
+ type BackendEventKind,
12
+ type BackendFailure,
13
+ type BackendFileHandle,
14
+ type BackendFileSystem,
15
+ type BackendJobKind,
16
+ type BackendJobListSnapshot,
17
+ type BackendJobListStatusFilter,
18
+ type BackendJobNotCancellableError,
19
+ type BackendJobNotFoundError,
20
+ type BackendJobProgress,
21
+ type BackendJobSnapshot,
22
+ type BackendJobStatus,
23
+ type BackendJobErrors,
24
+ type BackendResult,
25
+ type BackendSessionSnapshot,
26
+ type BackendSuccess,
27
+ type BackendRuntimeOptions,
28
+ type CacheRefreshFailedError,
29
+ type CancelJobInput,
30
+ type EventCursorInvalidError,
31
+ type GetCacheSnapshotInput,
32
+ type GetEventsInput,
33
+ type GetEventsSnapshot,
34
+ type GetJobInput,
35
+ type InProcessLockConflictError,
36
+ type ListJobsInput,
37
+ type RefreshCacheInput,
38
+ type SavePartialFailureError,
39
+ type SessionNotFoundError,
40
+ type SessionStaleWriteError,
41
+ } from './runtime.js';
42
+ export {
43
+ type BackendDiagnostic,
44
+ type BackendMessage,
45
+ type BackendResponseMeta,
46
+ type BackendStage,
47
+ BACKEND_CAPABILITY_SCHEMA_VERSION,
48
+ BACKEND_COMPATIBILITY_POLICY,
49
+ BACKEND_CONTRACT_VERSION,
50
+ } from './contracts.js';
@@ -0,0 +1,105 @@
1
+ import type { BackendCapabilities, BackendFileSystem } from './runtime.js';
2
+
3
+ export function normalizeComparablePath(value: string): string {
4
+ const normalized = value.replace(/[/\\]+$/, '').replace(/\\/g, '/');
5
+ const driveMatch = normalized.match(/^([a-z]:)(?:\/(.*))?$/i);
6
+ const drivePrefix = driveMatch?.[1].toLowerCase() ?? '';
7
+ const remainder = driveMatch ? (driveMatch[2] ?? '') : normalized;
8
+ const hasRoot = driveMatch ? true : remainder.startsWith('/');
9
+ const rawSegments = remainder.split('/').filter((segment) => segment.length > 0);
10
+ const segments: string[] = [];
11
+
12
+ for (const segment of rawSegments) {
13
+ if (segment === '.') continue;
14
+ if (segment === '..') {
15
+ if (segments.length > 0 && segments[segments.length - 1] !== '..') {
16
+ segments.pop();
17
+ } else if (!hasRoot) {
18
+ segments.push('..');
19
+ }
20
+ continue;
21
+ }
22
+ segments.push(segment);
23
+ }
24
+
25
+ const joined = segments.join('/');
26
+ const comparable = drivePrefix
27
+ ? `${drivePrefix}/${joined}`.replace(/\/$/, '')
28
+ : hasRoot
29
+ ? `/${joined}`.replace(/\/$/, '')
30
+ : joined || '.';
31
+ return comparable.toLowerCase();
32
+ }
33
+
34
+ export function createRuntimePathPolicy(): BackendCapabilities['runtime']['pathPolicy'] {
35
+ return {
36
+ canonicalization: 'realpath+normalized-casefold',
37
+ sessionIdentity: 'project-root',
38
+ saveTarget: 'opened-project-only',
39
+ outputTargets: 'deferred',
40
+ workspaceBoundary: 'project-root-only',
41
+ };
42
+ }
43
+
44
+ export async function resolveFairyPath(fileSystem: BackendFileSystem, input: string): Promise<string> {
45
+ const resolvedInput = fileSystem.resolve(input);
46
+ const stat = await fileSystem.stat(resolvedInput);
47
+
48
+ if (stat.isFile() && resolvedInput.endsWith('.fairy')) {
49
+ return await fileSystem.resolvePath(resolvedInput);
50
+ }
51
+
52
+ if (stat.isDirectory()) {
53
+ const entries = await fileSystem.readdir(resolvedInput);
54
+ const fairyFiles = entries.filter((entry) => entry.endsWith('.fairy'));
55
+ if (fairyFiles.length === 1) {
56
+ return await fileSystem.resolvePath(fileSystem.join(resolvedInput, fairyFiles[0]!));
57
+ }
58
+ if (fairyFiles.length > 1) {
59
+ throw new Error(`Multiple .fairy files found in ${resolvedInput}: ${fairyFiles.join(', ')}`);
60
+ }
61
+ throw new Error(`No .fairy file found in ${resolvedInput}`);
62
+ }
63
+
64
+ throw new Error(`Input is not a .fairy file or directory: ${resolvedInput}`);
65
+ }
66
+
67
+ export async function resolveCanonicalProjectRoot(fileSystem: BackendFileSystem, input: string): Promise<{
68
+ fairyPath: string;
69
+ canonicalProjectPath: string;
70
+ canonicalPathKey: string;
71
+ }> {
72
+ const fairyPath = await resolveFairyPath(fileSystem, input);
73
+ const canonicalProjectPath = await fileSystem.resolvePath(fileSystem.dirname(fairyPath));
74
+ return {
75
+ fairyPath,
76
+ canonicalProjectPath,
77
+ canonicalPathKey: normalizeComparablePath(canonicalProjectPath),
78
+ };
79
+ }
80
+
81
+ export interface PathPolicyViolationError {
82
+ code: 'path_policy_violation';
83
+ message: string;
84
+ policy: 'save_target';
85
+ attemptedPath: string;
86
+ allowedPath: string;
87
+ }
88
+
89
+ export async function validateSaveTarget(
90
+ fileSystem: BackendFileSystem,
91
+ openedFairyPath: string,
92
+ targetPath: string | undefined,
93
+ ): Promise<PathPolicyViolationError | null> {
94
+ if (!targetPath) return null;
95
+ const attemptedPath = await fileSystem.resolvePath(fileSystem.resolve(targetPath));
96
+ const allowedPath = await fileSystem.resolvePath(openedFairyPath);
97
+ if (normalizeComparablePath(attemptedPath) === normalizeComparablePath(allowedPath)) return null;
98
+ return {
99
+ code: 'path_policy_violation',
100
+ message: `Save target is restricted to the originally opened project file: ${allowedPath}`,
101
+ policy: 'save_target',
102
+ attemptedPath,
103
+ allowedPath,
104
+ };
105
+ }