@memberjunction/server 6.1.0-edge.1 → 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.
- package/LICENSE +180 -4
- 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/exampleNewUserSubClass.js +1 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/index.d.ts +11 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +65 -18
- 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/magicLink/MagicLinkService.js +1 -1
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.js +1 -1
- package/dist/auth/newUsers.js.map +1 -1
- 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.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +540 -1595
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2755 -16857
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +20 -4
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +58 -21
- 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 +183 -54
- package/dist/index.js.map +1 -1
- 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 +52 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.js +50 -0
- package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
- package/dist/logging/secretRedactor.d.ts +9 -0
- package/dist/logging/secretRedactor.d.ts.map +1 -1
- package/dist/logging/secretRedactor.js +39 -0
- package/dist/logging/secretRedactor.js.map +1 -1
- package/dist/middleware/BaseServerMiddleware.d.ts +5 -0
- package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
- package/dist/middleware/BaseServerMiddleware.js +5 -0
- package/dist/middleware/BaseServerMiddleware.js.map +1 -1
- 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/realtimeWidget/WidgetSessionService.js +1 -1
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +12 -0
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/EntityPermissionResolver.js +1 -1
- package/dist/resolvers/EntityPermissionResolver.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/IntegrationDiscoveryResolver.d.ts +85 -3
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +302 -82
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.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/RealtimeClientSessionResolver.d.ts +10 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +5 -0
- package/dist/resolvers/RealtimeClientSessionResolver.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/SearchEntitiesResolver.js +1 -1
- package/dist/resolvers/SearchEntitiesResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +2 -2
- package/dist/resolvers/SyncRolesUsersResolver.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/UserFavoriteResolver.js +1 -1
- package/dist/resolvers/UserFavoriteResolver.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/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 +130 -44
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/SignatureWebhookHandler.js +1 -1
- package/dist/rest/SignatureWebhookHandler.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/DurableEntityActionTaskSubmitter.d.ts +26 -0
- package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -0
- package/dist/services/DurableEntityActionTaskSubmitter.js +55 -0
- package/dist/services/DurableEntityActionTaskSubmitter.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/ScheduledJobsService.js +1 -1
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +37 -2
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +34 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphActionRunner.js +95 -0
- package/dist/services/TaskGraphActionRunner.js.map +1 -0
- package/dist/services/TaskGraphAgentRunner.d.ts +20 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +53 -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/services/TaskGraphPromptRunner.d.ts +31 -0
- package/dist/services/TaskGraphPromptRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphPromptRunner.js +134 -0
- package/dist/services/TaskGraphPromptRunner.js.map +1 -0
- package/dist/telephony/RingCentralTelephonyService.js +1 -1
- package/dist/telephony/RingCentralTelephonyService.js.map +1 -1
- package/dist/telephony/TwilioTelephonyRouter.js +1 -1
- package/dist/telephony/TwilioTelephonyRouter.js.map +1 -1
- package/dist/telephony/VonageTelephonyRouter.js +1 -1
- package/dist/telephony/VonageTelephonyRouter.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +91 -91
- package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
- package/src/__tests__/AuthProviderEngine.test.ts +278 -0
- package/src/__tests__/EntityCRUDHandler.test.ts +378 -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__/RealtimeBridgeResolver.test.ts +10 -2
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +1 -1
- package/src/__tests__/ResolverBase.frozenRecordMapping.test.ts +133 -0
- package/src/__tests__/ResolverBase.transportMapping.test.ts +289 -0
- package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
- package/src/__tests__/SessionManager.test.ts +1 -1
- package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
- package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
- package/src/__tests__/WidgetSessionService.test.ts +1 -1
- package/src/__tests__/config-units.test.ts +74 -0
- package/src/__tests__/databaseAbstraction.test.ts +1 -1
- package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +1 -1
- package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
- package/src/__tests__/newUsers.test.ts +726 -0
- package/src/__tests__/resolverBase.rls.test.ts +3 -0
- package/src/__tests__/secretRedactor.failOpen.test.ts +135 -0
- package/src/__tests__/uploadTokenManager.test.ts +159 -0
- package/src/__tests__/widgetGuestElevation.test.ts +1 -1
- package/src/auth/AuthProviderCatalogRouter.ts +76 -0
- package/src/auth/AuthProviderEngine.ts +253 -0
- package/src/auth/exampleNewUserSubClass.ts +1 -1
- package/src/auth/index.ts +69 -21
- package/src/auth/initializeProviders.ts +128 -7
- package/src/auth/magicLink/MagicLinkService.ts +1 -1
- package/src/auth/newUsers.ts +1 -1
- package/src/config.ts +55 -36
- package/src/context.ts +2 -2
- package/src/generated/generated.ts +2038 -10529
- package/src/generic/ResolverBase.ts +63 -21
- package/src/generic/RunViewResolver.ts +31 -0
- package/src/index.ts +199 -56
- package/src/integration/RSUProgressBridge.ts +248 -0
- package/src/integration/SchemaRefreshLaunch.ts +77 -0
- package/src/logging/secretRedactor.ts +43 -0
- package/src/middleware/BaseServerMiddleware.ts +5 -0
- package/src/providerConfigUnits.ts +21 -0
- package/src/realtimeWidget/WidgetSessionService.ts +1 -1
- package/src/realtimeWidget/widgetGuestElevation.ts +1 -1
- package/src/resolvers/ActionResolver.ts +13 -0
- package/src/resolvers/ComponentRegistryResolver.ts +1 -1
- package/src/resolvers/EntityPermissionResolver.ts +1 -1
- package/src/resolvers/FileResolver.ts +200 -40
- package/src/resolvers/IntegrationDiscoveryResolver.ts +328 -70
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
- package/src/resolvers/QueryResolver.ts +25 -11
- package/src/resolvers/QuerySystemUserResolver.ts +28 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +13 -0
- package/src/resolvers/RunAIAgentResolver.ts +21 -6
- package/src/resolvers/SearchEntitiesResolver.ts +1 -1
- package/src/resolvers/SqlLoggingConfigResolver.ts +1 -2
- package/src/resolvers/SyncRolesUsersResolver.ts +2 -2
- package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
- package/src/resolvers/UserFavoriteResolver.ts +1 -1
- package/src/resolvers/UserResolver.ts +99 -60
- package/src/resolvers/currentUserRoles.ts +47 -0
- package/src/rest/MediaAccessKeys.ts +47 -1
- package/src/rest/MediaStreamHandler.ts +98 -3
- package/src/rest/OAuthCallbackHandler.ts +150 -42
- package/src/rest/SignatureWebhookHandler.ts +1 -1
- package/src/rest/UploadTokenManager.ts +211 -0
- package/src/services/DurableEntityActionTaskSubmitter.ts +78 -0
- package/src/services/IntegrationSyncWorkerService.ts +141 -0
- package/src/services/ScheduledJobsService.ts +1 -1
- package/src/services/StartTaskGraphDispatcher.ts +38 -1
- package/src/services/TaskGraphActionRunner.ts +99 -0
- package/src/services/TaskGraphAgentRunner.ts +58 -1
- package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
- package/src/services/TaskGraphPromptRunner.ts +144 -0
- package/src/telephony/RingCentralTelephonyService.ts +1 -1
- package/src/telephony/TwilioTelephonyRouter.ts +1 -1
- package/src/telephony/VonageTelephonyRouter.ts +1 -1
- package/src/types.ts +6 -0
|
@@ -16,6 +16,9 @@ const { mockUserCacheUsers } = vi.hoisted(() => ({
|
|
|
16
16
|
// Stub external deps before imports
|
|
17
17
|
vi.mock('@memberjunction/sqlserver-dataprovider', () => ({
|
|
18
18
|
SQLServerDataProvider: class {},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock('@memberjunction/generic-database-provider', () => ({
|
|
19
22
|
UserCache: {
|
|
20
23
|
get Users() { return mockUserCacheUsers; },
|
|
21
24
|
},
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import type { IMetadataProvider } from '@memberjunction/core';
|
|
3
|
+
import { redactArg, ResetUnboundCrudInputWarnings } from '../logging/secretRedactor.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `redactArg` binds a GraphQL input type to an entity by NAME:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const match = /^(Create|Update|Delete)(?<name>.+)Input$/.exec(inputTypeName);
|
|
10
|
+
* const entity = provider.Entities.find((e) => e.ClassName === match.groups.name);
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Codegen inputs embed the entity ClassName (`CreateMJCredentialInput` → `MJCredential`)
|
|
14
|
+
* so the lookup succeeds. Hand-written resolver inputs do not (`CreateConnectionInput`
|
|
15
|
+
* → `Connection`, which is no entity), so the lookup fails, no encrypted-field names
|
|
16
|
+
* are contributed, and the arg falls through to `shortenForLog` with values intact.
|
|
17
|
+
*
|
|
18
|
+
* The boot audit tests only the NAME pattern, so it classifies these as metadata-bound
|
|
19
|
+
* and stays silent — the reason the gap went unnoticed. These tests pin both the
|
|
20
|
+
* fail-open behaviour and the warning that now surfaces it.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const SECRET = 'sk-live-CREDENTIAL-PAYLOAD';
|
|
24
|
+
|
|
25
|
+
/** Minimal provider exposing one entity with one encrypted column. */
|
|
26
|
+
function providerWith(entities: { ClassName: string; EncryptedFields: { Name: string }[] }[]): IMetadataProvider {
|
|
27
|
+
return { Entities: entities } as unknown as IMetadataProvider;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const CREDENTIAL_ENTITY = providerWith([
|
|
31
|
+
{ ClassName: 'MJCredential', EncryptedFields: [{ Name: 'Values' }] },
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
ResetUnboundCrudInputWarnings();
|
|
36
|
+
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
vi.restoreAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('redactArg — entity-bound inputs (the codegen case)', () => {
|
|
44
|
+
it('redacts an encrypted column, keeping the rest', () => {
|
|
45
|
+
const out = redactArg({
|
|
46
|
+
inputTypeName: 'CreateMJCredentialInput',
|
|
47
|
+
rawValue: { Name: 'Stripe', Values: SECRET },
|
|
48
|
+
provider: CREDENTIAL_ENTITY,
|
|
49
|
+
noLogParameter: false,
|
|
50
|
+
noLogFields: new Set(),
|
|
51
|
+
}) as Record<string, unknown>;
|
|
52
|
+
|
|
53
|
+
expect(out.Values).toBe('<redacted>');
|
|
54
|
+
expect(out.Name).toBe('Stripe');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('emits no warning, because the binding resolved', () => {
|
|
58
|
+
redactArg({
|
|
59
|
+
inputTypeName: 'CreateMJCredentialInput',
|
|
60
|
+
rawValue: { Values: SECRET },
|
|
61
|
+
provider: CREDENTIAL_ENTITY,
|
|
62
|
+
noLogParameter: false,
|
|
63
|
+
noLogFields: new Set(),
|
|
64
|
+
});
|
|
65
|
+
expect(console.warn).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('redactArg — name matches the convention but no entity exists', () => {
|
|
70
|
+
const unbound = {
|
|
71
|
+
inputTypeName: 'CreateConnectionInput',
|
|
72
|
+
rawValue: { CredentialName: 'acme', CredentialValues: SECRET },
|
|
73
|
+
provider: CREDENTIAL_ENTITY,
|
|
74
|
+
noLogParameter: false,
|
|
75
|
+
noLogFields: new Set<string>(),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
it('falls open — values are logged, which is the defect being surfaced', () => {
|
|
79
|
+
const out = redactArg({ ...unbound });
|
|
80
|
+
// Documents current behaviour: no entity binding and no @NoLog means no
|
|
81
|
+
// redaction source, so the value survives into the log.
|
|
82
|
+
expect(JSON.stringify(out)).toContain(SECRET);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('now warns, naming the input type and what to do about it', () => {
|
|
86
|
+
redactArg({ ...unbound });
|
|
87
|
+
|
|
88
|
+
expect(console.warn).toHaveBeenCalledTimes(1);
|
|
89
|
+
const message = vi.mocked(console.warn).mock.calls[0][0] as string;
|
|
90
|
+
expect(message).toContain('CreateConnectionInput');
|
|
91
|
+
expect(message).toContain('@NoLog');
|
|
92
|
+
// The warning itself must not become a second leak.
|
|
93
|
+
expect(message).not.toContain(SECRET);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('warns only once per input type, however hot the mutation is', () => {
|
|
97
|
+
redactArg({ ...unbound });
|
|
98
|
+
redactArg({ ...unbound });
|
|
99
|
+
redactArg({ ...unbound });
|
|
100
|
+
expect(console.warn).toHaveBeenCalledTimes(1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('warns separately for a different unbound input type', () => {
|
|
104
|
+
redactArg({ ...unbound });
|
|
105
|
+
redactArg({ ...unbound, inputTypeName: 'CreateScheduleInput' });
|
|
106
|
+
expect(console.warn).toHaveBeenCalledTimes(2);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('redactArg — @NoLog closes the gap the metadata cannot', () => {
|
|
111
|
+
it('redacts a marked field on an input with no entity binding', () => {
|
|
112
|
+
const out = redactArg({
|
|
113
|
+
inputTypeName: 'CreateConnectionInput',
|
|
114
|
+
rawValue: { CredentialName: 'acme', CredentialValues: SECRET },
|
|
115
|
+
provider: CREDENTIAL_ENTITY,
|
|
116
|
+
noLogParameter: false,
|
|
117
|
+
noLogFields: new Set(['CredentialValues']),
|
|
118
|
+
}) as Record<string, unknown>;
|
|
119
|
+
|
|
120
|
+
expect(out.CredentialValues).toBe('<redacted>');
|
|
121
|
+
expect(out.CredentialName).toBe('acme');
|
|
122
|
+
expect(JSON.stringify(out)).not.toContain(SECRET);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('a parameter-level mark redacts the whole arg regardless of binding', () => {
|
|
126
|
+
const out = redactArg({
|
|
127
|
+
inputTypeName: 'CreateConnectionInput',
|
|
128
|
+
rawValue: { CredentialValues: SECRET },
|
|
129
|
+
provider: CREDENTIAL_ENTITY,
|
|
130
|
+
noLogParameter: true,
|
|
131
|
+
noLogFields: new Set(),
|
|
132
|
+
});
|
|
133
|
+
expect(out).toBe('<redacted>');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { UploadTokenManager } from '../rest/UploadTokenManager.js';
|
|
3
|
+
|
|
4
|
+
describe('UploadTokenManager', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
UploadTokenManager.Instance.Clear();
|
|
7
|
+
UploadTokenManager.Instance.maxFileSizeBytes = 100 * 1024 * 1024;
|
|
8
|
+
UploadTokenManager.Instance.maxPoolMemoryBytes = 500 * 1024 * 1024;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('stages and consumes a buffer atomically', () => {
|
|
12
|
+
const buf = Buffer.from('hello world raw binary', 'utf-8');
|
|
13
|
+
const token = UploadTokenManager.Instance.Stage({
|
|
14
|
+
buffer: buf,
|
|
15
|
+
fileName: 'test.txt',
|
|
16
|
+
mimeType: 'text/plain',
|
|
17
|
+
userId: 'user-123',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(token).toMatch(/^upt_[a-f0-9]{64}$/);
|
|
21
|
+
expect(UploadTokenManager.Instance.ActiveCount).toBe(1);
|
|
22
|
+
expect(UploadTokenManager.Instance.TotalMemoryBytes).toBe(buf.length);
|
|
23
|
+
|
|
24
|
+
// Consume with matching user
|
|
25
|
+
const consumed = UploadTokenManager.Instance.Consume(token, 'user-123');
|
|
26
|
+
expect(consumed).not.toBeNull();
|
|
27
|
+
expect(consumed?.buffer.toString('utf-8')).toBe('hello world raw binary');
|
|
28
|
+
expect(consumed?.fileName).toBe('test.txt');
|
|
29
|
+
expect(consumed?.mimeType).toBe('text/plain');
|
|
30
|
+
|
|
31
|
+
// Single-use: consuming again must return null
|
|
32
|
+
expect(UploadTokenManager.Instance.Consume(token, 'user-123')).toBeNull();
|
|
33
|
+
expect(UploadTokenManager.Instance.ActiveCount).toBe(0);
|
|
34
|
+
expect(UploadTokenManager.Instance.TotalMemoryBytes).toBe(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('rejects claim by a different user and evicts token', () => {
|
|
38
|
+
const buf = Buffer.from('secret payload', 'utf-8');
|
|
39
|
+
const token = UploadTokenManager.Instance.Stage({
|
|
40
|
+
buffer: buf,
|
|
41
|
+
fileName: 'secret.pdf',
|
|
42
|
+
mimeType: 'application/pdf',
|
|
43
|
+
userId: 'user-authorized',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Claim attempt by attacker
|
|
47
|
+
const result = UploadTokenManager.Instance.Consume(token, 'user-attacker');
|
|
48
|
+
expect(result).toBeNull();
|
|
49
|
+
|
|
50
|
+
// Token must be evicted after failed attempt
|
|
51
|
+
expect(UploadTokenManager.Instance.ActiveCount).toBe(0);
|
|
52
|
+
expect(UploadTokenManager.Instance.Consume(token, 'user-authorized')).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('enforces single file size limit', () => {
|
|
56
|
+
UploadTokenManager.Instance.maxFileSizeBytes = 10;
|
|
57
|
+
const buf = Buffer.alloc(20);
|
|
58
|
+
|
|
59
|
+
expect(() => {
|
|
60
|
+
UploadTokenManager.Instance.Stage({
|
|
61
|
+
buffer: buf,
|
|
62
|
+
fileName: 'huge.dat',
|
|
63
|
+
mimeType: 'application/octet-stream',
|
|
64
|
+
userId: 'user-123',
|
|
65
|
+
});
|
|
66
|
+
}).toThrow(/exceeds maximum allowed file size/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('enforces pool memory capacity limit', () => {
|
|
70
|
+
UploadTokenManager.Instance.maxPoolMemoryBytes = 50;
|
|
71
|
+
|
|
72
|
+
UploadTokenManager.Instance.Stage({
|
|
73
|
+
buffer: Buffer.alloc(30),
|
|
74
|
+
fileName: 'file1.dat',
|
|
75
|
+
mimeType: 'application/octet-stream',
|
|
76
|
+
userId: 'user-123',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(() => {
|
|
80
|
+
UploadTokenManager.Instance.Stage({
|
|
81
|
+
buffer: Buffer.alloc(30),
|
|
82
|
+
fileName: 'file2.dat',
|
|
83
|
+
mimeType: 'application/octet-stream',
|
|
84
|
+
userId: 'user-123',
|
|
85
|
+
});
|
|
86
|
+
}).toThrow(/memory capacity reached/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('auto-evicts token after TTL expiry', async () => {
|
|
90
|
+
vi.useFakeTimers();
|
|
91
|
+
try {
|
|
92
|
+
const buf = Buffer.from('ephemeral data');
|
|
93
|
+
const token = UploadTokenManager.Instance.Stage({
|
|
94
|
+
buffer: buf,
|
|
95
|
+
fileName: 'temp.txt',
|
|
96
|
+
mimeType: 'text/plain',
|
|
97
|
+
userId: 'user-123',
|
|
98
|
+
ttlSeconds: 5,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(UploadTokenManager.Instance.ActiveCount).toBe(1);
|
|
102
|
+
|
|
103
|
+
// Fast-forward 6 seconds
|
|
104
|
+
vi.advanceTimersByTime(6000);
|
|
105
|
+
|
|
106
|
+
expect(UploadTokenManager.Instance.ActiveCount).toBe(0);
|
|
107
|
+
expect(UploadTokenManager.Instance.Consume(token, 'user-123')).toBeNull();
|
|
108
|
+
} finally {
|
|
109
|
+
vi.useRealTimers();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('normalizes UUIDs when verifying claiming user', () => {
|
|
114
|
+
const buf = Buffer.from('uuid test data');
|
|
115
|
+
const token = UploadTokenManager.Instance.Stage({
|
|
116
|
+
buffer: buf,
|
|
117
|
+
fileName: 'uuid.txt',
|
|
118
|
+
mimeType: 'text/plain',
|
|
119
|
+
userId: '4A68B0D7-49EE-4D56-82DE-92F66627EE2B',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Claim with lowercase UUID
|
|
123
|
+
const consumed = UploadTokenManager.Instance.Consume(token, '4a68b0d7-49ee-4d56-82de-92f66627ee2b');
|
|
124
|
+
expect(consumed).not.toBeNull();
|
|
125
|
+
expect(consumed?.buffer.toString('utf-8')).toBe('uuid test data');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('enforces per-user memory quota independently of pool memory', () => {
|
|
129
|
+
UploadTokenManager.Instance.maxPoolMemoryBytes = 500;
|
|
130
|
+
UploadTokenManager.Instance.maxUserMemoryBytes = 40;
|
|
131
|
+
|
|
132
|
+
// User A stages 30 bytes (OK)
|
|
133
|
+
UploadTokenManager.Instance.Stage({
|
|
134
|
+
buffer: Buffer.alloc(30),
|
|
135
|
+
fileName: 'userA_1.dat',
|
|
136
|
+
mimeType: 'application/octet-stream',
|
|
137
|
+
userId: 'user-A',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// User A tries to stage another 20 bytes (exceeds 40 byte user quota)
|
|
141
|
+
expect(() => {
|
|
142
|
+
UploadTokenManager.Instance.Stage({
|
|
143
|
+
buffer: Buffer.alloc(20),
|
|
144
|
+
fileName: 'userA_2.dat',
|
|
145
|
+
mimeType: 'application/octet-stream',
|
|
146
|
+
userId: 'user-A',
|
|
147
|
+
});
|
|
148
|
+
}).toThrow(/Per-user upload memory quota reached/);
|
|
149
|
+
|
|
150
|
+
// User B can still stage 30 bytes because User B has their own quota
|
|
151
|
+
const tokenB = UploadTokenManager.Instance.Stage({
|
|
152
|
+
buffer: Buffer.alloc(30),
|
|
153
|
+
fileName: 'userB_1.dat',
|
|
154
|
+
mimeType: 'application/octet-stream',
|
|
155
|
+
userId: 'user-B',
|
|
156
|
+
});
|
|
157
|
+
expect(tokenB).toBeDefined();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -4,7 +4,7 @@ import type { DatabaseProviderBase, UserInfo } from '@memberjunction/core';
|
|
|
4
4
|
// Controls what UserCache.Instance.GetSystemUser() serves — undefined simulates an unpopulated
|
|
5
5
|
// cache (no system user available), the fail-closed path.
|
|
6
6
|
const getSystemUserMock = vi.fn<[], UserInfo | undefined>();
|
|
7
|
-
vi.mock('@memberjunction/
|
|
7
|
+
vi.mock('@memberjunction/generic-database-provider', () => ({
|
|
8
8
|
UserCache: {
|
|
9
9
|
get Instance() {
|
|
10
10
|
return { GetSystemUser: getSystemUserMock };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Unauthenticated provider-catalog endpoint.
|
|
3
|
+
*
|
|
4
|
+
* The browser must know which identity providers exist BEFORE it can authenticate, so this
|
|
5
|
+
* router is mounted ahead of the unified auth middleware — the same pattern the magic-link JWKS
|
|
6
|
+
* and widget-session routes already use.
|
|
7
|
+
*
|
|
8
|
+
* @module @memberjunction/server/auth
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Router, type Request, type Response } from 'express';
|
|
12
|
+
import { rateLimit } from 'express-rate-limit';
|
|
13
|
+
import { LogError, type PublicAuthProviderCatalog } from '@memberjunction/core';
|
|
14
|
+
import { AuthProviderEngine } from './AuthProviderEngine.js';
|
|
15
|
+
|
|
16
|
+
/** Mount path for the public catalog router. */
|
|
17
|
+
export const AUTH_CATALOG_MOUNT_PATH = '/auth';
|
|
18
|
+
|
|
19
|
+
/** Requests per window per IP. Generous — a browser makes one call per page load. */
|
|
20
|
+
const DEFAULT_RATE_LIMIT_MAX = 60;
|
|
21
|
+
|
|
22
|
+
/** Rate-limit window. */
|
|
23
|
+
const DEFAULT_RATE_LIMIT_WINDOW_MS = 60_000;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* How long a browser may cache the catalog.
|
|
27
|
+
*
|
|
28
|
+
* Short by design: this drives the login screen, so an admin who enables a provider expects to
|
|
29
|
+
* see it without telling users to hard-refresh. A minute keeps the endpoint cheap without making
|
|
30
|
+
* configuration changes feel stuck.
|
|
31
|
+
*/
|
|
32
|
+
const CACHE_MAX_AGE_SECONDS = 60;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Builds the unauthenticated `GET /auth/providers` router.
|
|
36
|
+
*
|
|
37
|
+
* **What it exposes.** Only `Active` + `ClientVisible` rows, projected to the public allow-list
|
|
38
|
+
* by {@link AuthProviderEngine.GetPublicCatalog}. Secret material is structurally unable to reach
|
|
39
|
+
* this response: `CredentialID` and `AdditionalConfiguration` are not part of the public shape,
|
|
40
|
+
* and non-primitive `ClientConfiguration` values are dropped rather than serialized.
|
|
41
|
+
*
|
|
42
|
+
* **Why publishing this is safe.** Every value here was already shipped to anonymous users — a
|
|
43
|
+
* single-provider SPA compiled its client ID, domain and issuer straight into its JavaScript
|
|
44
|
+
* bundle. This endpoint moves that same data behind one source of truth; it does not widen what
|
|
45
|
+
* an anonymous caller can learn.
|
|
46
|
+
*
|
|
47
|
+
* **Failure is empty, not an error.** If the catalog cannot be read, the endpoint returns an empty
|
|
48
|
+
* list so the browser falls back to its compiled `AUTH_TYPE` and the user can still sign in.
|
|
49
|
+
* Returning 500 would take the login screen down for a problem the client can route around.
|
|
50
|
+
*/
|
|
51
|
+
export function createAuthProviderCatalogRouter(): Router {
|
|
52
|
+
const router = Router();
|
|
53
|
+
|
|
54
|
+
const limiter = rateLimit({
|
|
55
|
+
windowMs: DEFAULT_RATE_LIMIT_WINDOW_MS,
|
|
56
|
+
limit: DEFAULT_RATE_LIMIT_MAX,
|
|
57
|
+
standardHeaders: 'draft-7',
|
|
58
|
+
legacyHeaders: false,
|
|
59
|
+
message: { providers: [] }
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
router.get('/providers', limiter, (_req: Request, res: Response) => {
|
|
63
|
+
try {
|
|
64
|
+
const body: PublicAuthProviderCatalog = { providers: AuthProviderEngine.Instance.GetPublicCatalog() };
|
|
65
|
+
res.set('Cache-Control', `public, max-age=${CACHE_MAX_AGE_SECONDS}`);
|
|
66
|
+
res.status(200).json(body);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
|
+
LogError(`[Auth] Failed to serve the public provider catalog: ${message}`);
|
|
70
|
+
const empty: PublicAuthProviderCatalog = { providers: [] };
|
|
71
|
+
res.status(200).json(empty);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return router;
|
|
76
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Metadata-driven authentication provider catalog.
|
|
3
|
+
*
|
|
4
|
+
* Loads `MJ: Authentication Providers` rows and registers each one with the runtime
|
|
5
|
+
* `AuthProviderFactory`, resolving the concrete driver through `ClassFactory` by `DriverClass`.
|
|
6
|
+
* This is the auth subsystem's equivalent of the File Storage `ServerDriverKey` pattern:
|
|
7
|
+
* adding a provider is a subclass plus a row, never an edit to core code.
|
|
8
|
+
*
|
|
9
|
+
* @module @memberjunction/server/auth
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
BaseEngine,
|
|
14
|
+
BaseEnginePropertyConfig,
|
|
15
|
+
LogError,
|
|
16
|
+
LogStatusEx,
|
|
17
|
+
RegisterForStartup,
|
|
18
|
+
UserInfo,
|
|
19
|
+
type AuthProviderConfig,
|
|
20
|
+
type IMetadataProvider,
|
|
21
|
+
type PublicAuthProviderInfo
|
|
22
|
+
} from '@memberjunction/core';
|
|
23
|
+
import { MJAuthenticationProviderEntity } from '@memberjunction/core-entities';
|
|
24
|
+
import { AuthProviderFactory } from '@memberjunction/auth-providers';
|
|
25
|
+
import { CredentialEngine } from '@memberjunction/credentials';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Subsystem label used when this engine asks CredentialEngine to decrypt secret material,
|
|
29
|
+
* so credential-access auditing attributes the read to authentication.
|
|
30
|
+
*/
|
|
31
|
+
const CREDENTIAL_SUBSYSTEM = 'Authentication';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Loads the authentication-provider catalog from metadata and registers it with the
|
|
35
|
+
* {@link AuthProviderFactory}.
|
|
36
|
+
*
|
|
37
|
+
* **Not `deferred`**: no authenticated request may be served before the catalog is registered,
|
|
38
|
+
* or tokens from a metadata-defined provider would be rejected during the startup window.
|
|
39
|
+
*
|
|
40
|
+
* **Severity is `warn`, not `fatal`**, and that is deliberate. This engine is one of two possible
|
|
41
|
+
* sources of providers — `initializeAuthProviders` also registers whatever `mj.config.cjs`
|
|
42
|
+
* declares. Making a catalog load failure fatal would turn a transient metadata problem into a
|
|
43
|
+
* total authentication outage for deployments that never adopted the table. The failure is
|
|
44
|
+
* logged loudly and the config-declared providers still stand.
|
|
45
|
+
*/
|
|
46
|
+
@RegisterForStartup({
|
|
47
|
+
priority: 20,
|
|
48
|
+
severity: 'warn',
|
|
49
|
+
deferred: false,
|
|
50
|
+
description: 'Loads the metadata authentication-provider catalog and registers it with AuthProviderFactory'
|
|
51
|
+
})
|
|
52
|
+
export class AuthProviderEngine extends BaseEngine<AuthProviderEngine> {
|
|
53
|
+
private _providers: MJAuthenticationProviderEntity[] = [];
|
|
54
|
+
|
|
55
|
+
public static get Instance(): AuthProviderEngine {
|
|
56
|
+
return super.getInstance<AuthProviderEngine>();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Loads Active providers only. Inactive rows are excluded at the query rather than filtered
|
|
61
|
+
* later, so a disabled provider is never even instantiated.
|
|
62
|
+
*/
|
|
63
|
+
public async Config(forceRefresh?: boolean, contextUser?: UserInfo, provider?: IMetadataProvider): Promise<void> {
|
|
64
|
+
const configs: Partial<BaseEnginePropertyConfig>[] = [
|
|
65
|
+
{
|
|
66
|
+
PropertyName: '_providers',
|
|
67
|
+
EntityName: 'MJ: Authentication Providers',
|
|
68
|
+
Filter: `Status='Active'`,
|
|
69
|
+
OrderBy: 'Sequence ASC, Name ASC',
|
|
70
|
+
CacheLocal: true
|
|
71
|
+
}
|
|
72
|
+
];
|
|
73
|
+
await this.Load(configs, provider, forceRefresh, contextUser);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Active providers from metadata, ordered for presentation. */
|
|
77
|
+
public get Providers(): MJAuthenticationProviderEntity[] {
|
|
78
|
+
return this.GetConfigData<MJAuthenticationProviderEntity>('_providers');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Startup hook: load the catalog, then register every row with the factory.
|
|
83
|
+
*/
|
|
84
|
+
public async HandleStartup(contextUser?: UserInfo, provider?: IMetadataProvider): Promise<void> {
|
|
85
|
+
await this.Config(false, contextUser, provider);
|
|
86
|
+
await this.RegisterAll(contextUser);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Instantiates and registers every catalog row with the {@link AuthProviderFactory}.
|
|
91
|
+
*
|
|
92
|
+
* One bad row must not cost the deployment its other providers, so each registration is
|
|
93
|
+
* isolated: a failure is logged with the offending provider's name and the loop continues.
|
|
94
|
+
*
|
|
95
|
+
* @returns the number of providers successfully registered.
|
|
96
|
+
*/
|
|
97
|
+
public async RegisterAll(contextUser?: UserInfo): Promise<number> {
|
|
98
|
+
const factory = AuthProviderFactory.Instance;
|
|
99
|
+
let registered = 0;
|
|
100
|
+
|
|
101
|
+
for (const row of this.Providers) {
|
|
102
|
+
try {
|
|
103
|
+
const config = await this.buildProviderConfig(row, contextUser);
|
|
104
|
+
factory.register(AuthProviderFactory.createProvider(config));
|
|
105
|
+
registered++;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
108
|
+
LogError(`[Auth] Could not register metadata provider '${row.Name}' (DriverClass '${row.DriverClass}'): ${message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (registered > 0) {
|
|
113
|
+
LogStatusEx({ message: `[Auth] Registered ${registered} provider(s) from metadata.`, verboseOnly: true });
|
|
114
|
+
}
|
|
115
|
+
return registered;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Projects a catalog row onto the runtime {@link AuthProviderConfig} the driver expects.
|
|
120
|
+
*
|
|
121
|
+
* `AdditionalConfiguration` is spread FIRST so that the modelled columns win over anything a
|
|
122
|
+
* driver-specific blob happens to repeat — the columns are the reviewable, described surface,
|
|
123
|
+
* and a JSON blob must not be able to silently redefine the issuer a token is validated against.
|
|
124
|
+
*/
|
|
125
|
+
private async buildProviderConfig(row: MJAuthenticationProviderEntity, contextUser?: UserInfo): Promise<AuthProviderConfig> {
|
|
126
|
+
const additional = this.parseJsonColumn(row.AdditionalConfiguration, row.Name, 'AdditionalConfiguration');
|
|
127
|
+
const secrets = await this.resolveCredential(row, contextUser);
|
|
128
|
+
|
|
129
|
+
const config: AuthProviderConfig = {
|
|
130
|
+
...additional,
|
|
131
|
+
...secrets,
|
|
132
|
+
name: row.Name,
|
|
133
|
+
type: row.DriverClass
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (row.ClientID) config.clientId = row.ClientID;
|
|
137
|
+
if (row.Domain) config.domain = row.Domain;
|
|
138
|
+
if (row.Issuer) config.issuer = row.Issuer;
|
|
139
|
+
if (row.Audience) config.audience = row.Audience;
|
|
140
|
+
if (row.JWKSUri) config.jwksUri = row.JWKSUri;
|
|
141
|
+
if (row.Scopes) config.scopes = this.parseScopes(row.Scopes);
|
|
142
|
+
|
|
143
|
+
return config;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Splits the delimited `Scopes` column into the parsed list both projections publish.
|
|
148
|
+
*
|
|
149
|
+
* This is the single home of the delimiter convention: `buildProviderConfig` and
|
|
150
|
+
* `GetPublicCatalog` must agree on it, or the server would validate tokens against one scope
|
|
151
|
+
* set while telling the browser to request another.
|
|
152
|
+
*/
|
|
153
|
+
private parseScopes(raw: string | null): string[] {
|
|
154
|
+
return raw ? raw.split(/[\s,]+/).filter((s) => s.length > 0) : [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Decrypts the row's linked credential, when it has one.
|
|
159
|
+
*
|
|
160
|
+
* Almost every provider validates tokens against a public JWKS and needs no secret at all, so
|
|
161
|
+
* this is the uncommon path — present for confidential-client flows, management APIs, and SCIM.
|
|
162
|
+
*/
|
|
163
|
+
private async resolveCredential(row: MJAuthenticationProviderEntity, contextUser?: UserInfo): Promise<Record<string, string>> {
|
|
164
|
+
if (!row.CredentialID || !row.Credential) {
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await CredentialEngine.Instance.Config(false, contextUser);
|
|
169
|
+
const resolved = await CredentialEngine.Instance.getCredential(row.Credential, {
|
|
170
|
+
contextUser,
|
|
171
|
+
subsystem: CREDENTIAL_SUBSYSTEM
|
|
172
|
+
});
|
|
173
|
+
return resolved.values;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parses a JSON configuration column, treating malformed content as absent.
|
|
178
|
+
*
|
|
179
|
+
* Throwing here would take down an otherwise-valid provider over a stray character in an
|
|
180
|
+
* optional extras blob, so the row is registered without the extras and the problem is logged.
|
|
181
|
+
*/
|
|
182
|
+
private parseJsonColumn(raw: string | null, providerName: string, columnName: string): Record<string, unknown> {
|
|
183
|
+
if (!raw?.trim()) {
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const parsed: unknown = JSON.parse(raw);
|
|
188
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
189
|
+
return parsed as Record<string, unknown>;
|
|
190
|
+
}
|
|
191
|
+
LogError(`[Auth] ${columnName} on provider '${providerName}' is not a JSON object; ignoring it.`);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
194
|
+
LogError(`[Auth] ${columnName} on provider '${providerName}' is not valid JSON (${message}); ignoring it.`);
|
|
195
|
+
}
|
|
196
|
+
return {};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Projects the catalog to the PUBLIC shape served by the unauthenticated catalog endpoint.
|
|
201
|
+
*
|
|
202
|
+
* The projection is an explicit allow-list rather than a redaction of the full row: a column
|
|
203
|
+
* added to this entity later is invisible to anonymous callers until someone deliberately adds
|
|
204
|
+
* it here. `AdditionalConfiguration` and `CredentialID` are structurally absent, never merely
|
|
205
|
+
* omitted at serialization time.
|
|
206
|
+
*/
|
|
207
|
+
public GetPublicCatalog(): PublicAuthProviderInfo[] {
|
|
208
|
+
return this.Providers.filter((row) => row.ClientVisible).map((row) => {
|
|
209
|
+
const info: PublicAuthProviderInfo = {
|
|
210
|
+
name: row.Name,
|
|
211
|
+
driverClass: row.DriverClass,
|
|
212
|
+
displayName: row.DisplayName ?? row.Name,
|
|
213
|
+
sequence: row.Sequence,
|
|
214
|
+
isDefault: row.IsDefault
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
if (row.Icon) info.icon = row.Icon;
|
|
218
|
+
if (row.ClientID) info.clientId = row.ClientID;
|
|
219
|
+
if (row.Issuer) info.issuer = row.Issuer;
|
|
220
|
+
if (row.Domain) info.domain = row.Domain;
|
|
221
|
+
|
|
222
|
+
// Published pre-parsed so the browser never re-derives the delimiter convention —
|
|
223
|
+
// the drivers hand this straight to SDK config typed string[].
|
|
224
|
+
const scopes = this.parseScopes(row.Scopes);
|
|
225
|
+
if (scopes.length > 0) info.scopes = scopes;
|
|
226
|
+
|
|
227
|
+
const clientConfig = this.parseJsonColumn(row.ClientConfiguration, row.Name, 'ClientConfiguration');
|
|
228
|
+
if (Object.keys(clientConfig).length > 0) {
|
|
229
|
+
info.clientConfiguration = this.toPublicClientConfig(clientConfig, row.Name);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return info;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Flattens a parsed `ClientConfiguration` blob to the primitive key/value shape the browser
|
|
238
|
+
* contract declares. Nested objects and arrays are dropped with a warning rather than passed
|
|
239
|
+
* through, so a structured blob (the shape a credential would take) cannot ride along to an
|
|
240
|
+
* anonymous caller.
|
|
241
|
+
*/
|
|
242
|
+
private toPublicClientConfig(parsed: Record<string, unknown>, providerName: string): Record<string, string | number | boolean | null> {
|
|
243
|
+
const result: Record<string, string | number | boolean | null> = {};
|
|
244
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
245
|
+
if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) {
|
|
246
|
+
result[key] = value as string | number | boolean | null;
|
|
247
|
+
} else {
|
|
248
|
+
LogError(`[Auth] ClientConfiguration key '${key}' on provider '${providerName}' is not a primitive value; it was NOT published to the public catalog.`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RegisterClass } from '@memberjunction/global';
|
|
2
2
|
import { Metadata, RunView, LogError, EntitySaveOptions } from '@memberjunction/core';
|
|
3
3
|
import { NewUserBase } from './newUsers.js';
|
|
4
|
-
import { UserCache } from '@memberjunction/
|
|
4
|
+
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
5
5
|
import { configInfo } from '../config.js';
|
|
6
6
|
import { MJUserEntity } from '@memberjunction/core-entities';
|
|
7
7
|
|