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

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 (210) hide show
  1. package/LICENSE +180 -4
  2. package/dist/agentSessions/SessionManager.d.ts +19 -5
  3. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  4. package/dist/agentSessions/SessionManager.js +27 -9
  5. package/dist/agentSessions/SessionManager.js.map +1 -1
  6. package/dist/auth/AuthProviderCatalogRouter.d.ts +31 -0
  7. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -0
  8. package/dist/auth/AuthProviderCatalogRouter.js +69 -0
  9. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -0
  10. package/dist/auth/AuthProviderEngine.d.ts +96 -0
  11. package/dist/auth/AuthProviderEngine.d.ts.map +1 -0
  12. package/dist/auth/AuthProviderEngine.js +245 -0
  13. package/dist/auth/AuthProviderEngine.js.map +1 -0
  14. package/dist/auth/index.d.ts +12 -0
  15. package/dist/auth/index.d.ts.map +1 -1
  16. package/dist/auth/index.js +57 -13
  17. package/dist/auth/index.js.map +1 -1
  18. package/dist/auth/initializeProviders.d.ts +64 -1
  19. package/dist/auth/initializeProviders.d.ts.map +1 -1
  20. package/dist/auth/initializeProviders.js +117 -5
  21. package/dist/auth/initializeProviders.js.map +1 -1
  22. package/dist/auth/scopeLimitedPrincipal.d.ts +39 -0
  23. package/dist/auth/scopeLimitedPrincipal.d.ts.map +1 -0
  24. package/dist/auth/scopeLimitedPrincipal.js +49 -0
  25. package/dist/auth/scopeLimitedPrincipal.js.map +1 -0
  26. package/dist/config.d.ts +89 -0
  27. package/dist/config.d.ts.map +1 -1
  28. package/dist/config.js +52 -35
  29. package/dist/config.js.map +1 -1
  30. package/dist/context.d.ts.map +1 -1
  31. package/dist/context.js +21 -1
  32. package/dist/context.js.map +1 -1
  33. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts.map +1 -1
  34. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js +8 -7
  35. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js.map +1 -1
  36. package/dist/generated/generated.d.ts +619 -1597
  37. package/dist/generated/generated.d.ts.map +1 -1
  38. package/dist/generated/generated.js +3341 -17021
  39. package/dist/generated/generated.js.map +1 -1
  40. package/dist/generic/ResolverBase.d.ts +28 -1
  41. package/dist/generic/ResolverBase.d.ts.map +1 -1
  42. package/dist/generic/ResolverBase.js +57 -8
  43. package/dist/generic/ResolverBase.js.map +1 -1
  44. package/dist/generic/RunViewResolver.d.ts +4 -0
  45. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  46. package/dist/generic/RunViewResolver.js +31 -0
  47. package/dist/generic/RunViewResolver.js.map +1 -1
  48. package/dist/index.d.ts +3 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +147 -14
  51. package/dist/index.js.map +1 -1
  52. package/dist/integration/CustomColumnPromoter.d.ts +79 -15
  53. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
  54. package/dist/integration/CustomColumnPromoter.js +472 -97
  55. package/dist/integration/CustomColumnPromoter.js.map +1 -1
  56. package/dist/integration/InactiveRowWarnings.d.ts +48 -0
  57. package/dist/integration/InactiveRowWarnings.d.ts.map +1 -0
  58. package/dist/integration/InactiveRowWarnings.js +57 -0
  59. package/dist/integration/InactiveRowWarnings.js.map +1 -0
  60. package/dist/integration/RSUProgressBridge.d.ts +83 -0
  61. package/dist/integration/RSUProgressBridge.d.ts.map +1 -0
  62. package/dist/integration/RSUProgressBridge.js +230 -0
  63. package/dist/integration/RSUProgressBridge.js.map +1 -0
  64. package/dist/integration/SchemaRefreshLaunch.d.ts +62 -0
  65. package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
  66. package/dist/integration/SchemaRefreshLaunch.js +67 -0
  67. package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
  68. package/dist/providerConfigUnits.d.ts +20 -0
  69. package/dist/providerConfigUnits.d.ts.map +1 -0
  70. package/dist/providerConfigUnits.js +22 -0
  71. package/dist/providerConfigUnits.js.map +1 -0
  72. package/dist/resolvers/AdhocQueryResolver.d.ts +4 -0
  73. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  74. package/dist/resolvers/AdhocQueryResolver.js +20 -6
  75. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  76. package/dist/resolvers/FileResolver.d.ts +34 -4
  77. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  78. package/dist/resolvers/FileResolver.js +218 -28
  79. package/dist/resolvers/FileResolver.js.map +1 -1
  80. package/dist/resolvers/IdentityClaimRedemptionResolver.d.ts +37 -0
  81. package/dist/resolvers/IdentityClaimRedemptionResolver.d.ts.map +1 -0
  82. package/dist/resolvers/IdentityClaimRedemptionResolver.js +220 -0
  83. package/dist/resolvers/IdentityClaimRedemptionResolver.js.map +1 -0
  84. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +108 -3
  85. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  86. package/dist/resolvers/IntegrationDiscoveryResolver.js +374 -87
  87. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  88. package/dist/resolvers/QueryResolver.d.ts +5 -4
  89. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  90. package/dist/resolvers/QueryResolver.js +29 -14
  91. package/dist/resolvers/QueryResolver.js.map +1 -1
  92. package/dist/resolvers/QuerySystemUserResolver.d.ts +12 -3
  93. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  94. package/dist/resolvers/QuerySystemUserResolver.js +33 -14
  95. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  96. package/dist/resolvers/RunAIAgentResolver.d.ts +3 -1
  97. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  98. package/dist/resolvers/RunAIAgentResolver.js +20 -7
  99. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  100. package/dist/resolvers/TaskGraphFrameResolver.d.ts +18 -0
  101. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  102. package/dist/resolvers/TaskGraphFrameResolver.js +75 -0
  103. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  104. package/dist/resolvers/UserResolver.d.ts +19 -17
  105. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  106. package/dist/resolvers/UserResolver.js +66 -67
  107. package/dist/resolvers/UserResolver.js.map +1 -1
  108. package/dist/resolvers/currentUserRoles.d.ts +19 -0
  109. package/dist/resolvers/currentUserRoles.d.ts.map +1 -0
  110. package/dist/resolvers/currentUserRoles.js +36 -0
  111. package/dist/resolvers/currentUserRoles.js.map +1 -0
  112. package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -1
  113. package/dist/resolvers/meetingRecordingRegistration.js +3 -6
  114. package/dist/resolvers/meetingRecordingRegistration.js.map +1 -1
  115. package/dist/rest/MediaAccessKeys.d.ts +17 -0
  116. package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
  117. package/dist/rest/MediaAccessKeys.js +42 -1
  118. package/dist/rest/MediaAccessKeys.js.map +1 -1
  119. package/dist/rest/MediaStreamHandler.d.ts +3 -2
  120. package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
  121. package/dist/rest/MediaStreamHandler.js +83 -2
  122. package/dist/rest/MediaStreamHandler.js.map +1 -1
  123. package/dist/rest/OAuthCallbackHandler.d.ts +49 -1
  124. package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
  125. package/dist/rest/OAuthCallbackHandler.js +129 -43
  126. package/dist/rest/OAuthCallbackHandler.js.map +1 -1
  127. package/dist/rest/UploadTokenManager.d.ts +90 -0
  128. package/dist/rest/UploadTokenManager.d.ts.map +1 -0
  129. package/dist/rest/UploadTokenManager.js +156 -0
  130. package/dist/rest/UploadTokenManager.js.map +1 -0
  131. package/dist/services/IntegrationSyncWorkerService.d.ts +48 -0
  132. package/dist/services/IntegrationSyncWorkerService.d.ts.map +1 -0
  133. package/dist/services/IntegrationSyncWorkerService.js +132 -0
  134. package/dist/services/IntegrationSyncWorkerService.js.map +1 -0
  135. package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
  136. package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
  137. package/dist/services/StartTaskGraphDispatcher.js +21 -1
  138. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  139. package/dist/services/TaskGraphAgentRunner.d.ts +7 -0
  140. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  141. package/dist/services/TaskGraphAgentRunner.js +23 -0
  142. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  143. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  144. package/dist/services/TaskGraphContinuationDeliverer.js +12 -1
  145. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  146. package/dist/types.d.ts +14 -0
  147. package/dist/types.d.ts.map +1 -1
  148. package/dist/types.js.map +1 -1
  149. package/package.json +92 -92
  150. package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
  151. package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +84 -0
  152. package/src/__tests__/AuthProviderEngine.test.ts +278 -0
  153. package/src/__tests__/CustomColumnPromoterOnePass.test.ts +387 -0
  154. package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
  155. package/src/__tests__/InactiveRowWarnings.test.ts +87 -0
  156. package/src/__tests__/IntegrationSyncWorkerService.test.ts +275 -0
  157. package/src/__tests__/OAuthCallbackHandler.openRedirect.test.ts +117 -0
  158. package/src/__tests__/OAuthCallbackHandler.xss.test.ts +4 -1
  159. package/src/__tests__/RESTEndpointHandler.test.ts +672 -0
  160. package/src/__tests__/RSUProgressBridge.test.ts +250 -0
  161. package/src/__tests__/ReactivateRefreshMessage.test.ts +85 -0
  162. package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
  163. package/src/__tests__/ResolverBase.filterEscaping.test.ts +147 -0
  164. package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
  165. package/src/__tests__/SessionManager.test.ts +55 -0
  166. package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
  167. package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
  168. package/src/__tests__/config-units.test.ts +74 -0
  169. package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
  170. package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
  171. package/src/__tests__/newUsers.test.ts +726 -0
  172. package/src/__tests__/uploadTokenManager.test.ts +159 -0
  173. package/src/agentSessions/SessionManager.ts +27 -9
  174. package/src/auth/AuthProviderCatalogRouter.ts +76 -0
  175. package/src/auth/AuthProviderEngine.ts +253 -0
  176. package/src/auth/index.ts +60 -15
  177. package/src/auth/initializeProviders.ts +128 -7
  178. package/src/auth/scopeLimitedPrincipal.ts +50 -0
  179. package/src/config.ts +55 -36
  180. package/src/context.ts +26 -2
  181. package/src/entitySubclasses/MJEntityPermissionEntityServer.server.ts +8 -7
  182. package/src/generated/generated.ts +2347 -10551
  183. package/src/generic/ResolverBase.ts +67 -8
  184. package/src/generic/RunViewResolver.ts +31 -0
  185. package/src/index.ts +165 -16
  186. package/src/integration/CustomColumnPromoter.ts +520 -98
  187. package/src/integration/InactiveRowWarnings.ts +85 -0
  188. package/src/integration/RSUProgressBridge.ts +248 -0
  189. package/src/integration/SchemaRefreshLaunch.ts +93 -0
  190. package/src/providerConfigUnits.ts +21 -0
  191. package/src/resolvers/AdhocQueryResolver.ts +21 -6
  192. package/src/resolvers/FileResolver.ts +200 -40
  193. package/src/resolvers/IdentityClaimRedemptionResolver.ts +184 -0
  194. package/src/resolvers/IntegrationDiscoveryResolver.ts +390 -71
  195. package/src/resolvers/QueryResolver.ts +25 -11
  196. package/src/resolvers/QuerySystemUserResolver.ts +28 -13
  197. package/src/resolvers/RunAIAgentResolver.ts +21 -6
  198. package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
  199. package/src/resolvers/UserResolver.ts +99 -60
  200. package/src/resolvers/currentUserRoles.ts +47 -0
  201. package/src/resolvers/meetingRecordingRegistration.ts +3 -7
  202. package/src/rest/MediaAccessKeys.ts +47 -1
  203. package/src/rest/MediaStreamHandler.ts +98 -3
  204. package/src/rest/OAuthCallbackHandler.ts +149 -41
  205. package/src/rest/UploadTokenManager.ts +211 -0
  206. package/src/services/IntegrationSyncWorkerService.ts +141 -0
  207. package/src/services/StartTaskGraphDispatcher.ts +21 -1
  208. package/src/services/TaskGraphAgentRunner.ts +23 -0
  209. package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
  210. package/src/types.ts +14 -0
@@ -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
 
@@ -0,0 +1,184 @@
1
+ /**
2
+ * @fileoverview GraphQL redemption surface for MJ: Identity Claims.
3
+ *
4
+ * Exposes the server-side claim lifecycle (`IdentityClaimEngineServer`) to authenticated
5
+ * clients: list my pending claims, redeem one claim (optionally with the emailed verification
6
+ * token), and redeem everything pending for my email in one call. This is the surface the
7
+ * claim emails' `/claims/redeem?id=..&token=..` links land on (via the Explorer redeem page).
8
+ *
9
+ * Security posture:
10
+ * - Authentication is required (the engine refuses without a user); authorization is the
11
+ * engine's own gate — email match or timing-safe token match, further constrained by the
12
+ * claim type's Configuration and the IdP's `email_verified` assertion carried on
13
+ * `UserPayload.emailVerified`.
14
+ * - A small in-memory fixed-window rate limit bounds redemption attempts per user, since
15
+ * GraphQL mutations get no per-operation limit from the transport tier.
16
+ *
17
+ * @module @memberjunction/server/resolvers/IdentityClaimRedemptionResolver
18
+ */
19
+ import { Arg, Ctx, Field, Mutation, ObjectType, Query, Resolver } from 'type-graphql';
20
+ import { UserInfo } from '@memberjunction/core';
21
+ import { IdentityClaimEngineServer } from '@memberjunction/core-entities-server';
22
+ import { AppContext } from '../types.js';
23
+ import { GetReadWriteProvider } from '../util.js';
24
+ import { ResolverBase } from '../generic/ResolverBase.js';
25
+
26
+ @ObjectType()
27
+ export class IdentityClaimRedemptionResult {
28
+ @Field()
29
+ Success: boolean;
30
+
31
+ @Field({ nullable: true })
32
+ ErrorMessage?: string;
33
+
34
+ @Field({ nullable: true })
35
+ ClaimID?: string;
36
+
37
+ /** JSON-serialized driver result data (shape is driver-specific) */
38
+ @Field({ nullable: true })
39
+ Data?: string;
40
+ }
41
+
42
+ @ObjectType()
43
+ export class PendingIdentityClaim {
44
+ @Field()
45
+ ID: string;
46
+
47
+ @Field()
48
+ ClaimTypeID: string;
49
+
50
+ @Field({ nullable: true })
51
+ ClaimTypeName?: string;
52
+
53
+ @Field({ nullable: true })
54
+ EntityID?: string;
55
+
56
+ @Field({ nullable: true })
57
+ RecordID?: string;
58
+
59
+ @Field()
60
+ Status: string;
61
+
62
+ @Field()
63
+ ExpiresAt: Date;
64
+ }
65
+
66
+ /**
67
+ * Fixed-window per-user attempt limiter for redemption mutations. Redemption is a
68
+ * human-interactive flow — a handful of attempts per minute is generous; dozens is
69
+ * token guessing.
70
+ */
71
+ const REDEEM_WINDOW_MS = 60_000;
72
+ const REDEEM_MAX_ATTEMPTS_PER_WINDOW = 10;
73
+ const REDEEM_ATTEMPTS_CACHE_MAX = 10_000;
74
+ const redeemAttempts = new Map<string, { windowStart: number; count: number }>();
75
+
76
+ function redeemRateLimitExceeded(userId: string): boolean {
77
+ const now = Date.now();
78
+ const entry = redeemAttempts.get(userId);
79
+ if (!entry || now - entry.windowStart >= REDEEM_WINDOW_MS) {
80
+ if (redeemAttempts.size >= REDEEM_ATTEMPTS_CACHE_MAX) {
81
+ // Bounded map: evict the oldest insertion rather than growing without limit.
82
+ const oldest = redeemAttempts.keys().next().value;
83
+ if (oldest !== undefined) {
84
+ redeemAttempts.delete(oldest);
85
+ }
86
+ }
87
+ redeemAttempts.set(userId, { windowStart: now, count: 1 });
88
+ return false;
89
+ }
90
+ entry.count++;
91
+ return entry.count > REDEEM_MAX_ATTEMPTS_PER_WINDOW;
92
+ }
93
+
94
+ @Resolver()
95
+ export class IdentityClaimRedemptionResolver extends ResolverBase {
96
+ /**
97
+ * Lists the caller's pending, unexpired identity claims (matched on their normalized email).
98
+ */
99
+ @Query(() => [PendingIdentityClaim])
100
+ async GetMyPendingIdentityClaims(@Ctx() ctx: AppContext): Promise<PendingIdentityClaim[]> {
101
+ const user = ctx.userPayload.userRecord as UserInfo;
102
+ if (!user) {
103
+ throw new Error('User is not authenticated');
104
+ }
105
+ await this.CheckAPIKeyScopeAuthorization('identity:claims', 'read', ctx.userPayload);
106
+
107
+ const engine = IdentityClaimEngineServer.Instance;
108
+ await engine.Config(false, user);
109
+ const provider = GetReadWriteProvider(ctx.providers);
110
+ const claims = await engine.GetPendingClaimsForEmail(user.Email, user, provider);
111
+ return claims.map((c) => ({
112
+ ID: c.ID,
113
+ ClaimTypeID: c.ClaimTypeID,
114
+ ClaimTypeName: engine.GetClaimTypeByID(c.ClaimTypeID)?.Name,
115
+ EntityID: c.EntityID ?? undefined,
116
+ RecordID: c.RecordID ?? undefined,
117
+ Status: c.Status,
118
+ ExpiresAt: c.ExpiresAt,
119
+ }));
120
+ }
121
+
122
+ /**
123
+ * Redeems a single identity claim for the authenticated caller. The engine enforces the
124
+ * authorization gate: email match (subject to the IdP's email_verified assertion and the
125
+ * claim type's Configuration) OR the emailed verification token.
126
+ */
127
+ @Mutation(() => IdentityClaimRedemptionResult)
128
+ async RedeemIdentityClaim(
129
+ @Arg('ClaimID') claimID: string,
130
+ @Ctx() ctx: AppContext,
131
+ @Arg('Token', { nullable: true }) token?: string,
132
+ ): Promise<IdentityClaimRedemptionResult> {
133
+ const user = ctx.userPayload.userRecord as UserInfo;
134
+ if (!user) {
135
+ throw new Error('User is not authenticated');
136
+ }
137
+ await this.CheckAPIKeyScopeAuthorization('identity:claims', claimID, ctx.userPayload);
138
+
139
+ if (redeemRateLimitExceeded(user.ID)) {
140
+ return { Success: false, ErrorMessage: 'Too many redemption attempts — please wait a minute and try again', ClaimID: claimID };
141
+ }
142
+
143
+ const engine = IdentityClaimEngineServer.Instance;
144
+ await engine.Config(false, user);
145
+ const provider = GetReadWriteProvider(ctx.providers);
146
+ const result = await engine.RedeemClaim(claimID, user, provider, token ?? undefined, {
147
+ EmailVerified: ctx.userPayload.emailVerified,
148
+ });
149
+ return {
150
+ Success: result.Success,
151
+ ErrorMessage: result.ErrorMessage,
152
+ ClaimID: claimID,
153
+ Data: result.Data ? JSON.stringify(result.Data) : undefined,
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Discovers and redeems every pending claim addressed to the caller's email — the on-demand
159
+ * form of the automatic claim-on-login sweep. Each redemption runs through the full engine
160
+ * gate; failures are returned per-claim, never thrown.
161
+ */
162
+ @Mutation(() => [IdentityClaimRedemptionResult])
163
+ async AutoClaimPendingIdentityClaims(@Ctx() ctx: AppContext): Promise<IdentityClaimRedemptionResult[]> {
164
+ const user = ctx.userPayload.userRecord as UserInfo;
165
+ if (!user) {
166
+ throw new Error('User is not authenticated');
167
+ }
168
+ await this.CheckAPIKeyScopeAuthorization('identity:claims', 'auto', ctx.userPayload);
169
+
170
+ if (redeemRateLimitExceeded(user.ID)) {
171
+ return [{ Success: false, ErrorMessage: 'Too many redemption attempts — please wait a minute and try again' }];
172
+ }
173
+
174
+ const engine = IdentityClaimEngineServer.Instance;
175
+ await engine.Config(false, user);
176
+ const provider = GetReadWriteProvider(ctx.providers);
177
+ const results = await engine.AutoClaimForUser(user, provider, { EmailVerified: ctx.userPayload.emailVerified });
178
+ return results.map((r) => ({
179
+ Success: r.Success,
180
+ ErrorMessage: r.ErrorMessage,
181
+ Data: r.Data ? JSON.stringify(r.Data) : undefined,
182
+ }));
183
+ }
184
+ }