@memberjunction/server 6.1.0-edge.2 → 6.1.0-edge.3

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 (162) hide show
  1. package/LICENSE +180 -4
  2. package/dist/auth/AuthProviderCatalogRouter.d.ts +31 -0
  3. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -0
  4. package/dist/auth/AuthProviderCatalogRouter.js +69 -0
  5. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -0
  6. package/dist/auth/AuthProviderEngine.d.ts +96 -0
  7. package/dist/auth/AuthProviderEngine.d.ts.map +1 -0
  8. package/dist/auth/AuthProviderEngine.js +245 -0
  9. package/dist/auth/AuthProviderEngine.js.map +1 -0
  10. package/dist/auth/index.d.ts +11 -0
  11. package/dist/auth/index.d.ts.map +1 -1
  12. package/dist/auth/index.js +56 -13
  13. package/dist/auth/index.js.map +1 -1
  14. package/dist/auth/initializeProviders.d.ts +64 -1
  15. package/dist/auth/initializeProviders.d.ts.map +1 -1
  16. package/dist/auth/initializeProviders.js +117 -5
  17. package/dist/auth/initializeProviders.js.map +1 -1
  18. package/dist/config.d.ts +89 -0
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +52 -35
  21. package/dist/config.js.map +1 -1
  22. package/dist/generated/generated.d.ts +501 -1597
  23. package/dist/generated/generated.d.ts.map +1 -1
  24. package/dist/generated/generated.js +2711 -17021
  25. package/dist/generated/generated.js.map +1 -1
  26. package/dist/generic/ResolverBase.d.ts +1 -1
  27. package/dist/generic/ResolverBase.d.ts.map +1 -1
  28. package/dist/generic/ResolverBase.js +7 -4
  29. package/dist/generic/ResolverBase.js.map +1 -1
  30. package/dist/generic/RunViewResolver.d.ts +4 -0
  31. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  32. package/dist/generic/RunViewResolver.js +31 -0
  33. package/dist/generic/RunViewResolver.js.map +1 -1
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +99 -14
  37. package/dist/index.js.map +1 -1
  38. package/dist/integration/RSUProgressBridge.d.ts +83 -0
  39. package/dist/integration/RSUProgressBridge.d.ts.map +1 -0
  40. package/dist/integration/RSUProgressBridge.js +230 -0
  41. package/dist/integration/RSUProgressBridge.js.map +1 -0
  42. package/dist/integration/SchemaRefreshLaunch.d.ts +52 -0
  43. package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
  44. package/dist/integration/SchemaRefreshLaunch.js +50 -0
  45. package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
  46. package/dist/providerConfigUnits.d.ts +20 -0
  47. package/dist/providerConfigUnits.d.ts.map +1 -0
  48. package/dist/providerConfigUnits.js +22 -0
  49. package/dist/providerConfigUnits.js.map +1 -0
  50. package/dist/resolvers/FileResolver.d.ts +34 -4
  51. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  52. package/dist/resolvers/FileResolver.js +218 -28
  53. package/dist/resolvers/FileResolver.js.map +1 -1
  54. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +76 -3
  55. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  56. package/dist/resolvers/IntegrationDiscoveryResolver.js +299 -81
  57. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  58. package/dist/resolvers/QueryResolver.d.ts +5 -4
  59. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  60. package/dist/resolvers/QueryResolver.js +29 -14
  61. package/dist/resolvers/QueryResolver.js.map +1 -1
  62. package/dist/resolvers/QuerySystemUserResolver.d.ts +12 -3
  63. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  64. package/dist/resolvers/QuerySystemUserResolver.js +33 -14
  65. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  66. package/dist/resolvers/RunAIAgentResolver.d.ts +3 -1
  67. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  68. package/dist/resolvers/RunAIAgentResolver.js +20 -7
  69. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  70. package/dist/resolvers/TaskGraphFrameResolver.d.ts +18 -0
  71. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  72. package/dist/resolvers/TaskGraphFrameResolver.js +75 -0
  73. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  74. package/dist/resolvers/UserResolver.d.ts +19 -17
  75. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  76. package/dist/resolvers/UserResolver.js +66 -67
  77. package/dist/resolvers/UserResolver.js.map +1 -1
  78. package/dist/resolvers/currentUserRoles.d.ts +19 -0
  79. package/dist/resolvers/currentUserRoles.d.ts.map +1 -0
  80. package/dist/resolvers/currentUserRoles.js +36 -0
  81. package/dist/resolvers/currentUserRoles.js.map +1 -0
  82. package/dist/rest/MediaAccessKeys.d.ts +17 -0
  83. package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
  84. package/dist/rest/MediaAccessKeys.js +42 -1
  85. package/dist/rest/MediaAccessKeys.js.map +1 -1
  86. package/dist/rest/MediaStreamHandler.d.ts +3 -2
  87. package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
  88. package/dist/rest/MediaStreamHandler.js +83 -2
  89. package/dist/rest/MediaStreamHandler.js.map +1 -1
  90. package/dist/rest/OAuthCallbackHandler.d.ts +49 -1
  91. package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
  92. package/dist/rest/OAuthCallbackHandler.js +129 -43
  93. package/dist/rest/OAuthCallbackHandler.js.map +1 -1
  94. package/dist/rest/UploadTokenManager.d.ts +90 -0
  95. package/dist/rest/UploadTokenManager.d.ts.map +1 -0
  96. package/dist/rest/UploadTokenManager.js +156 -0
  97. package/dist/rest/UploadTokenManager.js.map +1 -0
  98. package/dist/services/IntegrationSyncWorkerService.d.ts +48 -0
  99. package/dist/services/IntegrationSyncWorkerService.d.ts.map +1 -0
  100. package/dist/services/IntegrationSyncWorkerService.js +132 -0
  101. package/dist/services/IntegrationSyncWorkerService.js.map +1 -0
  102. package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
  103. package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
  104. package/dist/services/StartTaskGraphDispatcher.js +21 -1
  105. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  106. package/dist/services/TaskGraphAgentRunner.d.ts +7 -0
  107. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  108. package/dist/services/TaskGraphAgentRunner.js +23 -0
  109. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  110. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  111. package/dist/services/TaskGraphContinuationDeliverer.js +12 -1
  112. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  113. package/dist/types.d.ts +6 -0
  114. package/dist/types.d.ts.map +1 -1
  115. package/dist/types.js.map +1 -1
  116. package/package.json +91 -91
  117. package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
  118. package/src/__tests__/AuthProviderEngine.test.ts +278 -0
  119. package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
  120. package/src/__tests__/IntegrationSyncWorkerService.test.ts +275 -0
  121. package/src/__tests__/OAuthCallbackHandler.openRedirect.test.ts +117 -0
  122. package/src/__tests__/OAuthCallbackHandler.xss.test.ts +4 -1
  123. package/src/__tests__/RESTEndpointHandler.test.ts +672 -0
  124. package/src/__tests__/RSUProgressBridge.test.ts +250 -0
  125. package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
  126. package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
  127. package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
  128. package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
  129. package/src/__tests__/config-units.test.ts +74 -0
  130. package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
  131. package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
  132. package/src/__tests__/newUsers.test.ts +726 -0
  133. package/src/__tests__/uploadTokenManager.test.ts +159 -0
  134. package/src/auth/AuthProviderCatalogRouter.ts +76 -0
  135. package/src/auth/AuthProviderEngine.ts +253 -0
  136. package/src/auth/index.ts +59 -15
  137. package/src/auth/initializeProviders.ts +128 -7
  138. package/src/config.ts +55 -36
  139. package/src/generated/generated.ts +1911 -10551
  140. package/src/generic/ResolverBase.ts +11 -4
  141. package/src/generic/RunViewResolver.ts +31 -0
  142. package/src/index.ts +108 -15
  143. package/src/integration/RSUProgressBridge.ts +248 -0
  144. package/src/integration/SchemaRefreshLaunch.ts +77 -0
  145. package/src/providerConfigUnits.ts +21 -0
  146. package/src/resolvers/FileResolver.ts +200 -40
  147. package/src/resolvers/IntegrationDiscoveryResolver.ts +316 -68
  148. package/src/resolvers/QueryResolver.ts +25 -11
  149. package/src/resolvers/QuerySystemUserResolver.ts +28 -13
  150. package/src/resolvers/RunAIAgentResolver.ts +21 -6
  151. package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
  152. package/src/resolvers/UserResolver.ts +99 -60
  153. package/src/resolvers/currentUserRoles.ts +47 -0
  154. package/src/rest/MediaAccessKeys.ts +47 -1
  155. package/src/rest/MediaStreamHandler.ts +98 -3
  156. package/src/rest/OAuthCallbackHandler.ts +149 -41
  157. package/src/rest/UploadTokenManager.ts +211 -0
  158. package/src/services/IntegrationSyncWorkerService.ts +141 -0
  159. package/src/services/StartTaskGraphDispatcher.ts +21 -1
  160. package/src/services/TaskGraphAgentRunner.ts +23 -0
  161. package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
  162. package/src/types.ts +6 -0
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @fileoverview Pure reporting helpers for the connector schema-refresh pipeline launch.
3
+ *
4
+ * The refresh is a live vendor introspect and can run for minutes on a large catalog, so
5
+ * IntegrationCreateConnection / IntegrationUpdateConnection can launch it *detached* and hand the
6
+ * caller a tailable run ID instead of blocking. That split creates one easy-to-get-wrong reporting
7
+ * case: a detached summary's counts are placeholder zeros, and describing them as results ("0
8
+ * created, 0 updated") reads to an operator as "the refresh found nothing" — the opposite of "the
9
+ * refresh has not finished yet". This module owns that decision so it can be pinned by a test
10
+ * without standing up a resolver.
11
+ */
12
+
13
+ /**
14
+ * The subset of the pipeline summary that message construction reads. Structural so both the
15
+ * GraphQL `@ObjectType` and a test fixture satisfy it without either depending on the other.
16
+ */
17
+ export interface SchemaRefreshSummaryLike {
18
+ /** Tailable run ID — the only meaningful field while `InProgress` is true. */
19
+ RunID: string;
20
+ /** True ⇒ launched detached and still running; every count below is a placeholder zero. */
21
+ InProgress: boolean;
22
+ /**
23
+ * Whether the pipeline itself succeeded. A pipeline that fails at ConnectionTest RETURNS rather
24
+ * than throwing, with every count at zero — so counts alone read identically to a clean
25
+ * no-op refresh. Reporting that as "0 created, 0 updated" tells an operator the vendor had
26
+ * nothing new when in fact the credentials were never accepted.
27
+ */
28
+ Succeeded: boolean;
29
+ /** The pipeline's own failure reason, when `Succeeded` is false. */
30
+ FailureMessage?: string;
31
+ ObjectsCreated: number;
32
+ ObjectsUpdated: number;
33
+ UnresolvedObjects: string[];
34
+ }
35
+
36
+ /** The counts clause, or an explicit failure clause when the pipeline did not succeed. */
37
+ function describeFinishedRefresh(summary: SchemaRefreshSummaryLike): string {
38
+ if (!summary.Succeeded) {
39
+ return `schema refresh FAILED (${summary.FailureMessage ?? 'no reason reported'}) — see run ${summary.RunID}`;
40
+ }
41
+ return `schema refresh: ${summary.ObjectsCreated} created, ` +
42
+ `${summary.ObjectsUpdated} updated, ${summary.UnresolvedObjects.length} PK-unresolved`;
43
+ }
44
+
45
+ /**
46
+ * Builds the human-facing `Message` for IntegrationCreateConnection.
47
+ *
48
+ * @param testConnection whether the caller asked for (and got) a live connection test
49
+ * @param summary the refresh summary, or undefined when no refresh was requested
50
+ */
51
+ export function BuildCreateConnectionMessage(
52
+ testConnection: boolean,
53
+ summary: SchemaRefreshSummaryLike | undefined
54
+ ): string {
55
+ if (!summary) return 'Connection created and test passed';
56
+ const created = `Connection created${testConnection ? ', test passed' : ''}`;
57
+ // Never report a detached run's placeholder counts as findings — point at the run stream instead.
58
+ if (summary.InProgress) return `${created}, schema refresh running — tail run ${summary.RunID}`;
59
+ return `${created}, ${describeFinishedRefresh(summary)}`;
60
+ }
61
+
62
+ /**
63
+ * Builds the human-facing `Message` for IntegrationUpdateConnection's blocking-refresh path.
64
+ * Shares {@link describeFinishedRefresh} with create so a failed refresh can never be reported as
65
+ * a clean zero-count run on one path and honestly on the other.
66
+ */
67
+ export function BuildUpdateConnectionMessage(summary: SchemaRefreshSummaryLike): string {
68
+ return `Updated, ${describeFinishedRefresh(summary)}`;
69
+ }
70
+
71
+ /**
72
+ * Builds the human-facing `Message` for IntegrationUpdateConnection's detached-refresh path.
73
+ * Separate from {@link BuildCreateConnectionMessage} because update has no connection-test clause.
74
+ */
75
+ export function BuildDetachedRefreshMessage(runID: string): string {
76
+ return `Updated, schema refresh running — tail run ${runID}`;
77
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Unit-conversion seam between MJServer's configuration and SQLServerDataProvider.
3
+ *
4
+ * MJServer configures `databaseSettings.metadataCacheRefreshInterval` in **milliseconds**
5
+ * (default 180000 = 3 minutes, overridable via the METADATA_CACHE_REFRESH_INTERVAL env var).
6
+ * `SQLServerProviderConfigData`'s third constructor parameter is `checkRefreshIntervalSeconds`
7
+ * — declared in **seconds**.
8
+ *
9
+ * Commit 645c5a5e8 fixed a bug where the raw millisecond value was handed straight to the
10
+ * provider, so the "3 minute" default became a 180000-second (~50 hour) metadata cache
11
+ * refresh cadence. This function is the single place that conversion happens; both provider
12
+ * bootstrap call sites in `index.ts` (the main pool and the CodeGen-credentialed pool) go
13
+ * through it, and `config-units.test.ts` pins the contract so the units can never silently
14
+ * regress again.
15
+ *
16
+ * @param metadataCacheRefreshIntervalMs - the configured refresh interval, in milliseconds
17
+ * @returns the same interval expressed in seconds, the unit `checkRefreshIntervalSeconds` declares
18
+ */
19
+ export function MetadataCacheRefreshIntervalSeconds(metadataCacheRefreshIntervalMs: number): number {
20
+ return metadataCacheRefreshIntervalMs / 1000;
21
+ }
@@ -41,6 +41,7 @@ import { FieldMapper } from '@memberjunction/graphql-dataprovider';
41
41
  import { GetReadOnlyProvider } from '../util.js';
42
42
  import { configInfo } from '../config.js';
43
43
  import { MediaAccessKeyManager } from '../rest/MediaAccessKeys.js';
44
+ import { UploadTokenManager } from '../rest/UploadTokenManager.js';
44
45
  import { deriveSidecarPath, parsePeaksSidecar } from './peaksSidecar.js';
45
46
 
46
47
  @InputType()
@@ -49,6 +50,63 @@ export class CreateUploadURLInput {
49
50
  FileID: string;
50
51
  }
51
52
 
53
+ @InputType()
54
+ export class UploadStorageFileInput {
55
+ @Field(() => String, { nullable: true })
56
+ FileName?: string;
57
+
58
+ @Field(() => String, { nullable: true })
59
+ Base64Data?: string;
60
+
61
+ @Field(() => String, { nullable: true })
62
+ UploadToken?: string;
63
+
64
+ @Field(() => String, { nullable: true })
65
+ MimeType?: string;
66
+
67
+ @Field(() => String, { nullable: true })
68
+ AccountID?: string;
69
+
70
+ @Field(() => String, { nullable: true })
71
+ CategoryID?: string;
72
+
73
+ @Field(() => String, { nullable: true })
74
+ Description?: string;
75
+
76
+ @Field(() => String, { nullable: true })
77
+ PathPrefix?: string;
78
+ }
79
+
80
+ @ObjectType()
81
+ export class UploadStorageFilePayload {
82
+ @Field(() => Boolean)
83
+ Success: boolean;
84
+
85
+ @Field(() => String, { nullable: true })
86
+ FileID?: string;
87
+
88
+ @Field(() => MJFile_, { nullable: true })
89
+ File?: MJFile_;
90
+
91
+ @Field(() => String, { nullable: true })
92
+ ErrorMessage?: string;
93
+ }
94
+
95
+ @ObjectType()
96
+ export class CreateUploadStageTokenPayload {
97
+ @Field(() => Boolean)
98
+ Success: boolean;
99
+
100
+ @Field(() => String, { nullable: true })
101
+ Token?: string;
102
+
103
+ @Field(() => String, { nullable: true })
104
+ Url?: string;
105
+
106
+ @Field(() => String, { nullable: true })
107
+ ErrorMessage?: string;
108
+ }
109
+
52
110
  @ObjectType()
53
111
  export class CreateFilePayload {
54
112
  @Field(() => MJFile_)
@@ -414,7 +472,8 @@ export class FileResolver extends FileResolverBase {
414
472
 
415
473
  // Access authorized — mint the capability token.
416
474
  const { Token, ExpiresAt } = MediaAccessKeyManager.Instance.Sign(fileId, contextUser.ID);
417
- const url = `${this.resolvePublicBaseUrl()}/media/${encodeURIComponent(fileId)}?token=${encodeURIComponent(Token)}`;
475
+ const fileNameSuffix = fileEntity.Name ? `/${encodeURIComponent(fileEntity.Name)}` : '';
476
+ const url = `${this.resolvePublicBaseUrl()}/media/${encodeURIComponent(fileId)}${fileNameSuffix}?token=${encodeURIComponent(Token)}`;
418
477
 
419
478
  // Best-effort: surface precomputed waveform peaks from a peaks.json sidecar beside the file, so
420
479
  // the player renders the real waveform instantly without fetching/decoding the audio. Never
@@ -573,6 +632,85 @@ export class FileResolver extends FileResolverBase {
573
632
  return { File, UploadUrl, NameExists };
574
633
  }
575
634
 
635
+ /**
636
+ * Direct server-side file upload using MJ Storage subsystem.
637
+ * Uploads file contents directly via the configured storage driver (Box, Azure, S3, Google Drive, Dropbox, etc.)
638
+ * and creates/persists the MJ: Files database record.
639
+ */
640
+ @Mutation(() => UploadStorageFilePayload)
641
+ async UploadStorageFile(
642
+ @Arg('input', () => UploadStorageFileInput) input: UploadStorageFileInput,
643
+ @Ctx() context: AppContext
644
+ ): Promise<UploadStorageFilePayload> {
645
+ try {
646
+ const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
647
+ const user = this.GetUserFromPayload(context.userPayload);
648
+
649
+ // Permission check on MJ: Files
650
+ const fileEntity = await provider.GetEntityObject<MJFileEntity>('MJ: Files', user);
651
+ fileEntity.CheckPermissions(EntityPermissionType.Create, true);
652
+
653
+ let buffer: Buffer;
654
+ let fileName: string = input.FileName || 'file';
655
+ let mimeType: string = input.MimeType || 'application/octet-stream';
656
+
657
+ if (input.UploadToken) {
658
+ const staged = UploadTokenManager.Instance.Consume(input.UploadToken, user.ID);
659
+ if (!staged) {
660
+ return {
661
+ Success: false,
662
+ ErrorMessage: 'Upload token is invalid, expired, or has already been used.',
663
+ };
664
+ }
665
+ buffer = staged.buffer;
666
+ fileName = input.FileName || staged.fileName || 'file';
667
+ mimeType = input.MimeType || staged.mimeType || 'application/octet-stream';
668
+ } else if (input.Base64Data) {
669
+ buffer = Buffer.from(input.Base64Data, 'base64');
670
+ } else {
671
+ return {
672
+ Success: false,
673
+ ErrorMessage: 'Either UploadToken or Base64Data must be provided.',
674
+ };
675
+ }
676
+
677
+ // Ensure FileStorageEngine is configured under the user context
678
+ await FileStorageEngine.Instance.Config(false, user, provider);
679
+ if (input.AccountID && !FileStorageEngine.Instance.GetAccountById(input.AccountID)) {
680
+ await FileStorageEngine.Instance.Config(true, user, provider);
681
+ }
682
+
683
+ const uploadResult = await FileStorageEngine.Instance.UploadFile({
684
+ content: buffer,
685
+ fileName,
686
+ mimeType,
687
+ contextUser: user,
688
+ storageAccountId: input.AccountID,
689
+ categoryID: input.CategoryID,
690
+ description: input.Description,
691
+ pathPrefix: input.PathPrefix,
692
+ provider,
693
+ });
694
+
695
+ const loaded = await fileEntity.Load(uploadResult.FileID);
696
+ const mapper = new FieldMapper();
697
+ const File = loaded ? (mapper.MapFields({ ...fileEntity.GetAll() }) as unknown as MJFile_) : undefined;
698
+
699
+ return {
700
+ Success: true,
701
+ FileID: uploadResult.FileID,
702
+ File,
703
+ };
704
+ } catch (error) {
705
+ const message = error instanceof Error ? error.message : String(error);
706
+ LogError(`UploadStorageFile failed for '${input.FileName}': ${message}`);
707
+ return {
708
+ Success: false,
709
+ ErrorMessage: message,
710
+ };
711
+ }
712
+ }
713
+
576
714
  @FieldResolver(() => String)
577
715
  async DownloadUrl(@Root() file: MJFile_, @Ctx() context: AppContext) {
578
716
  const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
@@ -580,6 +718,19 @@ export class FileResolver extends FileResolverBase {
580
718
  const fileEntity = await md.GetEntityObject<MJFileEntity>('MJ: Files', user);
581
719
  fileEntity.CheckPermissions(EntityPermissionType.Read, true);
582
720
 
721
+ await FileStorageEngine.Instance.Config(false, user, md);
722
+ let accounts = FileStorageEngine.Instance.GetAccountsByProviderID(file.ProviderID);
723
+ if (accounts.length === 0) {
724
+ await FileStorageEngine.Instance.Config(true, user, md);
725
+ accounts = FileStorageEngine.Instance.GetAccountsByProviderID(file.ProviderID);
726
+ }
727
+
728
+ if (accounts.length > 0) {
729
+ const account = accounts[0];
730
+ const driver = await FileStorageEngine.Instance.GetDriver(account.ID, user);
731
+ return await driver.CreatePreAuthDownloadUrl(file.ProviderKey ?? file.Name);
732
+ }
733
+
583
734
  const providerEntity = await md.GetEntityObject<MJFileStorageProviderEntity>('MJ: File Storage Providers', user);
584
735
  await providerEntity.Load(file.ProviderID);
585
736
 
@@ -734,16 +885,13 @@ export class FileResolver extends FileResolverBase {
734
885
  const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
735
886
  const user = this.GetUserFromPayload(context.userPayload);
736
887
 
737
- // Load the account and its provider
738
- const { account, provider: providerEntity } = await this.loadAccountAndProvider(input.AccountID, context);
739
-
740
888
  // Check permissions
741
889
  const fileEntity = await md.GetEntityObject<MJFileEntity>('MJ: Files', user);
742
890
  fileEntity.CheckPermissions(EntityPermissionType.Read, true);
743
891
 
744
- // Create download URL with extended user context (includes account for credential lookup)
745
- const userContext = this.buildExtendedUserContext(context, account);
746
- const downloadUrl = await createDownloadUrl(providerEntity, input.ObjectName, userContext);
892
+ await FileStorageEngine.Instance.Config(false, user, md);
893
+ const driver = await FileStorageEngine.Instance.GetDriver(input.AccountID, user);
894
+ const downloadUrl = await driver.CreatePreAuthDownloadUrl(input.ObjectName);
747
895
  return downloadUrl;
748
896
  }
749
897
 
@@ -762,67 +910,79 @@ export class FileResolver extends FileResolverBase {
762
910
  * @see CreateFile for the legacy path that also creates a File entity record.
763
911
  */
764
912
  @Mutation(() => CreatePreAuthUploadUrlPayload)
765
- async CreatePreAuthUploadUrl(@Arg('input', () => CreatePreAuthUploadUrlInput) input: CreatePreAuthUploadUrlInput, @Ctx() context: AppContext) {
913
+ async CreatePreAuthUploadUrl(
914
+ @Arg('input', () => CreatePreAuthUploadUrlInput) input: CreatePreAuthUploadUrlInput,
915
+ @Ctx() context: AppContext
916
+ ): Promise<CreatePreAuthUploadUrlPayload> {
766
917
  const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
767
918
  const user = this.GetUserFromPayload(context.userPayload);
768
919
 
769
- // Load the account and its provider
770
- const { account, provider: providerEntity } = await this.loadAccountAndProvider(input.AccountID, context);
771
-
772
920
  // Check permissions
773
921
  const fileEntity = await md.GetEntityObject<MJFileEntity>('MJ: Files', user);
774
922
  fileEntity.CheckPermissions(EntityPermissionType.Create, true);
775
923
 
776
- // Create upload URL with extended user context (includes account for credential lookup)
777
- const userContext = this.buildExtendedUserContext(context, account);
778
- const { UploadUrl, updatedInput } = await createUploadUrl(
779
- providerEntity,
780
- {
781
- ID: '', // Not needed for direct upload
782
- Name: input.ObjectName,
783
- ProviderID: providerEntity.ID,
784
- ContentType: input.ContentType,
785
- },
786
- userContext,
787
- );
788
-
789
- // Extract ProviderKey if it exists (spread into updatedInput by createUploadUrl)
790
- const providerKey = (updatedInput as { ProviderKey?: string }).ProviderKey;
924
+ await FileStorageEngine.Instance.Config(false, user, md);
925
+ const driver = await FileStorageEngine.Instance.GetDriver(input.AccountID, user);
926
+ if (!driver.SupportsPreAuthUpload) {
927
+ throw new Error(`Storage provider '${driver.constructor.name}' does not support pre-authenticated direct uploads.`);
928
+ }
929
+
930
+ const result = await driver.CreatePreAuthUploadUrl(input.ObjectName);
791
931
 
792
932
  return {
793
- UploadUrl,
794
- ProviderKey: providerKey,
933
+ UploadUrl: result.UploadUrl,
934
+ ProviderKey: result.ProviderKey,
795
935
  };
796
936
  }
797
937
 
938
+ /**
939
+ * Mints a cryptographically signed, short-lived media-upload token for staging raw binary files.
940
+ * Checks user permissions on `MJ: Files` before issuing.
941
+ */
942
+ @Mutation(() => CreateUploadStageTokenPayload)
943
+ async CreateUploadStageToken(
944
+ @Ctx() context: AppContext
945
+ ): Promise<CreateUploadStageTokenPayload> {
946
+ try {
947
+ const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
948
+ const user = this.GetUserFromPayload(context.userPayload);
949
+
950
+ // Permission check on MJ: Files
951
+ const fileEntity = await provider.GetEntityObject<MJFileEntity>('MJ: Files', user);
952
+ fileEntity.CheckPermissions(EntityPermissionType.Create, true);
953
+
954
+ const mint = MediaAccessKeyManager.Instance.SignUpload(user.ID);
955
+
956
+ return {
957
+ Success: true,
958
+ Token: mint.Token,
959
+ Url: '/media/upload-stage',
960
+ };
961
+ } catch (err) {
962
+ const message = err instanceof Error ? err.message : String(err);
963
+ return {
964
+ Success: false,
965
+ ErrorMessage: message,
966
+ };
967
+ }
968
+ }
969
+
798
970
  @Mutation(() => Boolean)
799
971
  async DeleteStorageObject(@Arg('input', () => DeleteStorageObjectInput) input: DeleteStorageObjectInput, @Ctx() context: AppContext) {
800
- console.log('[FileResolver] DeleteStorageObject called:', input);
801
-
802
972
  const md = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
803
973
  const user = this.GetUserFromPayload(context.userPayload);
804
974
 
805
975
  // Load the account and its provider
806
976
  const { account, provider: providerEntity } = await this.loadAccountAndProvider(input.AccountID, context);
807
977
 
808
- console.log('[FileResolver] Provider loaded:', {
809
- providerID: providerEntity.ID,
810
- providerName: providerEntity.Name,
811
- serverDriverKey: providerEntity.ServerDriverKey,
812
- });
813
-
814
978
  // Check permissions
815
979
  const fileEntity = await md.GetEntityObject<MJFileEntity>('MJ: Files', user);
816
980
  fileEntity.CheckPermissions(EntityPermissionType.Delete, true);
817
981
 
818
- console.log('[FileResolver] Permissions checked, calling deleteObject...');
819
-
820
982
  // Delete the object with extended user context (includes account for credential lookup)
821
983
  const userContext = this.buildExtendedUserContext(context, account);
822
984
  const success = await deleteObject(providerEntity, input.ObjectName, userContext);
823
985
 
824
- console.log('[FileResolver] deleteObject returned:', success);
825
-
826
986
  return success;
827
987
  }
828
988