@mh-gg/host-runtime 0.1.1-alpha.20260613T085325975Z
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/package.json +23 -0
- package/src/constants.cjs +9 -0
- package/src/errors.cjs +39 -0
- package/src/host/installAppPack.cjs +35 -0
- package/src/host/migrations.cjs +21 -0
- package/src/host/runtimeFromPack.cjs +25 -0
- package/src/host/startRoomHost.cjs +95 -0
- package/src/index.cjs +23 -0
- package/src/memory.cjs +81 -0
- package/src/plugins/definition.cjs +19 -0
- package/src/plugins/install.cjs +90 -0
- package/src/plugins/migrations.cjs +27 -0
- package/src/plugins/operationDescriptors.cjs +138 -0
- package/src/runtime/HostPluginRuntime.cjs +85 -0
- package/src/runtime/authorityReplay/applyAuthority.cjs +146 -0
- package/src/runtime/authorityReplay/applyContent.cjs +49 -0
- package/src/runtime/authorityReplay/index.cjs +70 -0
- package/src/runtime/authorityReplay/state.cjs +56 -0
- package/src/runtime/context.cjs +65 -0
- package/src/runtime/coreOperations.cjs +169 -0
- package/src/runtime/corePayloads.cjs +66 -0
- package/src/runtime/directMessages/commit.cjs +50 -0
- package/src/runtime/directMessages/constants.cjs +20 -0
- package/src/runtime/directMessages/helpers.cjs +59 -0
- package/src/runtime/directMessages/payloads.cjs +74 -0
- package/src/runtime/directMessages/state.cjs +168 -0
- package/src/runtime/directMessages.cjs +6 -0
- package/src/runtime/lifecycle.cjs +166 -0
- package/src/runtime/memberProfiles.cjs +74 -0
- package/src/runtime/methods.cjs +10 -0
- package/src/runtime/operations.cjs +166 -0
- package/src/runtime/queries.cjs +146 -0
- package/src/runtime/readTags.cjs +171 -0
- package/src/runtime/scopedRoleOperations.cjs +97 -0
- package/src/runtime/snowflake.cjs +43 -0
- package/src/security/authority/constants.cjs +10 -0
- package/src/security/authority/resolve/operations.cjs +7 -0
- package/src/security/authority/resolve/policy.cjs +7 -0
- package/src/security/authority/resolve/voids.cjs +8 -0
- package/src/security/authorization/coreGate.cjs +63 -0
- package/src/security/authorization/schemaActions.cjs +75 -0
- package/src/security/roleKeys/authenticator.cjs +36 -0
- package/src/security/roleKeys/authorities/index.cjs +4 -0
- package/src/security/roleKeys/authorities/shapes.cjs +98 -0
- package/src/security/roleKeys/authorities/signing.cjs +121 -0
- package/src/security/roleKeys/constants.cjs +15 -0
- package/src/security/roleKeys/fingerprints.cjs +24 -0
- package/src/security/roleKeys/grants.cjs +93 -0
- package/src/security/roleKeys/index.cjs +10 -0
- package/src/security/roleKeys/roles.cjs +21 -0
- package/src/security/roleKeys/signatures.cjs +126 -0
- package/src/security/roles.cjs +10 -0
- package/src/security/roomDeviceKeys.cjs +41 -0
- package/src/security/scopedRoles/access.cjs +123 -0
- package/src/security/scopedRoles/constants.cjs +23 -0
- package/src/security/scopedRoles/metadata.cjs +39 -0
- package/src/security/scopedRoles/normalize.cjs +179 -0
- package/src/security/scopedRoles/publicView.cjs +31 -0
- package/src/security/scopedRoles/stateOps.cjs +167 -0
- package/src/security/scopedRoles.cjs +7 -0
- package/src/security/standingAuthority.cjs +76 -0
- package/src/shared.cjs +14 -0
- package/src/state.cjs +54 -0
- package/test/authority-ordering-hardening.test.cjs +101 -0
- package/test/authorization-gate.test.cjs +610 -0
- package/test/cascading-authority.test.cjs +390 -0
- package/test/grant-authority-security.test.cjs +305 -0
- package/test/matterhorn-host-runtime.test.cjs +1629 -0
- package/test/operation-descriptor-policy.test.cjs +140 -0
- package/test/role-key-auth.test.cjs +289 -0
- package/test/security-isolation.test.cjs +112 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createSnowflakeGenerator,
|
|
3
|
+
isSnowflakeId,
|
|
4
|
+
prefixedSnowflakeId,
|
|
5
|
+
snowflakeIdForOperation,
|
|
6
|
+
stableSnowflakeNodeId
|
|
7
|
+
} = require("@mh-gg/protocol");
|
|
8
|
+
|
|
9
|
+
function runtimeSnowflakeNodeId(options = {}) {
|
|
10
|
+
if (options.snowflakeNodeId !== undefined) return stableSnowflakeNodeId(options.snowflakeNodeId);
|
|
11
|
+
if (options.nodeId !== undefined) return stableSnowflakeNodeId(options.nodeId);
|
|
12
|
+
if (process.env.MATTERHORN_SNOWFLAKE_NODE_ID) return stableSnowflakeNodeId(process.env.MATTERHORN_SNOWFLAKE_NODE_ID);
|
|
13
|
+
const roomId = options.room?.id || "matterhorn-sdk";
|
|
14
|
+
const appId = options.room?.appPack?.id || "app";
|
|
15
|
+
return stableSnowflakeNodeId(`${appId}:${roomId}:${process.pid || 0}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createRuntimeSnowflakeGenerator(runtime, options = {}) {
|
|
19
|
+
return createSnowflakeGenerator({
|
|
20
|
+
nodeId: runtimeSnowflakeNodeId(options),
|
|
21
|
+
now: () => runtime.now()
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function assignOperationLedgerId(runtime, operation) {
|
|
26
|
+
if (operation?.ledgerId && isSnowflakeId(operation.ledgerId)) return operation;
|
|
27
|
+
const ledgerId = runtime.snowflakes.next(operation?.createdAt || runtime.now());
|
|
28
|
+
return { ...operation, ledgerId, snowflakeId: operation?.snowflakeId && isSnowflakeId(operation.snowflakeId) ? operation.snowflakeId : ledgerId };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function entityIdForOperation(prefix, operation, index = 0) {
|
|
32
|
+
const base = operation?.ledgerId && isSnowflakeId(operation.ledgerId)
|
|
33
|
+
? operation.ledgerId
|
|
34
|
+
: snowflakeIdForOperation(operation || {});
|
|
35
|
+
return `${prefixedSnowflakeId(prefix, base)}${index > 0 ? `_${index}` : ""}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
assignOperationLedgerId,
|
|
40
|
+
createRuntimeSnowflakeGenerator,
|
|
41
|
+
entityIdForOperation,
|
|
42
|
+
runtimeSnowflakeNodeId
|
|
43
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const a = require("@mh-gg/authority");
|
|
2
|
+
module.exports = {
|
|
3
|
+
AUTHORITY_SCOPES: a.AUTHORITY_SCOPES,
|
|
4
|
+
AUTHORITY_STATE_KIND: a.AUTHORITY_STATE_KIND,
|
|
5
|
+
AUTHORITY_STATE_VERSION: a.AUTHORITY_STATE_VERSION,
|
|
6
|
+
CORE_AUTHORITY_GRANT_TYPE: a.CORE_AUTHORITY_GRANT_TYPE,
|
|
7
|
+
CORE_AUTHORITY_REVOKE_TYPE: a.CORE_AUTHORITY_REVOKE_TYPE,
|
|
8
|
+
CORE_PLUGIN_ID: a.CORE_PLUGIN_ID,
|
|
9
|
+
CORE_REVOKE_CREDENTIAL_TYPE: a.CORE_REVOKE_CREDENTIAL_TYPE
|
|
10
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const a = require("@mh-gg/authority");
|
|
2
|
+
module.exports = {
|
|
3
|
+
grantFromOperation: a.grantFromOperation,
|
|
4
|
+
isAuthorityGrantOperation: a.isAuthorityGrantOperation,
|
|
5
|
+
isAuthorityRevokeOperation: a.isAuthorityRevokeOperation,
|
|
6
|
+
revocationFromOperation: a.revocationFromOperation
|
|
7
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const a = require("@mh-gg/authority");
|
|
2
|
+
module.exports = {
|
|
3
|
+
computeVoidedGrantIds: a.computeVoidedGrantIds,
|
|
4
|
+
grantIsAtOrAfterHlc: a.grantIsAtOrAfterHlc,
|
|
5
|
+
grantRecordMap: a.grantRecordMap,
|
|
6
|
+
grantsMatchingRevocation: a.grantsMatchingRevocation,
|
|
7
|
+
recomputeAppliedGrants: a.recomputeAppliedGrants
|
|
8
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const { runtimeError } = require("../../errors.cjs");
|
|
2
|
+
const { actorWithStanding } = require("../standingAuthority.cjs");
|
|
3
|
+
const { minRoleByRank, roleMeetsRequirement } = require("../roles.cjs");
|
|
4
|
+
const { declaredRolesForOperation, operationDescriptor, requireDeclaredRolePolicy } = require("../../plugins/operationDescriptors.cjs");
|
|
5
|
+
const { assertOperationScopeAccess } = require("../scopedRoles.cjs");
|
|
6
|
+
const { schemaActionPolicyForOperation } = require("./schemaActions.cjs");
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async function evaluateStanding(standingAuthority, input) {
|
|
10
|
+
if (!standingAuthority || typeof standingAuthority.evaluate !== "function") {
|
|
11
|
+
throw runtimeError("STANDING_AUTHORITY_MISSING", "Standing authority is unavailable");
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const standing = await standingAuthority.evaluate(input);
|
|
15
|
+
if (!standing || typeof standing !== "object") throw new Error("standing result is invalid");
|
|
16
|
+
return standing;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (error?.code) throw error;
|
|
19
|
+
throw runtimeError("STANDING_AUTHORITY_UNAVAILABLE", `Standing authority unavailable: ${error?.message || error}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function authorizeCoreOperation(runtime, { operation, actor, state, plugin, roles, operationLabel } = {}) {
|
|
24
|
+
const standing = await evaluateStanding(runtime.standingAuthority, {
|
|
25
|
+
memberId: actor?.memberId || operation?.actor?.memberId,
|
|
26
|
+
deviceId: actor?.deviceId || operation?.actor?.deviceId,
|
|
27
|
+
credentialId: actor?.credentialId || operation?.auth?.credentialId,
|
|
28
|
+
actor,
|
|
29
|
+
operation,
|
|
30
|
+
roomState: state,
|
|
31
|
+
now: operation?.createdAt || runtime.now()
|
|
32
|
+
});
|
|
33
|
+
const effectiveActor = actorWithStanding({ ...(operation?.actor || {}), ...actor, credentialId: actor?.credentialId || operation?.auth?.credentialId }, standing);
|
|
34
|
+
if (effectiveActor.standing !== "active") {
|
|
35
|
+
throw runtimeError("ACTOR_NOT_ACTIVE", `Actor ${effectiveActor.memberId || "unknown"} standing is ${effectiveActor.standing}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const schemaPolicy = schemaActionPolicyForOperation(runtime, operation);
|
|
39
|
+
const descriptor = roles ? { authorize: { roles } } : operationDescriptor(plugin, operation.type);
|
|
40
|
+
const schemaRoles = schemaPolicy?.roles;
|
|
41
|
+
const requiredRoles = schemaRoles || roles || requireDeclaredRolePolicy(plugin, operation.type);
|
|
42
|
+
const authorize = schemaRoles ? { ...(descriptor?.authorize || {}), roles: schemaRoles } : (descriptor?.authorize || {});
|
|
43
|
+
const requiredRole = minRoleByRank(requiredRoles);
|
|
44
|
+
if (!requiredRole || !roleMeetsRequirement(effectiveActor.role, requiredRole)) {
|
|
45
|
+
throw runtimeError("FORBIDDEN", `${operationLabel || `${plugin.id}.${operation.type}`} requires ${requiredRole || "a declared role"}; actor role is ${effectiveActor.role}`);
|
|
46
|
+
}
|
|
47
|
+
assertOperationScopeAccess({
|
|
48
|
+
roomState: state,
|
|
49
|
+
actor: effectiveActor,
|
|
50
|
+
operation,
|
|
51
|
+
plugin,
|
|
52
|
+
authorize,
|
|
53
|
+
operationLabel
|
|
54
|
+
});
|
|
55
|
+
return effectiveActor;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
authorizeCoreOperation,
|
|
60
|
+
declaredRolesForOperation,
|
|
61
|
+
operationDescriptor,
|
|
62
|
+
requireDeclaredRolePolicy
|
|
63
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const { runtimeError } = require("../../errors.cjs");
|
|
2
|
+
|
|
3
|
+
const CORE_PLUGIN_ID = "matterhorn.core";
|
|
4
|
+
const INTERNAL_CORE_OPERATIONS = new Set([
|
|
5
|
+
"authority.grant",
|
|
6
|
+
"authority.revoke",
|
|
7
|
+
"credential.revoke",
|
|
8
|
+
"member.key.publish"
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
function compositionForRuntime(runtime) {
|
|
12
|
+
return runtime.hostPack?.composition || runtime.room?.appPack?.composition;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function hasSchemaActions(composition) {
|
|
16
|
+
return Array.isArray(composition?.actions) && composition.actions.length > 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function schemaActionName(operation) {
|
|
20
|
+
if (typeof operation?.schemaAction === "string" && operation.schemaAction.length > 0) return operation.schemaAction;
|
|
21
|
+
if (typeof operation?.action === "string" && operation.action.length > 0) return operation.action;
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isInternalCoreOperation(operation) {
|
|
26
|
+
return operation?.pluginId === CORE_PLUGIN_ID && INTERNAL_CORE_OPERATIONS.has(operation.type);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function primaryPluginId(runtime, composition) {
|
|
30
|
+
return composition?.primaryPlugin?.id
|
|
31
|
+
|| runtime.hostPack?.plugins?.[0]?.id
|
|
32
|
+
|| (runtime.room?.appPack?.id ? `${runtime.room.appPack.id}.plugin` : undefined);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pluginIdForAction(runtime, composition, action) {
|
|
36
|
+
if (action.plugin === "primary" || action.plugin === "$primary") return primaryPluginId(runtime, composition);
|
|
37
|
+
if (action.plugin === "core" || action.plugin === CORE_PLUGIN_ID) return CORE_PLUGIN_ID;
|
|
38
|
+
const ref = (composition.plugins || []).find((plugin) => plugin?.key === action.plugin || plugin?.id === action.plugin);
|
|
39
|
+
return ref?.id || action.plugin;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function rolesForAction(action) {
|
|
43
|
+
if (typeof action.requiredRole === "string" && action.requiredRole.length > 0) return [action.requiredRole];
|
|
44
|
+
const roles = action.payloadSchema?.authorize?.roles;
|
|
45
|
+
return Array.isArray(roles) && roles.length ? roles.slice() : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function schemaActionPolicyForOperation(runtime, operation) {
|
|
49
|
+
if (isInternalCoreOperation(operation)) return undefined;
|
|
50
|
+
const composition = compositionForRuntime(runtime);
|
|
51
|
+
if (!hasSchemaActions(composition)) return undefined;
|
|
52
|
+
|
|
53
|
+
const actionName = schemaActionName(operation);
|
|
54
|
+
if (!actionName) {
|
|
55
|
+
throw runtimeError("SCHEMA_ACTION_REQUIRED", `${operation.pluginId}.${operation.type} must include a schemaAction from the application schema`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const action = composition.actions.find((item) => item?.name === actionName);
|
|
59
|
+
if (!action) throw runtimeError("SCHEMA_ACTION_UNKNOWN", `Application schema action ${actionName} is not declared`);
|
|
60
|
+
|
|
61
|
+
const pluginId = pluginIdForAction(runtime, composition, action);
|
|
62
|
+
if (operation.pluginId !== pluginId || operation.type !== action.type) {
|
|
63
|
+
throw runtimeError("SCHEMA_ACTION_MISMATCH", `Application schema action ${actionName} maps to ${pluginId}.${action.type}, not ${operation.pluginId}.${operation.type}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
action,
|
|
68
|
+
pluginId,
|
|
69
|
+
roles: rolesForAction(action)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
schemaActionPolicyForOperation
|
|
75
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const { runtimeError } = require("../../errors.cjs");
|
|
2
|
+
const { verifyOperationRoleKey } = require("./signatures.cjs");
|
|
3
|
+
|
|
4
|
+
function createRoleKeyAuthenticator(options = {}) {
|
|
5
|
+
const grants = options.grants || options.operationRoleKeyGrants || options.operationKeyGrants;
|
|
6
|
+
const expected = {
|
|
7
|
+
roomId: options.roomId,
|
|
8
|
+
appPackId: options.appPackId,
|
|
9
|
+
appPackHash: options.appPackHash
|
|
10
|
+
};
|
|
11
|
+
const now = options.now;
|
|
12
|
+
const grantAuthorities = options.grantAuthorities || options.operationGrantAuthorities || options.authorities;
|
|
13
|
+
const allowUnsigned = options.allowUnsignedOperationRoleKeyGrants === true || options.devUnsafeUnsignedOperationRoleKeyGrants === true;
|
|
14
|
+
const productionGrantMode = (options.productionRuntime === true || options.relayExecuted === true) && !allowUnsigned;
|
|
15
|
+
const requireSignedGrants = options.requireSignedGrants
|
|
16
|
+
|| options.requireSignedOperationRoleKeyGrants
|
|
17
|
+
|| productionGrantMode
|
|
18
|
+
|| Boolean(grantAuthorities && (Array.isArray(grantAuthorities) ? grantAuthorities.length : true));
|
|
19
|
+
const acceptOperationAppPackHash = options.acceptOperationAppPackHash === true;
|
|
20
|
+
const roleKeyAuthenticator = async function roleKeyAuthenticator(auth, actor, operation = {}) {
|
|
21
|
+
const expectedForOperation = {
|
|
22
|
+
...expected,
|
|
23
|
+
appPackHash: acceptOperationAppPackHash && operation.roomId === expected.roomId && operation.appPackId === expected.appPackId && typeof operation.appPackHash === "string"
|
|
24
|
+
? operation.appPackHash
|
|
25
|
+
: expected.appPackHash
|
|
26
|
+
};
|
|
27
|
+
const verification = verifyOperationRoleKey({ ...operation, actor, auth }, grants, { ...expectedForOperation, now, grantAuthorities, requireSignedGrants });
|
|
28
|
+
if (!verification.ok) throw runtimeError("INVALID_OPERATION_ROLE_KEY", verification.error);
|
|
29
|
+
return verification.actor;
|
|
30
|
+
};
|
|
31
|
+
roleKeyAuthenticator.matterhornRoleKeyAuthenticator = true;
|
|
32
|
+
roleKeyAuthenticator.requireSignedGrants = requireSignedGrants === true;
|
|
33
|
+
return roleKeyAuthenticator;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { createRoleKeyAuthenticator };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
|
+
const { clone } = require("../../../shared.cjs");
|
|
3
|
+
const { OPERATION_GRANT_AUTHORITY_KIND, OPERATION_ROLE_KEY_VERSION, ROLE_ALG } = require("../constants.cjs");
|
|
4
|
+
const { ensureText } = require("../grants.cjs");
|
|
5
|
+
const { normalizeOperationKeyRole } = require("../roles.cjs");
|
|
6
|
+
|
|
7
|
+
function authorityFingerprint(publicKeyPem) {
|
|
8
|
+
if (typeof publicKeyPem !== "string" || publicKeyPem.length === 0) throw new Error("Operation grant authority public key is required.");
|
|
9
|
+
return `sha256-${crypto.createHash("sha256").update(publicKeyPem).digest("hex")}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function generateOperationGrantAuthorityKeyPair(options = {}) {
|
|
13
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
14
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
|
|
15
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
|
|
16
|
+
return {
|
|
17
|
+
issuer: options.issuer || "matterhorn-grant-authority",
|
|
18
|
+
publicKeyPem,
|
|
19
|
+
privateKeyPem,
|
|
20
|
+
publicKeyFingerprint: authorityFingerprint(publicKeyPem)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function optionalStringArray(value, label) {
|
|
25
|
+
if (value === undefined) return undefined;
|
|
26
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) throw new Error(`${label} must be an array of non-empty strings`);
|
|
27
|
+
return [...new Set(value)];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createOperationGrantAuthority(input = {}) {
|
|
31
|
+
const publicKeyPem = ensureText(input.publicKeyPem, "publicKeyPem");
|
|
32
|
+
const authority = {
|
|
33
|
+
kind: OPERATION_GRANT_AUTHORITY_KIND,
|
|
34
|
+
version: OPERATION_ROLE_KEY_VERSION,
|
|
35
|
+
alg: ROLE_ALG,
|
|
36
|
+
issuer: ensureText(input.issuer, "issuer"),
|
|
37
|
+
publicKeyPem,
|
|
38
|
+
publicKeyFingerprint: authorityFingerprint(publicKeyPem),
|
|
39
|
+
issuedAt: Number.isFinite(input.issuedAt) ? input.issuedAt : Date.now(),
|
|
40
|
+
...(typeof input.roomId === "string" && input.roomId.length > 0 ? { roomId: input.roomId } : {}),
|
|
41
|
+
...(typeof input.appPackId === "string" && input.appPackId.length > 0 ? { appPackId: input.appPackId } : {}),
|
|
42
|
+
...(typeof input.appPackHash === "string" && input.appPackHash.length > 0 ? { appPackHash: input.appPackHash } : {}),
|
|
43
|
+
...(Number.isFinite(input.expiresAt) ? { expiresAt: input.expiresAt } : {})
|
|
44
|
+
};
|
|
45
|
+
const allowedGrantRoles = optionalStringArray(input.allowedGrantRoles, "allowedGrantRoles")?.map(normalizeOperationKeyRole);
|
|
46
|
+
if (allowedGrantRoles) authority.allowedGrantRoles = allowedGrantRoles;
|
|
47
|
+
return authority;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isOperationGrantAuthority(value, expected = {}) {
|
|
51
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
52
|
+
if (value.kind !== OPERATION_GRANT_AUTHORITY_KIND || value.version !== OPERATION_ROLE_KEY_VERSION) return false;
|
|
53
|
+
if (value.alg !== ROLE_ALG) return false;
|
|
54
|
+
if (typeof value.issuer !== "string" || value.issuer.length === 0) return false;
|
|
55
|
+
if (typeof value.publicKeyPem !== "string" || value.publicKeyPem.length === 0) return false;
|
|
56
|
+
if (value.publicKeyFingerprint !== authorityFingerprint(value.publicKeyPem)) return false;
|
|
57
|
+
if (!Number.isFinite(value.issuedAt)) return false;
|
|
58
|
+
if (value.expiresAt !== undefined && !Number.isFinite(value.expiresAt)) return false;
|
|
59
|
+
if (value.roomId !== undefined && (typeof value.roomId !== "string" || value.roomId.length === 0)) return false;
|
|
60
|
+
if (expected.roomId && value.roomId && value.roomId !== expected.roomId) return false;
|
|
61
|
+
if (value.appPackId !== undefined && (typeof value.appPackId !== "string" || value.appPackId.length === 0)) return false;
|
|
62
|
+
if (expected.appPackId && value.appPackId && value.appPackId !== expected.appPackId) return false;
|
|
63
|
+
if (value.appPackHash !== undefined && (typeof value.appPackHash !== "string" || value.appPackHash.length === 0)) return false;
|
|
64
|
+
if (expected.appPackHash && value.appPackHash && value.appPackHash !== expected.appPackHash) return false;
|
|
65
|
+
if (value.allowedGrantRoles !== undefined && (!Array.isArray(value.allowedGrantRoles) || value.allowedGrantRoles.some((role) => role !== "admin" && role !== "user"))) return false;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function authorityArray(authorities) {
|
|
70
|
+
if (!authorities) return [];
|
|
71
|
+
if (Array.isArray(authorities)) return authorities;
|
|
72
|
+
if (authorities instanceof Map) return [...authorities.values()];
|
|
73
|
+
if (authorities.kind === OPERATION_GRANT_AUTHORITY_KIND) return [authorities];
|
|
74
|
+
return Object.values(authorities);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function publicOperationGrantAuthority(authority) {
|
|
78
|
+
const copy = clone(authority);
|
|
79
|
+
delete copy.privateKeyPem;
|
|
80
|
+
delete copy.privateKey;
|
|
81
|
+
delete copy.secret;
|
|
82
|
+
return copy;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function publicOperationGrantAuthorities(authorities) {
|
|
86
|
+
return authorityArray(authorities).map(publicOperationGrantAuthority);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = {
|
|
90
|
+
authorityArray,
|
|
91
|
+
authorityFingerprint,
|
|
92
|
+
createOperationGrantAuthority,
|
|
93
|
+
generateOperationGrantAuthorityKeyPair,
|
|
94
|
+
isOperationGrantAuthority,
|
|
95
|
+
optionalStringArray,
|
|
96
|
+
publicOperationGrantAuthorities,
|
|
97
|
+
publicOperationGrantAuthority
|
|
98
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
|
+
const { clone } = require("../../../shared.cjs");
|
|
3
|
+
const { OPERATION_ROLE_KEY_GRANT_SIGNATURE_KIND, OPERATION_ROLE_KEY_VERSION, ROLE_ALG } = require("../constants.cjs");
|
|
4
|
+
const { canonicalJson } = require("@mh-gg/event/canonicalJson");
|
|
5
|
+
const { ensureText, isOperationRoleKeyGrant } = require("../grants.cjs");
|
|
6
|
+
const { normalizeOperationKeyRole } = require("../roles.cjs");
|
|
7
|
+
const {
|
|
8
|
+
authorityArray,
|
|
9
|
+
isOperationGrantAuthority,
|
|
10
|
+
publicOperationGrantAuthority
|
|
11
|
+
} = require("./shapes.cjs");
|
|
12
|
+
|
|
13
|
+
function unsignedOperationRoleKeyGrant(grant) {
|
|
14
|
+
const next = clone(grant);
|
|
15
|
+
delete next.grantSignature;
|
|
16
|
+
delete next.privateKeyPem;
|
|
17
|
+
delete next.privateKey;
|
|
18
|
+
delete next.secret;
|
|
19
|
+
return next;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function unsignedGrantSignaturePayload(grant, signatureMeta) {
|
|
23
|
+
const meta = clone(signatureMeta);
|
|
24
|
+
delete meta.signature;
|
|
25
|
+
return {
|
|
26
|
+
grant: unsignedOperationRoleKeyGrant(grant),
|
|
27
|
+
grantSignature: meta
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function findAuthorityForSignature(signature, authorities, expected = {}) {
|
|
32
|
+
return authorityArray(authorities).find((authority) => {
|
|
33
|
+
if (!isOperationGrantAuthority(authority, expected)) return false;
|
|
34
|
+
if (signature.issuer !== authority.issuer) return false;
|
|
35
|
+
if (signature.authorityFingerprint !== authority.publicKeyFingerprint) return false;
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function validateGrantSignatureShape(signature) {
|
|
41
|
+
if (!signature || typeof signature !== "object" || Array.isArray(signature)) return "Operation role-key grant signature is required.";
|
|
42
|
+
if (signature.kind !== OPERATION_ROLE_KEY_GRANT_SIGNATURE_KIND) return "Operation role-key grant signature kind is invalid.";
|
|
43
|
+
if (signature.version !== OPERATION_ROLE_KEY_VERSION) return "Operation role-key grant signature version is invalid.";
|
|
44
|
+
if (signature.alg !== ROLE_ALG) return "Operation role-key grant signature algorithm is invalid.";
|
|
45
|
+
if (typeof signature.issuer !== "string" || signature.issuer.length === 0) return "Operation role-key grant signature issuer is invalid.";
|
|
46
|
+
if (typeof signature.authorityFingerprint !== "string" || signature.authorityFingerprint.length === 0) return "Operation role-key grant signature authority fingerprint is invalid.";
|
|
47
|
+
if (!Number.isFinite(signature.signedAt)) return "Operation role-key grant signature time is invalid.";
|
|
48
|
+
if (signature.expiresAt !== undefined && !Number.isFinite(signature.expiresAt)) return "Operation role-key grant signature expiry is invalid.";
|
|
49
|
+
if (typeof signature.signature !== "string" || signature.signature.length === 0) return "Operation role-key grant signature is invalid.";
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function signOperationRoleKeyGrant(grant, options = {}) {
|
|
54
|
+
if (!isOperationRoleKeyGrant(grant)) throw new Error("A valid operation role-key grant is required");
|
|
55
|
+
const authority = options.authority;
|
|
56
|
+
if (!isOperationGrantAuthority(authority)) throw new Error("A valid operation grant authority is required");
|
|
57
|
+
const privateKeyPem = ensureText(options.privateKeyPem || options.privateKey, "privateKeyPem");
|
|
58
|
+
const role = normalizeOperationKeyRole(grant.role);
|
|
59
|
+
if (authority.allowedGrantRoles && !authority.allowedGrantRoles.includes(role)) throw new Error("Operation grant authority is not allowed to sign this role");
|
|
60
|
+
if (authority.roomId && authority.roomId !== grant.roomId) throw new Error("Operation grant authority is for a different room");
|
|
61
|
+
if (authority.appPackId && authority.appPackId !== grant.appPackId) throw new Error("Operation grant authority is for a different app pack");
|
|
62
|
+
if (authority.appPackHash && grant.appPackHash && authority.appPackHash !== grant.appPackHash) throw new Error("Operation grant authority is for a different app pack hash");
|
|
63
|
+
const signedAt = Number.isFinite(options.signedAt) ? options.signedAt : Date.now();
|
|
64
|
+
const signatureMeta = {
|
|
65
|
+
kind: OPERATION_ROLE_KEY_GRANT_SIGNATURE_KIND,
|
|
66
|
+
version: OPERATION_ROLE_KEY_VERSION,
|
|
67
|
+
alg: ROLE_ALG,
|
|
68
|
+
issuer: authority.issuer,
|
|
69
|
+
authorityFingerprint: authority.publicKeyFingerprint,
|
|
70
|
+
signedAt,
|
|
71
|
+
...(Number.isFinite(options.expiresAt) ? { expiresAt: options.expiresAt } : {})
|
|
72
|
+
};
|
|
73
|
+
const payload = Buffer.from(canonicalJson(unsignedGrantSignaturePayload(grant, signatureMeta)));
|
|
74
|
+
return {
|
|
75
|
+
...unsignedOperationRoleKeyGrant(grant),
|
|
76
|
+
grantSignature: {
|
|
77
|
+
...signatureMeta,
|
|
78
|
+
signature: crypto.sign(null, payload, privateKeyPem).toString("base64url")
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createSignedOperationRoleKeyGrant(input = {}, options = {}) {
|
|
84
|
+
const { createOperationRoleKeyGrant } = require("../grants.cjs");
|
|
85
|
+
return signOperationRoleKeyGrant(createOperationRoleKeyGrant(input), options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function verifyOperationRoleKeyGrant(grant, authorities, options = {}) {
|
|
89
|
+
if (!isOperationRoleKeyGrant(grant, options)) return { ok: false, error: "Operation role-key grant is invalid." };
|
|
90
|
+
const requireSigned = options.requireSignedGrants === true || options.requireSignedGrant === true;
|
|
91
|
+
const signature = grant.grantSignature;
|
|
92
|
+
if (!signature) return requireSigned ? { ok: false, error: "Operation role-key grant is not signed by a trusted authority." } : { ok: true, grant: clone(grant) };
|
|
93
|
+
const shapeError = validateGrantSignatureShape(signature);
|
|
94
|
+
if (shapeError) return { ok: false, error: shapeError };
|
|
95
|
+
const authority = findAuthorityForSignature(signature, authorities, options);
|
|
96
|
+
if (!authority) return { ok: false, error: "Operation role-key grant authority is not trusted." };
|
|
97
|
+
const now = Number.isFinite(options.now) ? options.now : (typeof options.now === "function" ? options.now() : Date.now());
|
|
98
|
+
if (authority.expiresAt !== undefined && now > authority.expiresAt) return { ok: false, error: "Operation grant authority has expired." };
|
|
99
|
+
if (signature.expiresAt !== undefined && now > signature.expiresAt) return { ok: false, error: "Operation role-key grant signature has expired." };
|
|
100
|
+
if (signature.signedAt < authority.issuedAt) return { ok: false, error: "Operation role-key grant signature predates authority." };
|
|
101
|
+
if (signature.signedAt < grant.issuedAt) return { ok: false, error: "Operation role-key grant signature predates grant." };
|
|
102
|
+
const role = normalizeOperationKeyRole(grant.role);
|
|
103
|
+
if (authority.allowedGrantRoles && !authority.allowedGrantRoles.includes(role)) return { ok: false, error: "Operation grant authority is not allowed to sign this role." };
|
|
104
|
+
try {
|
|
105
|
+
const payload = Buffer.from(canonicalJson(unsignedGrantSignaturePayload(grant, signature)));
|
|
106
|
+
const ok = crypto.verify(null, payload, authority.publicKeyPem, Buffer.from(signature.signature, "base64url"));
|
|
107
|
+
if (!ok) return { ok: false, error: "Operation role-key grant signature is invalid." };
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return { ok: false, error: error?.message || "Operation role-key grant signature verification failed." };
|
|
110
|
+
}
|
|
111
|
+
return { ok: true, authority: publicOperationGrantAuthority(authority), grant: clone(grant) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
createSignedOperationRoleKeyGrant,
|
|
116
|
+
findAuthorityForSignature,
|
|
117
|
+
signOperationRoleKeyGrant,
|
|
118
|
+
unsignedOperationRoleKeyGrant,
|
|
119
|
+
validateGrantSignatureShape,
|
|
120
|
+
verifyOperationRoleKeyGrant
|
|
121
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const OPERATION_GRANT_AUTHORITY_KIND = "matterhorn.operation-grant-authority";
|
|
2
|
+
const OPERATION_ROLE_KEY_GRANT_KIND = "matterhorn.operation-role-key-grant";
|
|
3
|
+
const OPERATION_ROLE_KEY_GRANT_SIGNATURE_KIND = "matterhorn.operation-role-key-grant-signature";
|
|
4
|
+
const OPERATION_ROLE_KEY_PROOF_KIND = "matterhorn.operation-role-key-proof";
|
|
5
|
+
const OPERATION_ROLE_KEY_VERSION = 1;
|
|
6
|
+
const ROLE_ALG = "ed25519";
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
OPERATION_GRANT_AUTHORITY_KIND,
|
|
10
|
+
OPERATION_ROLE_KEY_GRANT_KIND,
|
|
11
|
+
OPERATION_ROLE_KEY_GRANT_SIGNATURE_KIND,
|
|
12
|
+
OPERATION_ROLE_KEY_PROOF_KIND,
|
|
13
|
+
OPERATION_ROLE_KEY_VERSION,
|
|
14
|
+
ROLE_ALG
|
|
15
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
|
+
|
|
3
|
+
function roleKeyFingerprint(publicKeyPem) {
|
|
4
|
+
if (typeof publicKeyPem !== "string" || publicKeyPem.length === 0) throw new Error("Operation role public key is required.");
|
|
5
|
+
return `sha256-${crypto.createHash("sha256").update(publicKeyPem).digest("hex")}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function generateOperationRoleKeyPair(options = {}) {
|
|
9
|
+
const { normalizeOperationKeyRole } = require("./roles.cjs");
|
|
10
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
11
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
|
|
12
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
|
|
13
|
+
return {
|
|
14
|
+
role: normalizeOperationKeyRole(options.role || "user"),
|
|
15
|
+
publicKeyPem,
|
|
16
|
+
privateKeyPem,
|
|
17
|
+
publicKeyFingerprint: roleKeyFingerprint(publicKeyPem)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
generateOperationRoleKeyPair,
|
|
23
|
+
roleKeyFingerprint
|
|
24
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const { clone } = require("../../shared.cjs");
|
|
2
|
+
const { OPERATION_ROLE_KEY_GRANT_KIND, OPERATION_ROLE_KEY_VERSION, ROLE_ALG } = require("./constants.cjs");
|
|
3
|
+
const { roleKeyFingerprint } = require("./fingerprints.cjs");
|
|
4
|
+
const { normalizeOperationKeyRole } = require("./roles.cjs");
|
|
5
|
+
|
|
6
|
+
function ensureText(value, label) {
|
|
7
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`${label} is required`);
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function optionalStringArray(value, label) {
|
|
12
|
+
if (value === undefined) return undefined;
|
|
13
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) throw new Error(`${label} must be an array of non-empty strings`);
|
|
14
|
+
return [...new Set(value)];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createOperationRoleKeyGrant(input = {}) {
|
|
18
|
+
const publicKeyPem = ensureText(input.publicKeyPem, "publicKeyPem");
|
|
19
|
+
const role = normalizeOperationKeyRole(ensureText(input.role, "role"));
|
|
20
|
+
if (role !== "owner" && role !== "admin" && role !== "moderator" && role !== "user") throw new Error("role must be owner, admin, moderator, or user");
|
|
21
|
+
const grant = {
|
|
22
|
+
kind: OPERATION_ROLE_KEY_GRANT_KIND,
|
|
23
|
+
version: OPERATION_ROLE_KEY_VERSION,
|
|
24
|
+
alg: ROLE_ALG,
|
|
25
|
+
credentialId: ensureText(input.credentialId, "credentialId"),
|
|
26
|
+
roomId: ensureText(input.roomId, "roomId"),
|
|
27
|
+
appPackId: ensureText(input.appPackId, "appPackId"),
|
|
28
|
+
role,
|
|
29
|
+
publicKeyPem,
|
|
30
|
+
publicKeyFingerprint: roleKeyFingerprint(publicKeyPem),
|
|
31
|
+
issuedAt: Number.isFinite(input.issuedAt) ? input.issuedAt : Date.now(),
|
|
32
|
+
...(typeof input.appPackHash === "string" && input.appPackHash.length > 0 ? { appPackHash: input.appPackHash } : {}),
|
|
33
|
+
...(typeof input.memberId === "string" && input.memberId.length > 0 ? { memberId: input.memberId } : {}),
|
|
34
|
+
...(typeof input.deviceId === "string" && input.deviceId.length > 0 ? { deviceId: input.deviceId } : {}),
|
|
35
|
+
...(Number.isFinite(input.expiresAt) ? { expiresAt: input.expiresAt } : {})
|
|
36
|
+
};
|
|
37
|
+
const allowedPluginIds = optionalStringArray(input.allowedPluginIds, "allowedPluginIds");
|
|
38
|
+
const allowedOperations = optionalStringArray(input.allowedOperations, "allowedOperations");
|
|
39
|
+
if (allowedPluginIds) grant.allowedPluginIds = allowedPluginIds;
|
|
40
|
+
if (allowedOperations) grant.allowedOperations = allowedOperations;
|
|
41
|
+
return grant;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isOperationRoleKeyGrant(value, expected = {}) {
|
|
45
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
46
|
+
if (value.kind !== OPERATION_ROLE_KEY_GRANT_KIND || value.version !== OPERATION_ROLE_KEY_VERSION) return false;
|
|
47
|
+
if (value.alg !== ROLE_ALG) return false;
|
|
48
|
+
if (value.role !== "owner" && value.role !== "admin" && value.role !== "moderator" && value.role !== "user") return false;
|
|
49
|
+
if (typeof value.credentialId !== "string" || value.credentialId.length === 0) return false;
|
|
50
|
+
if (typeof value.roomId !== "string" || value.roomId.length === 0) return false;
|
|
51
|
+
if (expected.roomId && value.roomId !== expected.roomId) return false;
|
|
52
|
+
if (typeof value.appPackId !== "string" || value.appPackId.length === 0) return false;
|
|
53
|
+
if (expected.appPackId && value.appPackId !== expected.appPackId) return false;
|
|
54
|
+
if (expected.appPackHash && value.appPackHash && value.appPackHash !== expected.appPackHash) return false;
|
|
55
|
+
if (typeof value.publicKeyPem !== "string" || value.publicKeyPem.length === 0) return false;
|
|
56
|
+
if (value.publicKeyFingerprint !== roleKeyFingerprint(value.publicKeyPem)) return false;
|
|
57
|
+
if (!Number.isFinite(value.issuedAt)) return false;
|
|
58
|
+
if (value.expiresAt !== undefined && !Number.isFinite(value.expiresAt)) return false;
|
|
59
|
+
if (value.memberId !== undefined && (typeof value.memberId !== "string" || value.memberId.length === 0)) return false;
|
|
60
|
+
if (value.deviceId !== undefined && (typeof value.deviceId !== "string" || value.deviceId.length === 0)) return false;
|
|
61
|
+
if (value.allowedPluginIds !== undefined && !Array.isArray(value.allowedPluginIds)) return false;
|
|
62
|
+
if (value.allowedOperations !== undefined && !Array.isArray(value.allowedOperations)) return false;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function publicOperationRoleKeyGrant(grant) {
|
|
67
|
+
const copy = clone(grant);
|
|
68
|
+
delete copy.privateKeyPem;
|
|
69
|
+
delete copy.privateKey;
|
|
70
|
+
delete copy.secret;
|
|
71
|
+
return copy;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function grantArray(grants) {
|
|
75
|
+
if (!grants) return [];
|
|
76
|
+
if (Array.isArray(grants)) return grants;
|
|
77
|
+
if (grants instanceof Map) return [...grants.values()];
|
|
78
|
+
if (grants.kind === OPERATION_ROLE_KEY_GRANT_KIND) return [grants];
|
|
79
|
+
return Object.values(grants);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function publicOperationRoleKeyGrants(grants) {
|
|
83
|
+
return grantArray(grants).map(publicOperationRoleKeyGrant);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
createOperationRoleKeyGrant,
|
|
88
|
+
ensureText,
|
|
89
|
+
grantArray,
|
|
90
|
+
isOperationRoleKeyGrant,
|
|
91
|
+
publicOperationRoleKeyGrant,
|
|
92
|
+
publicOperationRoleKeyGrants
|
|
93
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
...require("./authenticator.cjs"),
|
|
3
|
+
...require("@mh-gg/event/canonicalJson"),
|
|
4
|
+
...require("./constants.cjs"),
|
|
5
|
+
...require("./authorities/index.cjs"),
|
|
6
|
+
...require("./fingerprints.cjs"),
|
|
7
|
+
...require("./grants.cjs"),
|
|
8
|
+
...require("./roles.cjs"),
|
|
9
|
+
...require("./signatures.cjs")
|
|
10
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function normalizeOperationKeyRole(role) {
|
|
2
|
+
if (role === "owner") return "owner";
|
|
3
|
+
if (role === "admin") return "admin";
|
|
4
|
+
if (role === "moderator") return "moderator";
|
|
5
|
+
if (role === "member" || role === "user") return "user";
|
|
6
|
+
return role;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function operationKeyRoleToActorRole(role) {
|
|
10
|
+
const normalized = normalizeOperationKeyRole(role);
|
|
11
|
+
if (normalized === "owner") return "owner";
|
|
12
|
+
if (normalized === "admin") return "admin";
|
|
13
|
+
if (normalized === "moderator") return "moderator";
|
|
14
|
+
if (normalized === "user") return "member";
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
normalizeOperationKeyRole,
|
|
20
|
+
operationKeyRoleToActorRole
|
|
21
|
+
};
|