@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.
Files changed (94) hide show
  1. package/dist/browser/index.cjs +1479 -926
  2. package/dist/browser/index.mjs +1472 -927
  3. package/dist/cjs/naylence/fame/connector/broadcast-channel-connector.browser.js +1 -1
  4. package/dist/cjs/naylence/fame/factory-manifest.js +6 -0
  5. package/dist/cjs/naylence/fame/grants/grant-materializer.js +59 -0
  6. package/dist/cjs/naylence/fame/node/admission/admission-profile-factory.js +4 -2
  7. package/dist/cjs/naylence/fame/node/admission/direct-admission-client-factory.js +3 -1
  8. package/dist/cjs/naylence/fame/node/admission/direct-admission-client.js +12 -9
  9. package/dist/cjs/naylence/fame/node/default-node-identity-policy-factory.js +21 -0
  10. package/dist/cjs/naylence/fame/node/default-node-identity-policy.js +60 -0
  11. package/dist/cjs/naylence/fame/node/factory-commons.js +31 -7
  12. package/dist/cjs/naylence/fame/node/index.js +11 -1
  13. package/dist/cjs/naylence/fame/node/node-config.js +4 -0
  14. package/dist/cjs/naylence/fame/node/node-identity-policy-factory.js +22 -0
  15. package/dist/cjs/naylence/fame/node/node-identity-policy-profile-factory.js +67 -0
  16. package/dist/cjs/naylence/fame/node/node-identity-policy.js +2 -0
  17. package/dist/cjs/naylence/fame/node/node.js +45 -9
  18. package/dist/cjs/naylence/fame/node/root-session-manager.js +1 -11
  19. package/dist/cjs/naylence/fame/node/rpc-client-manager.js +10 -3
  20. package/dist/cjs/naylence/fame/node/token-subject-node-identity-policy-factory.js +55 -0
  21. package/dist/cjs/naylence/fame/node/token-subject-node-identity-policy.js +84 -0
  22. package/dist/cjs/naylence/fame/node/upstream-session-manager.js +87 -9
  23. package/dist/cjs/naylence/fame/security/auth/auth-identity.js +2 -0
  24. package/dist/cjs/naylence/fame/security/auth/materializable-token-provider.js +9 -0
  25. package/dist/cjs/naylence/fame/security/auth/oauth2-pkce-token-provider.js +9 -0
  26. package/dist/cjs/naylence/fame/security/auth/static-token-provider.js +44 -0
  27. package/dist/cjs/naylence/fame/security/auth/token-provider.js +6 -0
  28. package/dist/cjs/naylence/fame/security/default-security-manager.js +4 -2
  29. package/dist/cjs/naylence/fame/security/index.js +1 -0
  30. package/dist/cjs/naylence/fame/security/keys/default-key-manager.js +1 -1
  31. package/dist/cjs/naylence/fame/util/task-spawner.js +8 -0
  32. package/dist/cjs/version.js +2 -2
  33. package/dist/esm/naylence/fame/connector/broadcast-channel-connector.browser.js +1 -1
  34. package/dist/esm/naylence/fame/factory-manifest.js +6 -0
  35. package/dist/esm/naylence/fame/grants/grant-materializer.js +55 -0
  36. package/dist/esm/naylence/fame/node/admission/admission-profile-factory.js +4 -2
  37. package/dist/esm/naylence/fame/node/admission/direct-admission-client-factory.js +3 -1
  38. package/dist/esm/naylence/fame/node/admission/direct-admission-client.js +13 -10
  39. package/dist/esm/naylence/fame/node/default-node-identity-policy-factory.js +17 -0
  40. package/dist/esm/naylence/fame/node/default-node-identity-policy.js +56 -0
  41. package/dist/esm/naylence/fame/node/factory-commons.js +31 -7
  42. package/dist/esm/naylence/fame/node/index.js +7 -0
  43. package/dist/esm/naylence/fame/node/node-config.js +4 -0
  44. package/dist/esm/naylence/fame/node/node-identity-policy-factory.js +18 -0
  45. package/dist/esm/naylence/fame/node/node-identity-policy-profile-factory.js +63 -0
  46. package/dist/esm/naylence/fame/node/node-identity-policy.js +1 -0
  47. package/dist/esm/naylence/fame/node/node.js +45 -9
  48. package/dist/esm/naylence/fame/node/root-session-manager.js +1 -11
  49. package/dist/esm/naylence/fame/node/rpc-client-manager.js +10 -3
  50. package/dist/esm/naylence/fame/node/token-subject-node-identity-policy-factory.js +18 -0
  51. package/dist/esm/naylence/fame/node/token-subject-node-identity-policy.js +80 -0
  52. package/dist/esm/naylence/fame/node/upstream-session-manager.js +87 -9
  53. package/dist/esm/naylence/fame/security/auth/auth-identity.js +1 -0
  54. package/dist/esm/naylence/fame/security/auth/materializable-token-provider.js +6 -0
  55. package/dist/esm/naylence/fame/security/auth/oauth2-pkce-token-provider.js +9 -0
  56. package/dist/esm/naylence/fame/security/auth/static-token-provider.js +44 -0
  57. package/dist/esm/naylence/fame/security/auth/token-provider.js +5 -0
  58. package/dist/esm/naylence/fame/security/default-security-manager.js +4 -2
  59. package/dist/esm/naylence/fame/security/index.js +1 -0
  60. package/dist/esm/naylence/fame/security/keys/default-key-manager.js +1 -1
  61. package/dist/esm/naylence/fame/util/task-spawner.js +8 -0
  62. package/dist/esm/version.js +2 -2
  63. package/dist/node/index.cjs +1432 -879
  64. package/dist/node/index.mjs +1425 -880
  65. package/dist/node/node.cjs +1560 -1007
  66. package/dist/node/node.mjs +1553 -1008
  67. package/dist/types/naylence/fame/factory-manifest.d.ts +1 -1
  68. package/dist/types/naylence/fame/grants/grant-materializer.d.ts +4 -0
  69. package/dist/types/naylence/fame/node/admission/admission-profile-factory.d.ts +1 -1
  70. package/dist/types/naylence/fame/node/admission/direct-admission-client-factory.d.ts +1 -1
  71. package/dist/types/naylence/fame/node/admission/direct-admission-client.d.ts +3 -0
  72. package/dist/types/naylence/fame/node/default-node-identity-policy-factory.d.ts +15 -0
  73. package/dist/types/naylence/fame/node/default-node-identity-policy.d.ts +5 -0
  74. package/dist/types/naylence/fame/node/factory-commons.d.ts +2 -0
  75. package/dist/types/naylence/fame/node/index.d.ts +7 -0
  76. package/dist/types/naylence/fame/node/node-config.d.ts +2 -0
  77. package/dist/types/naylence/fame/node/node-identity-policy-factory.d.ts +12 -0
  78. package/dist/types/naylence/fame/node/node-identity-policy-profile-factory.d.ts +15 -0
  79. package/dist/types/naylence/fame/node/node-identity-policy.d.ts +26 -0
  80. package/dist/types/naylence/fame/node/node-like.d.ts +3 -1
  81. package/dist/types/naylence/fame/node/node.d.ts +4 -1
  82. package/dist/types/naylence/fame/node/root-session-manager.d.ts +0 -1
  83. package/dist/types/naylence/fame/node/rpc-client-manager.d.ts +2 -0
  84. package/dist/types/naylence/fame/node/token-subject-node-identity-policy-factory.d.ts +14 -0
  85. package/dist/types/naylence/fame/node/token-subject-node-identity-policy.d.ts +5 -0
  86. package/dist/types/naylence/fame/node/upstream-session-manager.d.ts +4 -0
  87. package/dist/types/naylence/fame/security/auth/auth-identity.d.ts +6 -0
  88. package/dist/types/naylence/fame/security/auth/materializable-token-provider.d.ts +12 -0
  89. package/dist/types/naylence/fame/security/auth/oauth2-pkce-token-provider.d.ts +4 -2
  90. package/dist/types/naylence/fame/security/auth/static-token-provider.d.ts +4 -2
  91. package/dist/types/naylence/fame/security/auth/token-provider.d.ts +5 -0
  92. package/dist/types/naylence/fame/security/index.d.ts +1 -0
  93. package/dist/types/version.d.ts +1 -1
  94. 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, generateIdAsync, } from '@naylence/core';
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 effectiveSystemId = systemId && systemId.trim().length > 0
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: this.connectionGrants.map((grant) => cloneGrant(grant)),
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 admissionClient = await resolveAdmissionClient(admissionConfig ?? null, expressionOptions);
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 systemId = systemIdOverride ??
166
- nodeMeta?.id ??
167
- (await generateIdAsync({ mode: 'fingerprint' }));
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
- return await AdmissionClientFactory.createAdmissionClient((config ?? null), cloneCreateOptions(options));
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;
@@ -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._id = systemIdOption ?? generateId();
132
+ this._provisionalId = systemIdOption ?? generateId();
132
133
  const physicalPathOption = resolveStringOption(options, 'physicalPath', 'physical_path');
133
- this._physicalPath = physicalPathOption ?? `/${this._id}`;
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._id,
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 ?? new NoopAdmissionClient({ systemId: this._id });
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._id = welcome.systemId;
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._id = info.systemId;
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
- return this._id;
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._id })
908
- : new NodeMetaRecord(this._id);
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.initializeRootIdentityIfNeeded();
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
- if (this.rpcBound) {
274
+ const currentPhysicalPath = this.getPhysicalPath();
275
+ if (this.rpcBound && this.boundPhysicalPath === currentPhysicalPath) {
271
276
  return;
272
277
  }
273
- const recipient = `__rpc__${generateId()}`;
274
- this.rpcReplyAddress = formatAddress(recipient, this.getPhysicalPath());
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
+ }