@openfairygui/backend 0.2.0-alpha.0 → 0.2.0-alpha.10

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/src/node.ts ADDED
@@ -0,0 +1,96 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {
4
+ BackendRuntime,
5
+ type BackendFileHandle,
6
+ type BackendFileStat,
7
+ type BackendFileSystem,
8
+ type BackendHostAdapter,
9
+ type BackendRuntimeOptions,
10
+ } from './runtime.js';
11
+
12
+ export function createNodeBackendFileSystem(): BackendFileSystem {
13
+ return {
14
+ stat(filePath: string): Promise<BackendFileStat> {
15
+ return fs.stat(filePath);
16
+ },
17
+ readdir(dirPath: string): Promise<string[]> {
18
+ return fs.readdir(dirPath);
19
+ },
20
+ readFile(filePath: string): Promise<string> {
21
+ return fs.readFile(filePath, 'utf-8');
22
+ },
23
+ async readFileRaw(filePath: string): Promise<Uint8Array> {
24
+ const buffer = await fs.readFile(filePath);
25
+ return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
26
+ },
27
+ writeFile(filePath: string, content: string): Promise<void> {
28
+ return fs.writeFile(filePath, content, 'utf-8');
29
+ },
30
+ writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
31
+ return fs.writeFile(filePath, data);
32
+ },
33
+ async mkdir(dirPath: string, options?: { recursive?: boolean }): Promise<void> {
34
+ await fs.mkdir(dirPath, { recursive: options?.recursive ?? false });
35
+ },
36
+ async resolvePath(filePath: string): Promise<string> {
37
+ try {
38
+ return await fs.realpath(filePath);
39
+ } catch {
40
+ return path.resolve(filePath);
41
+ }
42
+ },
43
+ async openExclusive(filePath: string): Promise<BackendFileHandle> {
44
+ const handle = await fs.open(filePath, 'wx');
45
+ return {
46
+ writeFile(content: string): Promise<void> {
47
+ return handle.writeFile(content, 'utf-8');
48
+ },
49
+ close(): Promise<void> {
50
+ return handle.close();
51
+ },
52
+ };
53
+ },
54
+ unlink(filePath: string): Promise<void> {
55
+ return fs.unlink(filePath);
56
+ },
57
+ join(...paths: string[]): string {
58
+ return path.join(...paths);
59
+ },
60
+ dirname(filePath: string): string {
61
+ return path.dirname(filePath);
62
+ },
63
+ resolve(...paths: string[]): string {
64
+ return path.resolve(...paths);
65
+ },
66
+ };
67
+ }
68
+
69
+ export function createNodeBackendHostAdapter(): BackendHostAdapter {
70
+ return {
71
+ lockMetadata(input) {
72
+ return {
73
+ pid: process.pid,
74
+ createdAt: new Date().toISOString(),
75
+ canonicalPathKey: input.canonicalPathKey,
76
+ };
77
+ },
78
+ };
79
+ }
80
+
81
+ export function createNodeBackendRuntime(options: BackendRuntimeOptions = {}): BackendRuntime {
82
+ return new BackendRuntime({
83
+ ...options,
84
+ fileSystem: options.fileSystem ?? createNodeBackendFileSystem(),
85
+ host: options.host ?? createNodeBackendHostAdapter(),
86
+ });
87
+ }
88
+
89
+ export { BackendRuntime };
90
+ export type {
91
+ BackendFileHandle,
92
+ BackendFileStat,
93
+ BackendFileSystem,
94
+ BackendHostAdapter,
95
+ BackendRuntimeOptions,
96
+ } from './runtime.js';
package/src/runtime.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import {
2
2
  UAM_SUPPORTED_MATERIALIZATION_SCOPE,
3
+ UAM_SUPPORTED_TRANSACTION_SCOPE,
4
+ type UamProject,
3
5
  type UamTransactionOperation,
4
- } from '@openfairygui/core';
5
- import type { ApplyUamTransactionAppError } from '@openfairygui/functions';
6
- import fs from 'node:fs/promises';
7
- import type { Stats } from 'node:fs';
8
- import path from 'node:path';
6
+ } from '@openfairygui/core/uam';
7
+ import type { ApplyUamTransactionAppError } from '@openfairygui/functions/uam';
9
8
  import {
10
9
  BACKEND_CAPABILITY_SCHEMA_VERSION,
11
10
  BACKEND_COMPATIBILITY_POLICY,
@@ -27,8 +26,13 @@ export interface BackendFileHandle {
27
26
  close(): Promise<void>;
28
27
  }
29
28
 
29
+ export interface BackendFileStat {
30
+ isFile(): boolean;
31
+ isDirectory(): boolean;
32
+ }
33
+
30
34
  export interface BackendFileSystem {
31
- stat(filePath: string): Promise<Stats>;
35
+ stat(filePath: string): Promise<BackendFileStat>;
32
36
  readdir(dirPath: string): Promise<string[]>;
33
37
  readFile(filePath: string): Promise<string>;
34
38
  readFileRaw(filePath: string): Promise<Uint8Array>;
@@ -43,6 +47,50 @@ export interface BackendFileSystem {
43
47
  resolve(...paths: string[]): string;
44
48
  }
45
49
 
50
+ export interface BackendHostAdapter {
51
+ lockMetadata?(input: { canonicalPathKey: string; canonicalProjectPath: string; lockFilePath: string }): unknown;
52
+ }
53
+
54
+ export interface BackendArtifactBridgeCapability {
55
+ available: false;
56
+ requiredHost: 'node';
57
+ executionBoundary: 'external-bridge';
58
+ bridgeEntrypoint: '@openfairygui/backend/node';
59
+ reason: string;
60
+ }
61
+
62
+ export interface BackendCapabilityManifest {
63
+ browserSafe: true;
64
+ rootEntrypoint: '@openfairygui/backend';
65
+ nodeEntrypoint: '@openfairygui/backend/node';
66
+ adapters: {
67
+ fileSystem: {
68
+ injected: true;
69
+ requiredFor: readonly ['openSession', 'saveSession', 'materializeSession'];
70
+ };
71
+ projectStorage: {
72
+ injected: true;
73
+ browserSafe: true;
74
+ requiredFor: readonly ['openProjectSession.writeback', 'saveSession', 'materializeSession'];
75
+ adapterFactory: 'createBackendStorageFileSystem';
76
+ };
77
+ host: {
78
+ injected: true;
79
+ requiredFor: readonly ['advisoryLockMetadata'];
80
+ };
81
+ };
82
+ executionBoundaries: {
83
+ projectSession: 'in-process-browser-safe';
84
+ fileBackedSession: 'adapter-backed';
85
+ artifactPublish: BackendArtifactBridgeCapability;
86
+ artifactRestore: BackendArtifactBridgeCapability;
87
+ };
88
+ diagnostics: {
89
+ stableCodes: true;
90
+ errorDiagnosticMirror: true;
91
+ };
92
+ }
93
+
46
94
  export interface BackendCapabilities {
47
95
  contractVersion: typeof BACKEND_CONTRACT_VERSION;
48
96
  capabilitySchemaVersion: typeof BACKEND_CAPABILITY_SCHEMA_VERSION;
@@ -52,9 +100,11 @@ export interface BackendCapabilities {
52
100
  methods: readonly [
53
101
  'getCapabilities',
54
102
  'openSession',
103
+ 'openProjectSession',
55
104
  'getSession',
56
105
  'applyTransaction',
57
106
  'saveSession',
107
+ 'materializeSession',
58
108
  'closeSession',
59
109
  'getEvents',
60
110
  'getJob',
@@ -73,13 +123,21 @@ export interface BackendCapabilities {
73
123
  resourceKinds: readonly string[];
74
124
  nodeKinds: readonly string[];
75
125
  gearKinds: readonly string[];
126
+ transactionScope: {
127
+ resourceKinds: readonly string[];
128
+ nodeKinds: readonly string[];
129
+ gearKinds: readonly string[];
130
+ };
76
131
  unsupported: readonly ['artifact.publish', 'artifact.restore'];
77
132
  };
78
133
  artifact: {
79
134
  publish: false;
80
135
  restore: false;
81
- status: 'deferred';
136
+ status: 'bridge-required';
137
+ publishBridge: BackendArtifactBridgeCapability;
138
+ restoreBridge: BackendArtifactBridgeCapability;
82
139
  };
140
+ manifest: BackendCapabilityManifest;
83
141
  compatibilityPolicy: typeof BACKEND_COMPATIBILITY_POLICY;
84
142
  runtime: {
85
143
  sessionRuntime: true;
@@ -127,6 +185,16 @@ export interface BackendSessionSnapshot {
127
185
  capabilities: BackendCapabilities;
128
186
  }
129
187
 
188
+ export interface MaterializeSessionSnapshot extends BackendSessionSnapshot {
189
+ mode: 'fullProject';
190
+ reason?: string;
191
+ materializeRevision: number;
192
+ saveRevision: number;
193
+ writtenPaths: string[];
194
+ skippedPaths: string[];
195
+ diagnostics: import('./contracts.js').BackendDiagnostic[];
196
+ }
197
+
130
198
  export interface BackendSuccess<T> {
131
199
  ok: true;
132
200
  meta: BackendResponseMeta;
@@ -140,9 +208,7 @@ export interface BackendFailure<E extends BackendError = BackendError> {
140
208
  session?: BackendSessionSnapshot;
141
209
  }
142
210
 
143
- export type BackendResult<T, E extends BackendError = BackendError> =
144
- | BackendSuccess<T>
145
- | BackendFailure<E>;
211
+ export type BackendResult<T, E extends BackendError = BackendError> = BackendSuccess<T> | BackendFailure<E>;
146
212
 
147
213
  export interface SessionNotFoundError {
148
214
  code: 'session_not_found';
@@ -189,6 +255,29 @@ export interface SavePartialFailureError {
189
255
  diskMayBePartiallyUpdated: true;
190
256
  }
191
257
 
258
+ export interface MaterializeValidationFailedError {
259
+ code: 'materialize_validation_failed';
260
+ message: string;
261
+ sessionId: string;
262
+ canonicalPathKey: string;
263
+ issueCount: number;
264
+ diagnostics: import('./contracts.js').BackendDiagnostic[];
265
+ }
266
+
267
+ export interface MaterializeWriteFailedError {
268
+ code: 'write_failed';
269
+ message: string;
270
+ sessionId: string;
271
+ canonicalPathKey: string;
272
+ attemptedRevision: number;
273
+ lastSavedRevision: number;
274
+ writtenPaths: string[];
275
+ failedPaths: string[];
276
+ skippedPaths: string[];
277
+ diagnostics: import('./contracts.js').BackendDiagnostic[];
278
+ diskMayBePartiallyUpdated: true;
279
+ }
280
+
192
281
  export type BackendEventKind =
193
282
  | 'session.opened'
194
283
  | 'transaction.applied'
@@ -319,6 +408,15 @@ export interface CacheRefreshFailedError {
319
408
  causeCode?: string;
320
409
  }
321
410
 
411
+ export interface BackendCapabilityUnavailableError {
412
+ code: 'capability_unavailable';
413
+ message: string;
414
+ capability: 'fileSystem' | 'artifact.publish' | 'artifact.restore';
415
+ requiredAdapter?: 'BackendFileSystem';
416
+ requiredHost?: 'node';
417
+ bridgeBoundary?: 'external-bridge';
418
+ }
419
+
322
420
  export type BackendJobErrors =
323
421
  | BackendJobNotFoundError
324
422
  | BackendJobNotCancellableError
@@ -360,12 +458,15 @@ export type BackendError =
360
458
  | InProcessLockConflictError
361
459
  | AdvisoryLockConflictError
362
460
  | SavePartialFailureError
461
+ | MaterializeValidationFailedError
462
+ | MaterializeWriteFailedError
363
463
  | PathPolicyViolationError
364
464
  | EventCursorInvalidError
365
465
  | BackendJobNotFoundError
366
466
  | BackendJobNotCancellableError
367
467
  | BackendJobCancelledError
368
468
  | CacheRefreshFailedError
469
+ | BackendCapabilityUnavailableError
369
470
  | ApplyUamTransactionAppError;
370
471
 
371
472
  export interface ApplySessionTransactionInput {
@@ -374,16 +475,53 @@ export interface ApplySessionTransactionInput {
374
475
  operations: UamTransactionOperation[];
375
476
  }
376
477
 
478
+ export interface OpenProjectSessionInput {
479
+ project: UamProject;
480
+ sessionId?: string;
481
+ canonicalProjectPath?: string;
482
+ canonicalPathKey?: string;
483
+ storage?: BackendProjectSessionStorage;
484
+ }
485
+
486
+ export interface BackendProjectSessionStorage {
487
+ fileSystem: BackendFileSystem;
488
+ fairyPath: string;
489
+ canonicalProjectPath?: string;
490
+ canonicalPathKey?: string;
491
+ }
492
+
493
+ export interface SaveSessionInput {
494
+ sessionId: string;
495
+ expectedRevision?: number;
496
+ targetPath?: string;
497
+ fileSystem?: BackendFileSystem;
498
+ force?: boolean;
499
+ mode?: 'materializeCleanSession';
500
+ }
501
+
502
+ export interface MaterializeSessionInput {
503
+ sessionId: string;
504
+ expectedRevision?: number;
505
+ storage?: BackendProjectSessionStorage;
506
+ targetPath?: string;
507
+ fileSystem?: BackendFileSystem;
508
+ mode?: 'fullProject';
509
+ reason?: string;
510
+ }
511
+
377
512
  export interface BackendRuntimeOptions {
378
513
  fileSystem?: BackendFileSystem;
514
+ host?: BackendHostAdapter;
379
515
  }
380
516
 
381
517
  const BACKEND_METHODS = [
382
518
  'getCapabilities',
383
519
  'openSession',
520
+ 'openProjectSession',
384
521
  'getSession',
385
522
  'applyTransaction',
386
523
  'saveSession',
524
+ 'materializeSession',
387
525
  'closeSession',
388
526
  'getEvents',
389
527
  'getJob',
@@ -393,6 +531,14 @@ const BACKEND_METHODS = [
393
531
  'refreshCache',
394
532
  ] as const;
395
533
 
534
+ const ARTIFACT_BRIDGE_CAPABILITY = {
535
+ available: false,
536
+ requiredHost: 'node',
537
+ executionBoundary: 'external-bridge',
538
+ bridgeEntrypoint: '@openfairygui/backend/node',
539
+ reason: 'publish/restore require explicit Node-hosted filesystem and artifact execution.',
540
+ } as const satisfies BackendArtifactBridgeCapability;
541
+
396
542
  function createCapabilities(): BackendCapabilities {
397
543
  return {
398
544
  contractVersion: BACKEND_CONTRACT_VERSION,
@@ -411,9 +557,45 @@ function createCapabilities(): BackendCapabilities {
411
557
  resourceKinds: [...UAM_SUPPORTED_MATERIALIZATION_SCOPE.resourceKinds],
412
558
  nodeKinds: [...UAM_SUPPORTED_MATERIALIZATION_SCOPE.nodeKinds],
413
559
  gearKinds: [...UAM_SUPPORTED_MATERIALIZATION_SCOPE.gearKinds],
560
+ transactionScope: {
561
+ resourceKinds: [...UAM_SUPPORTED_TRANSACTION_SCOPE.resourceKinds],
562
+ nodeKinds: [...UAM_SUPPORTED_TRANSACTION_SCOPE.nodeKinds],
563
+ gearKinds: [...UAM_SUPPORTED_TRANSACTION_SCOPE.gearKinds],
564
+ },
414
565
  unsupported: ['artifact.publish', 'artifact.restore'],
415
566
  },
416
567
  artifact: createArtifactCapabilities(),
568
+ manifest: {
569
+ browserSafe: true,
570
+ rootEntrypoint: '@openfairygui/backend',
571
+ nodeEntrypoint: '@openfairygui/backend/node',
572
+ adapters: {
573
+ fileSystem: {
574
+ injected: true,
575
+ requiredFor: ['openSession', 'saveSession', 'materializeSession'],
576
+ },
577
+ projectStorage: {
578
+ injected: true,
579
+ browserSafe: true,
580
+ requiredFor: ['openProjectSession.writeback', 'saveSession', 'materializeSession'],
581
+ adapterFactory: 'createBackendStorageFileSystem',
582
+ },
583
+ host: {
584
+ injected: true,
585
+ requiredFor: ['advisoryLockMetadata'],
586
+ },
587
+ },
588
+ executionBoundaries: {
589
+ projectSession: 'in-process-browser-safe',
590
+ fileBackedSession: 'adapter-backed',
591
+ artifactPublish: ARTIFACT_BRIDGE_CAPABILITY,
592
+ artifactRestore: ARTIFACT_BRIDGE_CAPABILITY,
593
+ },
594
+ diagnostics: {
595
+ stableCodes: true,
596
+ errorDiagnosticMirror: true,
597
+ },
598
+ },
417
599
  compatibilityPolicy: BACKEND_COMPATIBILITY_POLICY,
418
600
  runtime: {
419
601
  sessionRuntime: true,
@@ -446,65 +628,8 @@ function createCapabilities(): BackendCapabilities {
446
628
  };
447
629
  }
448
630
 
449
- export function createNodeBackendFileSystem(): BackendFileSystem {
450
- return {
451
- stat(filePath: string): Promise<Stats> {
452
- return fs.stat(filePath);
453
- },
454
- readdir(dirPath: string): Promise<string[]> {
455
- return fs.readdir(dirPath);
456
- },
457
- readFile(filePath: string): Promise<string> {
458
- return fs.readFile(filePath, 'utf-8');
459
- },
460
- async readFileRaw(filePath: string): Promise<Uint8Array> {
461
- const buffer = await fs.readFile(filePath);
462
- return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
463
- },
464
- writeFile(filePath: string, content: string): Promise<void> {
465
- return fs.writeFile(filePath, content, 'utf-8');
466
- },
467
- writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
468
- return fs.writeFile(filePath, data);
469
- },
470
- async mkdir(dirPath: string, options?: { recursive?: boolean }): Promise<void> {
471
- await fs.mkdir(dirPath, { recursive: options?.recursive ?? false });
472
- },
473
- async resolvePath(filePath: string): Promise<string> {
474
- try {
475
- return await fs.realpath(filePath);
476
- } catch {
477
- return path.resolve(filePath);
478
- }
479
- },
480
- async openExclusive(filePath: string): Promise<BackendFileHandle> {
481
- const handle = await fs.open(filePath, 'wx');
482
- return {
483
- writeFile(content: string): Promise<void> {
484
- return handle.writeFile(content, 'utf-8');
485
- },
486
- close(): Promise<void> {
487
- return handle.close();
488
- },
489
- };
490
- },
491
- unlink(filePath: string): Promise<void> {
492
- return fs.unlink(filePath);
493
- },
494
- join(...paths: string[]): string {
495
- return path.join(...paths);
496
- },
497
- dirname(filePath: string): string {
498
- return path.dirname(filePath);
499
- },
500
- resolve(...paths: string[]): string {
501
- return path.resolve(...paths);
502
- },
503
- };
504
- }
505
-
506
631
  export class BackendRuntime {
507
- private readonly fileSystem: BackendFileSystem;
632
+ private readonly fileSystem?: BackendFileSystem;
508
633
  private readonly capabilities: BackendCapabilities;
509
634
  private readonly sessions = new Map<string, BackendSessionState>();
510
635
  private readonly sessionsByPath = new Map<string, string>();
@@ -521,10 +646,11 @@ export class BackendRuntime {
521
646
  private readonly jobService: JobService;
522
647
 
523
648
  public constructor(options: BackendRuntimeOptions = {}) {
524
- this.fileSystem = options.fileSystem ?? createNodeBackendFileSystem();
649
+ this.fileSystem = options.fileSystem;
525
650
  this.capabilities = createCapabilities();
526
651
  this.context = {
527
652
  fileSystem: this.fileSystem,
653
+ host: options.host,
528
654
  capabilities: this.capabilities,
529
655
  sessions: this.sessions,
530
656
  sessionsByPath: this.sessionsByPath,
@@ -548,37 +674,82 @@ export class BackendRuntime {
548
674
  return this.readService.getCapabilities() as BackendSuccess<BackendCapabilities>;
549
675
  }
550
676
 
551
- public async openSession(input: { projectPath: string }): Promise<BackendResult<BackendSessionSnapshot, InProcessLockConflictError | AdvisoryLockConflictError>> {
677
+ public async openSession(input: {
678
+ projectPath: string;
679
+ }): Promise<
680
+ BackendResult<
681
+ BackendSessionSnapshot,
682
+ InProcessLockConflictError | AdvisoryLockConflictError | BackendCapabilityUnavailableError
683
+ >
684
+ > {
552
685
  return this.runtimeService.openSession(input);
553
686
  }
554
687
 
688
+ public openProjectSession(input: OpenProjectSessionInput): BackendResult<BackendSessionSnapshot> {
689
+ return this.runtimeService.openProjectSession(input);
690
+ }
691
+
555
692
  public getSession(input: { sessionId: string }): BackendResult<BackendSessionSnapshot, SessionNotFoundError> {
556
693
  return this.readService.getSession(input);
557
694
  }
558
695
 
559
696
  public async applyTransaction(
560
697
  input: ApplySessionTransactionInput,
561
- ): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError>> {
698
+ ): Promise<
699
+ BackendResult<
700
+ BackendSessionSnapshot,
701
+ SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
702
+ >
703
+ > {
562
704
  return this.authoringService.applyTransaction(input);
563
705
  }
564
706
 
565
- public async saveSession(
566
- input: { sessionId: string; expectedRevision?: number; targetPath?: string },
567
- ): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | PathPolicyViolationError>> {
707
+ public async saveSession(input: SaveSessionInput): Promise<
708
+ BackendResult<
709
+ BackendSessionSnapshot | MaterializeSessionSnapshot,
710
+ | SessionNotFoundError
711
+ | SessionStaleWriteError
712
+ | SavePartialFailureError
713
+ | MaterializeValidationFailedError
714
+ | MaterializeWriteFailedError
715
+ | PathPolicyViolationError
716
+ | InProcessLockConflictError
717
+ | BackendCapabilityUnavailableError
718
+ >
719
+ > {
568
720
  return this.authoringService.saveSession(input);
569
721
  }
570
722
 
571
- public async closeSession(
572
- input: { sessionId: string },
573
- ): Promise<BackendResult<{ sessionId: string; closed: true }, SessionNotFoundError>> {
723
+ public async materializeSession(input: MaterializeSessionInput): Promise<
724
+ BackendResult<
725
+ MaterializeSessionSnapshot,
726
+ | SessionNotFoundError
727
+ | SessionStaleWriteError
728
+ | MaterializeValidationFailedError
729
+ | MaterializeWriteFailedError
730
+ | PathPolicyViolationError
731
+ | InProcessLockConflictError
732
+ | BackendCapabilityUnavailableError
733
+ >
734
+ > {
735
+ return this.authoringService.materializeSession(input);
736
+ }
737
+
738
+ public async closeSession(input: {
739
+ sessionId: string;
740
+ }): Promise<BackendResult<{ sessionId: string; closed: true }, SessionNotFoundError>> {
574
741
  return this.runtimeService.closeSession(input);
575
742
  }
576
743
 
577
- public getEvents(input: GetEventsInput): BackendResult<GetEventsSnapshot, SessionNotFoundError | EventCursorInvalidError> {
744
+ public getEvents(
745
+ input: GetEventsInput,
746
+ ): BackendResult<GetEventsSnapshot, SessionNotFoundError | EventCursorInvalidError> {
578
747
  return this.eventService.getEvents(input);
579
748
  }
580
749
 
581
- public getJob(input: GetJobInput): BackendResult<BackendJobSnapshot, SessionNotFoundError | BackendJobNotFoundError> {
750
+ public getJob(
751
+ input: GetJobInput,
752
+ ): BackendResult<BackendJobSnapshot, SessionNotFoundError | BackendJobNotFoundError> {
582
753
  return this.jobService.getJob(input);
583
754
  }
584
755
 
@@ -586,7 +757,12 @@ export class BackendRuntime {
586
757
  return this.jobService.listJobs(input);
587
758
  }
588
759
 
589
- public cancelJob(input: CancelJobInput): BackendResult<BackendJobSnapshot, SessionNotFoundError | BackendJobNotFoundError | BackendJobNotCancellableError> {
760
+ public cancelJob(
761
+ input: CancelJobInput,
762
+ ): BackendResult<
763
+ BackendJobSnapshot,
764
+ SessionNotFoundError | BackendJobNotFoundError | BackendJobNotCancellableError
765
+ > {
590
766
  return this.jobService.cancelJob(input);
591
767
  }
592
768
 
@@ -1,9 +1,19 @@
1
1
  import type { BackendCapabilities } from '../runtime.js';
2
2
 
3
3
  export function createArtifactCapabilities(): BackendCapabilities['artifact'] {
4
+ const bridge = {
5
+ available: false,
6
+ requiredHost: 'node',
7
+ executionBoundary: 'external-bridge',
8
+ bridgeEntrypoint: '@openfairygui/backend/node',
9
+ reason: 'publish/restore require explicit Node-hosted filesystem and artifact execution.',
10
+ } as const;
11
+
4
12
  return {
5
13
  publish: false,
6
14
  restore: false,
7
- status: 'deferred',
15
+ status: 'bridge-required',
16
+ publishBridge: bridge,
17
+ restoreBridge: bridge,
8
18
  };
9
19
  }