@naylence/runtime 0.3.12 → 0.3.13
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/dist/browser/index.cjs +1479 -926
- package/dist/browser/index.mjs +1472 -927
- package/dist/cjs/naylence/fame/connector/broadcast-channel-connector.browser.js +1 -1
- package/dist/cjs/naylence/fame/factory-manifest.js +6 -0
- package/dist/cjs/naylence/fame/grants/grant-materializer.js +59 -0
- package/dist/cjs/naylence/fame/node/admission/admission-profile-factory.js +4 -2
- package/dist/cjs/naylence/fame/node/admission/direct-admission-client-factory.js +3 -1
- package/dist/cjs/naylence/fame/node/admission/direct-admission-client.js +12 -9
- package/dist/cjs/naylence/fame/node/default-node-identity-policy-factory.js +21 -0
- package/dist/cjs/naylence/fame/node/default-node-identity-policy.js +60 -0
- package/dist/cjs/naylence/fame/node/factory-commons.js +31 -7
- package/dist/cjs/naylence/fame/node/index.js +11 -1
- package/dist/cjs/naylence/fame/node/node-config.js +4 -0
- package/dist/cjs/naylence/fame/node/node-identity-policy-factory.js +22 -0
- package/dist/cjs/naylence/fame/node/node-identity-policy-profile-factory.js +67 -0
- package/dist/cjs/naylence/fame/node/node-identity-policy.js +2 -0
- package/dist/cjs/naylence/fame/node/node.js +45 -9
- package/dist/cjs/naylence/fame/node/root-session-manager.js +1 -11
- package/dist/cjs/naylence/fame/node/rpc-client-manager.js +10 -3
- package/dist/cjs/naylence/fame/node/token-subject-node-identity-policy-factory.js +55 -0
- package/dist/cjs/naylence/fame/node/token-subject-node-identity-policy.js +84 -0
- package/dist/cjs/naylence/fame/node/upstream-session-manager.js +87 -9
- package/dist/cjs/naylence/fame/security/auth/auth-identity.js +2 -0
- package/dist/cjs/naylence/fame/security/auth/materializable-token-provider.js +9 -0
- package/dist/cjs/naylence/fame/security/auth/oauth2-pkce-token-provider.js +9 -0
- package/dist/cjs/naylence/fame/security/auth/static-token-provider.js +44 -0
- package/dist/cjs/naylence/fame/security/auth/token-provider.js +6 -0
- package/dist/cjs/naylence/fame/security/default-security-manager.js +4 -2
- package/dist/cjs/naylence/fame/security/index.js +1 -0
- package/dist/cjs/naylence/fame/security/keys/default-key-manager.js +1 -1
- package/dist/cjs/naylence/fame/util/task-spawner.js +8 -0
- package/dist/cjs/version.js +2 -2
- package/dist/esm/naylence/fame/connector/broadcast-channel-connector.browser.js +1 -1
- package/dist/esm/naylence/fame/factory-manifest.js +6 -0
- package/dist/esm/naylence/fame/grants/grant-materializer.js +55 -0
- package/dist/esm/naylence/fame/node/admission/admission-profile-factory.js +4 -2
- package/dist/esm/naylence/fame/node/admission/direct-admission-client-factory.js +3 -1
- package/dist/esm/naylence/fame/node/admission/direct-admission-client.js +13 -10
- package/dist/esm/naylence/fame/node/default-node-identity-policy-factory.js +17 -0
- package/dist/esm/naylence/fame/node/default-node-identity-policy.js +56 -0
- package/dist/esm/naylence/fame/node/factory-commons.js +31 -7
- package/dist/esm/naylence/fame/node/index.js +7 -0
- package/dist/esm/naylence/fame/node/node-config.js +4 -0
- package/dist/esm/naylence/fame/node/node-identity-policy-factory.js +18 -0
- package/dist/esm/naylence/fame/node/node-identity-policy-profile-factory.js +63 -0
- package/dist/esm/naylence/fame/node/node-identity-policy.js +1 -0
- package/dist/esm/naylence/fame/node/node.js +45 -9
- package/dist/esm/naylence/fame/node/root-session-manager.js +1 -11
- package/dist/esm/naylence/fame/node/rpc-client-manager.js +10 -3
- package/dist/esm/naylence/fame/node/token-subject-node-identity-policy-factory.js +18 -0
- package/dist/esm/naylence/fame/node/token-subject-node-identity-policy.js +80 -0
- package/dist/esm/naylence/fame/node/upstream-session-manager.js +87 -9
- package/dist/esm/naylence/fame/security/auth/auth-identity.js +1 -0
- package/dist/esm/naylence/fame/security/auth/materializable-token-provider.js +6 -0
- package/dist/esm/naylence/fame/security/auth/oauth2-pkce-token-provider.js +9 -0
- package/dist/esm/naylence/fame/security/auth/static-token-provider.js +44 -0
- package/dist/esm/naylence/fame/security/auth/token-provider.js +5 -0
- package/dist/esm/naylence/fame/security/default-security-manager.js +4 -2
- package/dist/esm/naylence/fame/security/index.js +1 -0
- package/dist/esm/naylence/fame/security/keys/default-key-manager.js +1 -1
- package/dist/esm/naylence/fame/util/task-spawner.js +8 -0
- package/dist/esm/version.js +2 -2
- package/dist/node/index.cjs +1432 -879
- package/dist/node/index.mjs +1425 -880
- package/dist/node/node.cjs +1560 -1007
- package/dist/node/node.mjs +1553 -1008
- package/dist/types/naylence/fame/factory-manifest.d.ts +1 -1
- package/dist/types/naylence/fame/grants/grant-materializer.d.ts +4 -0
- package/dist/types/naylence/fame/node/admission/admission-profile-factory.d.ts +1 -1
- package/dist/types/naylence/fame/node/admission/direct-admission-client-factory.d.ts +1 -1
- package/dist/types/naylence/fame/node/admission/direct-admission-client.d.ts +3 -0
- package/dist/types/naylence/fame/node/default-node-identity-policy-factory.d.ts +15 -0
- package/dist/types/naylence/fame/node/default-node-identity-policy.d.ts +5 -0
- package/dist/types/naylence/fame/node/factory-commons.d.ts +2 -0
- package/dist/types/naylence/fame/node/index.d.ts +7 -0
- package/dist/types/naylence/fame/node/node-config.d.ts +2 -0
- package/dist/types/naylence/fame/node/node-identity-policy-factory.d.ts +12 -0
- package/dist/types/naylence/fame/node/node-identity-policy-profile-factory.d.ts +15 -0
- package/dist/types/naylence/fame/node/node-identity-policy.d.ts +26 -0
- package/dist/types/naylence/fame/node/node-like.d.ts +3 -1
- package/dist/types/naylence/fame/node/node.d.ts +4 -1
- package/dist/types/naylence/fame/node/root-session-manager.d.ts +0 -1
- package/dist/types/naylence/fame/node/rpc-client-manager.d.ts +2 -0
- package/dist/types/naylence/fame/node/token-subject-node-identity-policy-factory.d.ts +14 -0
- package/dist/types/naylence/fame/node/token-subject-node-identity-policy.d.ts +5 -0
- package/dist/types/naylence/fame/node/upstream-session-manager.d.ts +4 -0
- package/dist/types/naylence/fame/security/auth/auth-identity.d.ts +6 -0
- package/dist/types/naylence/fame/security/auth/materializable-token-provider.d.ts +12 -0
- package/dist/types/naylence/fame/security/auth/oauth2-pkce-token-provider.d.ts +4 -2
- package/dist/types/naylence/fame/security/auth/static-token-provider.d.ts +4 -2
- package/dist/types/naylence/fame/security/auth/token-provider.d.ts +5 -0
- package/dist/types/naylence/fame/security/index.d.ts +1 -0
- package/dist/types/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ export class DirectAdmissionClientFactory extends AdmissionClientFactory {
|
|
|
10
10
|
super(...arguments);
|
|
11
11
|
this.type = 'DirectAdmissionClient';
|
|
12
12
|
}
|
|
13
|
-
async create(config) {
|
|
13
|
+
async create(config, ...factoryArgs) {
|
|
14
14
|
if (!config) {
|
|
15
15
|
throw new Error('DirectAdmissionClient configuration is required');
|
|
16
16
|
}
|
|
@@ -21,9 +21,11 @@ export class DirectAdmissionClientFactory extends AdmissionClientFactory {
|
|
|
21
21
|
});
|
|
22
22
|
return JSON.parse(JSON.stringify(evaluated));
|
|
23
23
|
});
|
|
24
|
+
const identityPolicy = factoryArgs.find((arg) => Boolean(arg && typeof arg === 'object' && 'identityPolicy' in arg))?.identityPolicy;
|
|
24
25
|
return new DirectAdmissionClient({
|
|
25
26
|
connectionGrants: evaluatedGrants,
|
|
26
27
|
ttlSec: normalized.ttlSec ?? null,
|
|
28
|
+
nodeIdentityPolicy: identityPolicy,
|
|
27
29
|
});
|
|
28
30
|
}
|
|
29
31
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { createFameEnvelope,
|
|
1
|
+
import { createFameEnvelope, } from '@naylence/core';
|
|
2
2
|
import { DEFAULT_DIRECT_ADMISSION_TTL_SEC, TTL_NEVER_EXPIRES, } from '../../constants/ttl-constants.js';
|
|
3
3
|
import { getLogger } from '../../util/logging.js';
|
|
4
4
|
import { validateTtlSec } from '../../util/ttl-validation.js';
|
|
5
|
+
import { GrantMaterializer } from '../../grants/grant-materializer.js';
|
|
5
6
|
const logger = getLogger('naylence.fame.node.admission.direct_admission_client');
|
|
6
7
|
export class DirectAdmissionClient {
|
|
7
8
|
constructor(options) {
|
|
@@ -25,6 +26,7 @@ export class DirectAdmissionClient {
|
|
|
25
26
|
else {
|
|
26
27
|
this.ttlSec = ttlCandidate;
|
|
27
28
|
}
|
|
29
|
+
this.nodeIdentityPolicy = options.nodeIdentityPolicy;
|
|
28
30
|
}
|
|
29
31
|
async hello(systemId, instanceId, requestedLogicals) {
|
|
30
32
|
logger.debug('direct_admission_hello_start', {
|
|
@@ -32,26 +34,27 @@ export class DirectAdmissionClient {
|
|
|
32
34
|
instanceId,
|
|
33
35
|
requestedLogicals,
|
|
34
36
|
});
|
|
35
|
-
const
|
|
36
|
-
? systemId
|
|
37
|
-
: await generateIdAsync({ mode: 'fingerprint' }).catch(async () => {
|
|
38
|
-
logger.debug('direct_admission_fingerprint_generation_failed', {
|
|
39
|
-
reason: 'falling back to random id',
|
|
40
|
-
});
|
|
41
|
-
return generateIdAsync({ mode: 'random' });
|
|
42
|
-
});
|
|
37
|
+
const initialSystemId = systemId;
|
|
43
38
|
const acceptedLogicals = requestedLogicals && requestedLogicals.length > 0
|
|
44
39
|
? [...requestedLogicals]
|
|
45
40
|
: ['*'];
|
|
46
41
|
const now = Date.now();
|
|
47
42
|
const ttlSeconds = this.resolveTtlSeconds();
|
|
48
43
|
const expiresAt = new Date(now + ttlSeconds * 1000);
|
|
44
|
+
const materializedGrants = await Promise.all(this.connectionGrants.map((grant) => GrantMaterializer.materialize(grant)));
|
|
45
|
+
const effectiveSystemId = this.nodeIdentityPolicy
|
|
46
|
+
? await this.nodeIdentityPolicy.resolveAdmissionNodeId({
|
|
47
|
+
currentNodeId: initialSystemId,
|
|
48
|
+
identities: [],
|
|
49
|
+
grants: materializedGrants,
|
|
50
|
+
})
|
|
51
|
+
: initialSystemId;
|
|
49
52
|
const welcomeFrame = {
|
|
50
53
|
type: 'NodeWelcome',
|
|
51
54
|
systemId: effectiveSystemId,
|
|
52
55
|
instanceId,
|
|
53
56
|
acceptedLogicals,
|
|
54
|
-
connectionGrants:
|
|
57
|
+
connectionGrants: materializedGrants.map((grant) => cloneGrant(grant)),
|
|
55
58
|
expiresAt: expiresAt.toISOString(),
|
|
56
59
|
};
|
|
57
60
|
const envelope = createFameEnvelope({
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DefaultNodeIdentityPolicy } from './default-node-identity-policy.js';
|
|
2
|
+
import { NODE_IDENTITY_POLICY_FACTORY_BASE_TYPE, NodeIdentityPolicyFactory, } from './node-identity-policy-factory.js';
|
|
3
|
+
export const FACTORY_META = {
|
|
4
|
+
base: NODE_IDENTITY_POLICY_FACTORY_BASE_TYPE,
|
|
5
|
+
key: 'DefaultNodeIdentityPolicy',
|
|
6
|
+
};
|
|
7
|
+
export class DefaultNodeIdentityPolicyFactory extends NodeIdentityPolicyFactory {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.type = 'DefaultNodeIdentityPolicy';
|
|
11
|
+
this.isDefault = true;
|
|
12
|
+
}
|
|
13
|
+
async create(_config) {
|
|
14
|
+
return new DefaultNodeIdentityPolicy();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export default DefaultNodeIdentityPolicyFactory;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { generateIdAsync } from '@naylence/core';
|
|
2
|
+
import { TokenProviderFactory } from '../security/auth/token-provider-factory.js';
|
|
3
|
+
import { isIdentityExposingTokenProvider } from '../security/auth/token-provider.js';
|
|
4
|
+
import { getLogger } from '../util/logging.js';
|
|
5
|
+
const logger = getLogger('naylence.fame.node.default_node_identity_policy');
|
|
6
|
+
export class DefaultNodeIdentityPolicy {
|
|
7
|
+
async resolveInitialNodeId(context) {
|
|
8
|
+
if (context.configuredId) {
|
|
9
|
+
return context.configuredId;
|
|
10
|
+
}
|
|
11
|
+
if (context.persistedId) {
|
|
12
|
+
return context.persistedId;
|
|
13
|
+
}
|
|
14
|
+
return await generateIdAsync({ mode: 'fingerprint' });
|
|
15
|
+
}
|
|
16
|
+
async resolveAdmissionNodeId(context) {
|
|
17
|
+
// Try to extract identity from grants first
|
|
18
|
+
if (context.grants && context.grants.length > 0) {
|
|
19
|
+
for (const grant of context.grants) {
|
|
20
|
+
try {
|
|
21
|
+
const auth = grant.auth;
|
|
22
|
+
if (!auth) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const tokenProviderConfig = (auth.tokenProvider ??
|
|
26
|
+
auth.token_provider);
|
|
27
|
+
if (!tokenProviderConfig ||
|
|
28
|
+
typeof tokenProviderConfig.type !== 'string') {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const provider = await TokenProviderFactory.createTokenProvider(tokenProviderConfig);
|
|
32
|
+
if (isIdentityExposingTokenProvider(provider)) {
|
|
33
|
+
const identity = await provider.getIdentity();
|
|
34
|
+
if (identity && identity.subject) {
|
|
35
|
+
logger.debug('identity_extracted_from_grant', {
|
|
36
|
+
identity_id: identity.subject,
|
|
37
|
+
grant_type: grant.type,
|
|
38
|
+
});
|
|
39
|
+
return identity.subject;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
logger.warning('identity_extraction_failed', {
|
|
45
|
+
error: error instanceof Error ? error.message : String(error),
|
|
46
|
+
grant_type: grant.type,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!context.currentNodeId) {
|
|
52
|
+
return await generateIdAsync({ mode: 'fingerprint' });
|
|
53
|
+
}
|
|
54
|
+
return context.currentNodeId;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { generateIdAsync } from '@naylence/core';
|
|
2
1
|
import { createResource } from '@naylence/factory';
|
|
3
2
|
import { AdmissionClientFactory } from './admission/admission-client-factory.js';
|
|
3
|
+
import { DefaultNodeIdentityPolicy } from './default-node-identity-policy.js';
|
|
4
|
+
import { NodeIdentityPolicyFactory, } from './node-identity-policy-factory.js';
|
|
4
5
|
import { DefaultNodeAttachClient } from './admission/default-node-attach-client.js';
|
|
5
6
|
import { TransportListenerFactory } from '../connector/transport-listener-factory.js';
|
|
6
7
|
import { StorageProviderFactory } from '../storage/storage-provider-factory.js';
|
|
@@ -122,6 +123,7 @@ export async function makeCommonOptions(config, rawConfig) {
|
|
|
122
123
|
const deliveryConfig = pickOption(config.delivery ?? null, aliasRecord, 'delivery_policy');
|
|
123
124
|
const telemetryConfig = pickOption(config.telemetry ?? null, aliasRecord, 'trace_emitter', 'telemetry_config');
|
|
124
125
|
const securityConfig = pickOption(config.security ?? null, aliasRecord, 'security_manager', 'security_profile');
|
|
126
|
+
const identityPolicyConfig = pickOption(config.identityPolicy ?? null, aliasRecord, 'identity_policy', 'node_identity_policy');
|
|
125
127
|
const publicUrl = pickString(config.publicUrl ?? null, aliasRecord, 'public_url') ?? null;
|
|
126
128
|
const directParentUrl = pickString(config.directParentUrl ?? null, aliasRecord, 'direct_parent_url') ?? null;
|
|
127
129
|
const hasParentFlag = config.hasParent || Boolean(aliasRecord.has_parent ?? false);
|
|
@@ -130,7 +132,8 @@ export async function makeCommonOptions(config, rawConfig) {
|
|
|
130
132
|
const storageProvider = await resolveStorageProvider(storageConfig ?? null, expressionOptions);
|
|
131
133
|
const nodeMetaStore = await storageProvider.getKeyValueStore(NodeMetaRecord, NODE_META_NAMESPACE);
|
|
132
134
|
const nodeMeta = await nodeMetaStore.get('self');
|
|
133
|
-
const
|
|
135
|
+
const identityPolicy = await resolveNodeIdentityPolicy(identityPolicyConfig ?? null, expressionOptions);
|
|
136
|
+
const admissionClient = await resolveAdmissionClient(admissionConfig ?? null, expressionOptions, identityPolicy ?? undefined);
|
|
134
137
|
const hasParent = determineHasParent(hasParentFlag, directParentUrl, admissionClient);
|
|
135
138
|
const replicaStickinessManager = await resolveReplicaStickinessManager(hasParent, requestedLogicals, expressionOptions);
|
|
136
139
|
const attachmentKeyValidator = await resolveAttachmentKeyValidator(attachmentKeyValidatorConfig ?? null, expressionOptions);
|
|
@@ -162,9 +165,11 @@ export async function makeCommonOptions(config, rawConfig) {
|
|
|
162
165
|
addEventListener(listener, eventListeners);
|
|
163
166
|
}
|
|
164
167
|
const bindingStore = await storageProvider.getKeyValueStore(BindingStoreEntryRecord, BINDING_STORE_NAMESPACE);
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
const effectiveIdentityPolicy = identityPolicy ?? new DefaultNodeIdentityPolicy();
|
|
169
|
+
const systemId = await effectiveIdentityPolicy.resolveInitialNodeId({
|
|
170
|
+
configuredId: systemIdOverride,
|
|
171
|
+
persistedId: nodeMeta?.id,
|
|
172
|
+
});
|
|
168
173
|
const attachClientOptions = {
|
|
169
174
|
...(attachmentKeyValidator ? { attachmentKeyValidator } : {}),
|
|
170
175
|
...(replicaStickinessManager ? { replicaStickinessManager } : {}),
|
|
@@ -195,8 +200,20 @@ export async function makeCommonOptions(config, rawConfig) {
|
|
|
195
200
|
eventListeners,
|
|
196
201
|
transportListeners,
|
|
197
202
|
traceEmitter,
|
|
203
|
+
identityPolicy: identityPolicy ?? undefined,
|
|
198
204
|
};
|
|
199
205
|
}
|
|
206
|
+
async function resolveNodeIdentityPolicy(config, options) {
|
|
207
|
+
try {
|
|
208
|
+
return await NodeIdentityPolicyFactory.createNodeIdentityPolicy(config ?? undefined, cloneCreateOptions(options));
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
logger.warning('node_identity_policy_creation_failed', {
|
|
212
|
+
error: error instanceof Error ? error.message : String(error),
|
|
213
|
+
});
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
200
217
|
async function resolveStorageProvider(config, options) {
|
|
201
218
|
if (config) {
|
|
202
219
|
try {
|
|
@@ -210,12 +227,19 @@ async function resolveStorageProvider(config, options) {
|
|
|
210
227
|
}
|
|
211
228
|
return new InMemoryStorageProvider();
|
|
212
229
|
}
|
|
213
|
-
async function resolveAdmissionClient(config, options) {
|
|
230
|
+
async function resolveAdmissionClient(config, options, identityPolicy) {
|
|
214
231
|
if (config && typeof config.hello === 'function') {
|
|
215
232
|
return config;
|
|
216
233
|
}
|
|
217
234
|
try {
|
|
218
|
-
|
|
235
|
+
const createOptions = cloneCreateOptions(options);
|
|
236
|
+
if (identityPolicy) {
|
|
237
|
+
createOptions.factoryArgs = [
|
|
238
|
+
...(createOptions.factoryArgs ?? []),
|
|
239
|
+
{ identityPolicy },
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
return await AdmissionClientFactory.createAdmissionClient((config ?? null), createOptions);
|
|
219
243
|
}
|
|
220
244
|
catch (error) {
|
|
221
245
|
logger.warning('admission_client_creation_failed', {
|
|
@@ -18,3 +18,10 @@ export * from './fame-environment-context.js';
|
|
|
18
18
|
export * from './session-manager.js';
|
|
19
19
|
export * from './upstream-session-manager.js';
|
|
20
20
|
export * from './root-session-manager.js';
|
|
21
|
+
export * from './node-identity-policy.js';
|
|
22
|
+
export * from './node-identity-policy-factory.js';
|
|
23
|
+
export * from './default-node-identity-policy.js';
|
|
24
|
+
export { DefaultNodeIdentityPolicyFactory, } from './default-node-identity-policy-factory.js';
|
|
25
|
+
export * from './token-subject-node-identity-policy.js';
|
|
26
|
+
export { TokenSubjectNodeIdentityPolicyFactory, } from './token-subject-node-identity-policy-factory.js';
|
|
27
|
+
export { NodeIdentityPolicyProfileFactory, } from './node-identity-policy-profile-factory.js';
|
|
@@ -22,6 +22,7 @@ const FameNodeConfigSchemaInternal = z
|
|
|
22
22
|
attachmentKeyValidator: z.unknown().optional().nullable(),
|
|
23
23
|
telemetry: z.unknown().optional().nullable(),
|
|
24
24
|
requestedCapabilities: z.array(z.string()).optional(),
|
|
25
|
+
identityPolicy: z.unknown().optional().nullable(),
|
|
25
26
|
})
|
|
26
27
|
.passthrough();
|
|
27
28
|
export function normalizeFameNodeConfig(input) {
|
|
@@ -60,6 +61,9 @@ export function normalizeFameNodeConfig(input) {
|
|
|
60
61
|
telemetry: parsed.telemetry === undefined
|
|
61
62
|
? null
|
|
62
63
|
: parsed.telemetry,
|
|
64
|
+
identityPolicy: parsed.identityPolicy === undefined
|
|
65
|
+
? null
|
|
66
|
+
: parsed.identityPolicy,
|
|
63
67
|
};
|
|
64
68
|
if (parsed.requestedCapabilities) {
|
|
65
69
|
normalized.requestedCapabilities = coerceStringArray(parsed.requestedCapabilities);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AbstractResourceFactory, createDefaultResource, createResource, } from '@naylence/factory';
|
|
2
|
+
export const NODE_IDENTITY_POLICY_FACTORY_BASE_TYPE = 'NodeIdentityPolicyFactory';
|
|
3
|
+
export class NodeIdentityPolicyFactory extends AbstractResourceFactory {
|
|
4
|
+
static async createNodeIdentityPolicy(config, options = {}) {
|
|
5
|
+
if (config) {
|
|
6
|
+
const policy = await createResource(NODE_IDENTITY_POLICY_FACTORY_BASE_TYPE, config, options);
|
|
7
|
+
if (!policy) {
|
|
8
|
+
throw new Error('Failed to create node identity policy from configuration');
|
|
9
|
+
}
|
|
10
|
+
return policy;
|
|
11
|
+
}
|
|
12
|
+
const policy = await createDefaultResource(NODE_IDENTITY_POLICY_FACTORY_BASE_TYPE, null, options);
|
|
13
|
+
if (!policy) {
|
|
14
|
+
throw new Error('Failed to create default node identity policy');
|
|
15
|
+
}
|
|
16
|
+
return policy;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { NODE_IDENTITY_POLICY_FACTORY_BASE_TYPE, NodeIdentityPolicyFactory, } from './node-identity-policy-factory.js';
|
|
2
|
+
import { getLogger } from '../util/logging.js';
|
|
3
|
+
const logger = getLogger('naylence.fame.node.node_identity_policy_profile_factory');
|
|
4
|
+
const PROFILE_NAME_DEFAULT = 'default';
|
|
5
|
+
const PROFILE_NAME_TOKEN_SUBJECT = 'token-subject';
|
|
6
|
+
const PROFILE_NAME_TOKEN_SUBJECT_ALIAS = 'token_subject';
|
|
7
|
+
const DEFAULT_PROFILE = {
|
|
8
|
+
type: 'DefaultNodeIdentityPolicy',
|
|
9
|
+
};
|
|
10
|
+
const TOKEN_SUBJECT_PROFILE = {
|
|
11
|
+
type: 'TokenSubjectNodeIdentityPolicy',
|
|
12
|
+
};
|
|
13
|
+
const PROFILE_MAP = {
|
|
14
|
+
[PROFILE_NAME_DEFAULT]: DEFAULT_PROFILE,
|
|
15
|
+
[PROFILE_NAME_TOKEN_SUBJECT]: TOKEN_SUBJECT_PROFILE,
|
|
16
|
+
[PROFILE_NAME_TOKEN_SUBJECT_ALIAS]: TOKEN_SUBJECT_PROFILE,
|
|
17
|
+
};
|
|
18
|
+
export const FACTORY_META = {
|
|
19
|
+
base: NODE_IDENTITY_POLICY_FACTORY_BASE_TYPE,
|
|
20
|
+
key: 'NodeIdentityPolicyProfile',
|
|
21
|
+
};
|
|
22
|
+
export class NodeIdentityPolicyProfileFactory extends NodeIdentityPolicyFactory {
|
|
23
|
+
constructor() {
|
|
24
|
+
super(...arguments);
|
|
25
|
+
this.type = 'NodeIdentityPolicyProfile';
|
|
26
|
+
}
|
|
27
|
+
async create(config) {
|
|
28
|
+
const normalized = normalizeConfig(config);
|
|
29
|
+
const profileConfig = resolveProfileConfig(normalized.profile);
|
|
30
|
+
logger.debug('enabling_node_identity_policy_profile', {
|
|
31
|
+
profile: normalized.profile,
|
|
32
|
+
});
|
|
33
|
+
return NodeIdentityPolicyFactory.createNodeIdentityPolicy(profileConfig);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function normalizeConfig(config) {
|
|
37
|
+
if (!config) {
|
|
38
|
+
return { profile: PROFILE_NAME_DEFAULT };
|
|
39
|
+
}
|
|
40
|
+
const candidate = config;
|
|
41
|
+
const profileValue = typeof candidate.profile === 'string' && candidate.profile.trim().length > 0
|
|
42
|
+
? candidate.profile
|
|
43
|
+
: typeof candidate.profile_name === 'string' &&
|
|
44
|
+
candidate.profile_name.trim().length > 0
|
|
45
|
+
? candidate.profile_name
|
|
46
|
+
: typeof candidate.profileName === 'string' &&
|
|
47
|
+
candidate.profileName.trim().length > 0
|
|
48
|
+
? candidate.profileName
|
|
49
|
+
: PROFILE_NAME_DEFAULT;
|
|
50
|
+
const normalizedProfile = profileValue.trim().toLowerCase();
|
|
51
|
+
return { profile: normalizedProfile };
|
|
52
|
+
}
|
|
53
|
+
function resolveProfileConfig(profileName) {
|
|
54
|
+
const profile = PROFILE_MAP[profileName];
|
|
55
|
+
if (!profile) {
|
|
56
|
+
throw new Error(`Unknown node identity policy profile: ${profileName}`);
|
|
57
|
+
}
|
|
58
|
+
return deepClone(profile);
|
|
59
|
+
}
|
|
60
|
+
function deepClone(value) {
|
|
61
|
+
return JSON.parse(JSON.stringify(value));
|
|
62
|
+
}
|
|
63
|
+
export default NodeIdentityPolicyProfileFactory;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -123,14 +123,15 @@ function sortListeners(listeners) {
|
|
|
123
123
|
export class FameNode extends TaskSpawner {
|
|
124
124
|
constructor(options = {}) {
|
|
125
125
|
super();
|
|
126
|
+
this._confirmedId = null;
|
|
126
127
|
this._sessionManager = null;
|
|
127
128
|
this._upstreamConnector = null;
|
|
128
129
|
this._isStarted = false;
|
|
129
130
|
this._lastHeartbeatAt = null;
|
|
130
131
|
const systemIdOption = resolveStringOption(options, 'systemId', 'system_id');
|
|
131
|
-
this.
|
|
132
|
+
this._provisionalId = systemIdOption ?? generateId();
|
|
132
133
|
const physicalPathOption = resolveStringOption(options, 'physicalPath', 'physical_path');
|
|
133
|
-
this._physicalPath = physicalPathOption ?? `/${this.
|
|
134
|
+
this._physicalPath = physicalPathOption ?? `/${this._provisionalId}`;
|
|
134
135
|
const hasParentOption = resolveBooleanOption(options, 'hasParent', 'has_parent');
|
|
135
136
|
this._hasParent = hasParentOption ?? false;
|
|
136
137
|
const storageProviderOption = resolveOption(options, 'storageProvider', 'storage_provider');
|
|
@@ -184,7 +185,7 @@ export class FameNode extends TaskSpawner {
|
|
|
184
185
|
const bindingStoreOption = resolveOption(options, 'bindingStore', 'binding_store');
|
|
185
186
|
const bindingManagerOptions = {
|
|
186
187
|
hasUpstream: this._hasParent,
|
|
187
|
-
getId: () => this.
|
|
188
|
+
getId: () => this.id,
|
|
188
189
|
getPhysicalPath: () => this._physicalPath,
|
|
189
190
|
getAcceptedLogicals: () => this._acceptedLogicals,
|
|
190
191
|
forwardUpstream: (envelope, context) => this.forwardUpstream(envelope, context),
|
|
@@ -239,7 +240,8 @@ export class FameNode extends TaskSpawner {
|
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
async initializeRootSessionManager() {
|
|
242
|
-
const admissionClient = this._admissionClient ??
|
|
243
|
+
const admissionClient = this._admissionClient ??
|
|
244
|
+
new NoopAdmissionClient({ systemId: this._provisionalId });
|
|
243
245
|
const manager = new RootSessionManager({
|
|
244
246
|
node: this,
|
|
245
247
|
admissionClient,
|
|
@@ -276,9 +278,37 @@ export class FameNode extends TaskSpawner {
|
|
|
276
278
|
await this.deliver(envelope, context);
|
|
277
279
|
return null;
|
|
278
280
|
}
|
|
281
|
+
confirmIdentity(systemId, source) {
|
|
282
|
+
if (this._confirmedId) {
|
|
283
|
+
if (this._confirmedId !== systemId) {
|
|
284
|
+
logger.error('node_identity_mismatch', {
|
|
285
|
+
current_id: this._confirmedId,
|
|
286
|
+
new_id: systemId,
|
|
287
|
+
source,
|
|
288
|
+
});
|
|
289
|
+
throw new Error(`Node identity mismatch in ${source}: expected ${this._confirmedId}, got ${systemId}`);
|
|
290
|
+
}
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const isReassignment = this._provisionalId !== systemId;
|
|
294
|
+
this._confirmedId = systemId;
|
|
295
|
+
if (isReassignment) {
|
|
296
|
+
logger.debug('node_identity_reassigned', {
|
|
297
|
+
system_id: systemId,
|
|
298
|
+
previous_id: this._provisionalId,
|
|
299
|
+
source,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
logger.debug('node_identity_confirmed', {
|
|
304
|
+
system_id: systemId,
|
|
305
|
+
source,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
279
309
|
async handleWelcome(welcome) {
|
|
280
310
|
if (welcome.systemId) {
|
|
281
|
-
this.
|
|
311
|
+
this.confirmIdentity(welcome.systemId, 'handleWelcome');
|
|
282
312
|
}
|
|
283
313
|
if (welcome.acceptedLogicals) {
|
|
284
314
|
this._acceptedLogicals = new Set(welcome.acceptedLogicals);
|
|
@@ -299,7 +329,7 @@ export class FameNode extends TaskSpawner {
|
|
|
299
329
|
await this.dispatchEvent('onWelcome', welcome);
|
|
300
330
|
}
|
|
301
331
|
async handleAttach(info, connector) {
|
|
302
|
-
this.
|
|
332
|
+
this.confirmIdentity(info.systemId, 'handleAttach');
|
|
303
333
|
this._physicalPath =
|
|
304
334
|
info.assignedPath ?? info.targetPhysicalPath ?? this._physicalPath;
|
|
305
335
|
this._upstreamConnector = connector;
|
|
@@ -385,7 +415,13 @@ export class FameNode extends TaskSpawner {
|
|
|
385
415
|
});
|
|
386
416
|
}
|
|
387
417
|
get id() {
|
|
388
|
-
|
|
418
|
+
if (!this._confirmedId) {
|
|
419
|
+
throw new Error('Node ID has not been confirmed yet. Use provisionalId for bootstrapping.');
|
|
420
|
+
}
|
|
421
|
+
return this._confirmedId;
|
|
422
|
+
}
|
|
423
|
+
get provisionalId() {
|
|
424
|
+
return this._provisionalId;
|
|
389
425
|
}
|
|
390
426
|
get sid() {
|
|
391
427
|
return this._sid;
|
|
@@ -904,8 +940,8 @@ export class FameNode extends TaskSpawner {
|
|
|
904
940
|
const store = await this._nodeMetaStorePromise;
|
|
905
941
|
const existing = await store.get('self');
|
|
906
942
|
const record = existing
|
|
907
|
-
? Object.assign(existing, { id: this.
|
|
908
|
-
: new NodeMetaRecord(this.
|
|
943
|
+
? Object.assign(existing, { id: this.id })
|
|
944
|
+
: new NodeMetaRecord(this.id);
|
|
909
945
|
await store.set('self', record);
|
|
910
946
|
}
|
|
911
947
|
catch (error) {
|
|
@@ -170,8 +170,7 @@ export class RootSessionManager extends TaskSpawner {
|
|
|
170
170
|
}
|
|
171
171
|
async performAdmission() {
|
|
172
172
|
this.admissionEpoch += 1;
|
|
173
|
-
this.
|
|
174
|
-
const welcome = await this.admissionClient.hello(this.node.id, generateId(), this.requestedLogicals);
|
|
173
|
+
const welcome = await this.admissionClient.hello(this.node.provisionalId, generateId(), this.requestedLogicals);
|
|
175
174
|
this.currentWelcome = welcome.frame;
|
|
176
175
|
const cryptoProvider = this.node.cryptoProvider; //getCryptoProvider();
|
|
177
176
|
if (welcome.frame.assignedPath && cryptoProvider?.prepareForAttach) {
|
|
@@ -380,15 +379,6 @@ export class RootSessionManager extends TaskSpawner {
|
|
|
380
379
|
seconds_before_expiry: RootSessionManager.JWT_REFRESH_SAFETY,
|
|
381
380
|
});
|
|
382
381
|
}
|
|
383
|
-
initializeRootIdentityIfNeeded() {
|
|
384
|
-
const nodeAny = this.node;
|
|
385
|
-
if (!this.node.id) {
|
|
386
|
-
nodeAny._id = generateId();
|
|
387
|
-
logger.debug('root_identity_generated_id_for_admission', {
|
|
388
|
-
system_id: this.node.id,
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
382
|
async consumeTask(task) {
|
|
393
383
|
try {
|
|
394
384
|
await task.promise;
|
|
@@ -99,6 +99,8 @@ export class RPCClientManager {
|
|
|
99
99
|
this.rpcBound = false;
|
|
100
100
|
this.trackerEventHandler = null;
|
|
101
101
|
this.trackerWithEvents = null;
|
|
102
|
+
this.boundPhysicalPath = null;
|
|
103
|
+
this.rpcRecipient = null;
|
|
102
104
|
this.setupTrackerEventHandler();
|
|
103
105
|
}
|
|
104
106
|
setupTrackerEventHandler() {
|
|
@@ -250,6 +252,8 @@ export class RPCClientManager {
|
|
|
250
252
|
this.rpcBound = false;
|
|
251
253
|
this.rpcReplyAddress = null;
|
|
252
254
|
this.rpcListenerAddress = null;
|
|
255
|
+
this.boundPhysicalPath = null;
|
|
256
|
+
this.rpcRecipient = null;
|
|
253
257
|
for (const [requestId, pending] of Array.from(this.pending.entries())) {
|
|
254
258
|
if (pending.timer) {
|
|
255
259
|
clearTimeout(pending.timer);
|
|
@@ -267,17 +271,20 @@ export class RPCClientManager {
|
|
|
267
271
|
this.pendingByEnvelopeId.clear();
|
|
268
272
|
}
|
|
269
273
|
async ensureReplyListener() {
|
|
270
|
-
|
|
274
|
+
const currentPhysicalPath = this.getPhysicalPath();
|
|
275
|
+
if (this.rpcBound && this.boundPhysicalPath === currentPhysicalPath) {
|
|
271
276
|
return;
|
|
272
277
|
}
|
|
273
|
-
const recipient = `__rpc__${generateId()}`;
|
|
274
|
-
this.
|
|
278
|
+
const recipient = this.rpcRecipient || `__rpc__${generateId()}`;
|
|
279
|
+
this.rpcRecipient = recipient;
|
|
280
|
+
this.rpcReplyAddress = formatAddress(recipient, currentPhysicalPath);
|
|
275
281
|
const handler = async (envelope, _context) => {
|
|
276
282
|
await this.handleReplyEnvelope(envelope);
|
|
277
283
|
return null;
|
|
278
284
|
};
|
|
279
285
|
this.rpcListenerAddress = await this.listenCallback(recipient, handler);
|
|
280
286
|
this.rpcBound = true;
|
|
287
|
+
this.boundPhysicalPath = currentPhysicalPath;
|
|
281
288
|
logger.debug('rpc_reply_listener_bound', {
|
|
282
289
|
reply_recipient: recipient,
|
|
283
290
|
reply_address: this.rpcReplyAddress?.toString(),
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NODE_IDENTITY_POLICY_FACTORY_BASE_TYPE, NodeIdentityPolicyFactory, } from './node-identity-policy-factory.js';
|
|
2
|
+
export const FACTORY_META = {
|
|
3
|
+
base: NODE_IDENTITY_POLICY_FACTORY_BASE_TYPE,
|
|
4
|
+
key: 'TokenSubjectNodeIdentityPolicy',
|
|
5
|
+
};
|
|
6
|
+
export class TokenSubjectNodeIdentityPolicyFactory extends NodeIdentityPolicyFactory {
|
|
7
|
+
constructor() {
|
|
8
|
+
super(...arguments);
|
|
9
|
+
this.type = 'TokenSubjectNodeIdentityPolicy';
|
|
10
|
+
}
|
|
11
|
+
async create(
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
13
|
+
_config) {
|
|
14
|
+
const { TokenSubjectNodeIdentityPolicy } = await import('./token-subject-node-identity-policy.js');
|
|
15
|
+
return new TokenSubjectNodeIdentityPolicy();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export default TokenSubjectNodeIdentityPolicyFactory;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { generateIdAsync } from '@naylence/core';
|
|
2
|
+
import { TokenProviderFactory } from '../security/auth/token-provider-factory.js';
|
|
3
|
+
import { isIdentityExposingTokenProvider } from '../security/auth/token-provider.js';
|
|
4
|
+
import { getLogger } from '../util/logging.js';
|
|
5
|
+
const logger = getLogger('naylence.fame.node.token_subject_node_identity_policy');
|
|
6
|
+
export class TokenSubjectNodeIdentityPolicy {
|
|
7
|
+
async resolveInitialNodeId(context) {
|
|
8
|
+
if (context.configuredId) {
|
|
9
|
+
return context.configuredId;
|
|
10
|
+
}
|
|
11
|
+
if (context.persistedId) {
|
|
12
|
+
return context.persistedId;
|
|
13
|
+
}
|
|
14
|
+
return generateIdAsync();
|
|
15
|
+
}
|
|
16
|
+
async resolveAdmissionNodeId(context) {
|
|
17
|
+
logger.debug('resolve_admission_node_id_start', {
|
|
18
|
+
grantsCount: context.grants?.length ?? 0,
|
|
19
|
+
currentNodeId: context.currentNodeId,
|
|
20
|
+
});
|
|
21
|
+
if (context.grants && context.grants.length > 0) {
|
|
22
|
+
for (const grant of context.grants) {
|
|
23
|
+
try {
|
|
24
|
+
const auth = grant.auth;
|
|
25
|
+
if (!auth) {
|
|
26
|
+
logger.debug('skipping_grant_no_auth', { grantType: grant.type });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const tokenProviderConfig = (auth.tokenProvider ??
|
|
30
|
+
auth.token_provider);
|
|
31
|
+
if (!tokenProviderConfig ||
|
|
32
|
+
typeof tokenProviderConfig.type !== 'string') {
|
|
33
|
+
logger.debug('skipping_grant_invalid_token_provider_config', {
|
|
34
|
+
grantType: grant.type,
|
|
35
|
+
config: tokenProviderConfig,
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
logger.debug('creating_token_provider', {
|
|
40
|
+
type: tokenProviderConfig.type,
|
|
41
|
+
});
|
|
42
|
+
const provider = await TokenProviderFactory.createTokenProvider(tokenProviderConfig);
|
|
43
|
+
const isExposing = isIdentityExposingTokenProvider(provider);
|
|
44
|
+
logger.debug('token_provider_created', {
|
|
45
|
+
type: tokenProviderConfig.type,
|
|
46
|
+
isIdentityExposing: isExposing,
|
|
47
|
+
});
|
|
48
|
+
if (isExposing) {
|
|
49
|
+
const identity = await provider.getIdentity();
|
|
50
|
+
logger.debug('retrieved_identity', { identity });
|
|
51
|
+
if (identity && identity.subject) {
|
|
52
|
+
const hashedSubject = await generateIdAsync({
|
|
53
|
+
mode: 'fingerprint',
|
|
54
|
+
material: identity.subject,
|
|
55
|
+
length: 8,
|
|
56
|
+
});
|
|
57
|
+
const newNodeId = `${hashedSubject}-${context.currentNodeId}`;
|
|
58
|
+
logger.info('resolved_identity_from_token', {
|
|
59
|
+
subject: identity.subject,
|
|
60
|
+
hashedSubject,
|
|
61
|
+
newNodeId,
|
|
62
|
+
});
|
|
63
|
+
return newNodeId;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
logger.debug('identity_missing_subject', { identity });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
logger.warning('failed_to_extract_identity_from_grant', { error: err });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
logger.debug('no_grants_available');
|
|
77
|
+
}
|
|
78
|
+
return context.currentNodeId;
|
|
79
|
+
}
|
|
80
|
+
}
|