@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.
- package/LICENSE +180 -4
- package/dist/agentSessions/SessionManager.d.ts +19 -5
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +27 -9
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.d.ts +31 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -0
- package/dist/auth/AuthProviderCatalogRouter.js +69 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -0
- package/dist/auth/AuthProviderEngine.d.ts +96 -0
- package/dist/auth/AuthProviderEngine.d.ts.map +1 -0
- package/dist/auth/AuthProviderEngine.js +245 -0
- package/dist/auth/AuthProviderEngine.js.map +1 -0
- package/dist/auth/index.d.ts +12 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +57 -13
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +64 -1
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +117 -5
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/auth/scopeLimitedPrincipal.d.ts +39 -0
- package/dist/auth/scopeLimitedPrincipal.d.ts.map +1 -0
- package/dist/auth/scopeLimitedPrincipal.js +49 -0
- package/dist/auth/scopeLimitedPrincipal.js.map +1 -0
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +52 -35
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +21 -1
- package/dist/context.js.map +1 -1
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts.map +1 -1
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js +8 -7
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js.map +1 -1
- package/dist/generated/generated.d.ts +619 -1597
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +3341 -17021
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +28 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +57 -8
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +31 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +147 -14
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts +79 -15
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
- package/dist/integration/CustomColumnPromoter.js +472 -97
- package/dist/integration/CustomColumnPromoter.js.map +1 -1
- package/dist/integration/InactiveRowWarnings.d.ts +48 -0
- package/dist/integration/InactiveRowWarnings.d.ts.map +1 -0
- package/dist/integration/InactiveRowWarnings.js +57 -0
- package/dist/integration/InactiveRowWarnings.js.map +1 -0
- package/dist/integration/RSUProgressBridge.d.ts +83 -0
- package/dist/integration/RSUProgressBridge.d.ts.map +1 -0
- package/dist/integration/RSUProgressBridge.js +230 -0
- package/dist/integration/RSUProgressBridge.js.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts +62 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.js +67 -0
- package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
- package/dist/providerConfigUnits.d.ts +20 -0
- package/dist/providerConfigUnits.d.ts.map +1 -0
- package/dist/providerConfigUnits.js +22 -0
- package/dist/providerConfigUnits.js.map +1 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts +4 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +20 -6
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +34 -4
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +218 -28
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/IdentityClaimRedemptionResolver.d.ts +37 -0
- package/dist/resolvers/IdentityClaimRedemptionResolver.d.ts.map +1 -0
- package/dist/resolvers/IdentityClaimRedemptionResolver.js +220 -0
- package/dist/resolvers/IdentityClaimRedemptionResolver.js.map +1 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +108 -3
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +374 -87
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +5 -4
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +29 -14
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +12 -3
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +33 -14
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +3 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +20 -7
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +18 -0
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +75 -0
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.d.ts +19 -17
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +66 -67
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/currentUserRoles.d.ts +19 -0
- package/dist/resolvers/currentUserRoles.d.ts.map +1 -0
- package/dist/resolvers/currentUserRoles.js +36 -0
- package/dist/resolvers/currentUserRoles.js.map +1 -0
- package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -1
- package/dist/resolvers/meetingRecordingRegistration.js +3 -6
- package/dist/resolvers/meetingRecordingRegistration.js.map +1 -1
- package/dist/rest/MediaAccessKeys.d.ts +17 -0
- package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
- package/dist/rest/MediaAccessKeys.js +42 -1
- package/dist/rest/MediaAccessKeys.js.map +1 -1
- package/dist/rest/MediaStreamHandler.d.ts +3 -2
- package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
- package/dist/rest/MediaStreamHandler.js +83 -2
- package/dist/rest/MediaStreamHandler.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.d.ts +49 -1
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +129 -43
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/UploadTokenManager.d.ts +90 -0
- package/dist/rest/UploadTokenManager.d.ts.map +1 -0
- package/dist/rest/UploadTokenManager.js +156 -0
- package/dist/rest/UploadTokenManager.js.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts +48 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.js +132 -0
- package/dist/services/IntegrationSyncWorkerService.js.map +1 -0
- package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +21 -1
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphAgentRunner.d.ts +7 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +23 -0
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +12 -1
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +92 -92
- package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +84 -0
- package/src/__tests__/AuthProviderEngine.test.ts +278 -0
- package/src/__tests__/CustomColumnPromoterOnePass.test.ts +387 -0
- package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
- package/src/__tests__/InactiveRowWarnings.test.ts +87 -0
- package/src/__tests__/IntegrationSyncWorkerService.test.ts +275 -0
- package/src/__tests__/OAuthCallbackHandler.openRedirect.test.ts +117 -0
- package/src/__tests__/OAuthCallbackHandler.xss.test.ts +4 -1
- package/src/__tests__/RESTEndpointHandler.test.ts +672 -0
- package/src/__tests__/RSUProgressBridge.test.ts +250 -0
- package/src/__tests__/ReactivateRefreshMessage.test.ts +85 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +147 -0
- package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
- package/src/__tests__/SessionManager.test.ts +55 -0
- package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
- package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
- package/src/__tests__/config-units.test.ts +74 -0
- package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
- package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
- package/src/__tests__/newUsers.test.ts +726 -0
- package/src/__tests__/uploadTokenManager.test.ts +159 -0
- package/src/agentSessions/SessionManager.ts +27 -9
- package/src/auth/AuthProviderCatalogRouter.ts +76 -0
- package/src/auth/AuthProviderEngine.ts +253 -0
- package/src/auth/index.ts +60 -15
- package/src/auth/initializeProviders.ts +128 -7
- package/src/auth/scopeLimitedPrincipal.ts +50 -0
- package/src/config.ts +55 -36
- package/src/context.ts +26 -2
- package/src/entitySubclasses/MJEntityPermissionEntityServer.server.ts +8 -7
- package/src/generated/generated.ts +2347 -10551
- package/src/generic/ResolverBase.ts +67 -8
- package/src/generic/RunViewResolver.ts +31 -0
- package/src/index.ts +165 -16
- package/src/integration/CustomColumnPromoter.ts +520 -98
- package/src/integration/InactiveRowWarnings.ts +85 -0
- package/src/integration/RSUProgressBridge.ts +248 -0
- package/src/integration/SchemaRefreshLaunch.ts +93 -0
- package/src/providerConfigUnits.ts +21 -0
- package/src/resolvers/AdhocQueryResolver.ts +21 -6
- package/src/resolvers/FileResolver.ts +200 -40
- package/src/resolvers/IdentityClaimRedemptionResolver.ts +184 -0
- package/src/resolvers/IntegrationDiscoveryResolver.ts +390 -71
- package/src/resolvers/QueryResolver.ts +25 -11
- package/src/resolvers/QuerySystemUserResolver.ts +28 -13
- package/src/resolvers/RunAIAgentResolver.ts +21 -6
- package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
- package/src/resolvers/UserResolver.ts +99 -60
- package/src/resolvers/currentUserRoles.ts +47 -0
- package/src/resolvers/meetingRecordingRegistration.ts +3 -7
- package/src/rest/MediaAccessKeys.ts +47 -1
- package/src/rest/MediaStreamHandler.ts +98 -3
- package/src/rest/OAuthCallbackHandler.ts +149 -41
- package/src/rest/UploadTokenManager.ts +211 -0
- package/src/services/IntegrationSyncWorkerService.ts +141 -0
- package/src/services/StartTaskGraphDispatcher.ts +21 -1
- package/src/services/TaskGraphAgentRunner.ts +23 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
- 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
|
|
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
|
-
|
|
745
|
-
const
|
|
746
|
-
const downloadUrl = await
|
|
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(
|
|
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
|
-
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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:
|
|
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
|
+
}
|