@tearleads/client-sdk 0.1.105 → 0.1.106
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/client/organizations/index.js +5 -2
- package/client/organizations/index.js.map +1 -1
- package/client/organizations/organizationMemberships.js +26 -18
- package/client/organizations/organizationMemberships.js.map +1 -1
- package/client/organizations/organizationReadModels.d.ts +9 -4
- package/client/organizations/organizationReadModels.js +20 -9
- package/client/organizations/organizationReadModels.js.map +1 -1
- package/internal/api-client/ApiClient.d.ts +7 -0
- package/internal/crypto/organizationLeaveRequest.d.ts +1 -1
- package/internal/crypto/organizationLeaveRequest.js +9 -2
- package/internal/crypto/organizationLeaveRequest.js.map +1 -1
- package/internal/validators/operation/organizationMemberships.d.ts +7 -0
- package/internal/validators/operation/organizations.d.ts +56 -0
- package/internal/validators/response/organization.d.ts +91 -0
- package/internal/validators/response/organizationReadModel.d.ts +70 -0
- package/internal/validators/util/index.d.ts +1 -1
- package/internal/validators/util/index.js.map +1 -1
- package/internal/validators/util/organizationLeaveRequest.d.ts +25 -2
- package/internal/validators/util/organizationLeaveRequest.js +19 -3
- package/internal/validators/util/organizationLeaveRequest.js.map +1 -1
- package/package.json +1 -1
- package/workflows/organizations/memberships/leaveRequest.d.ts +6 -2
- package/workflows/organizations/memberships/leaveRequest.js +20 -5
- package/workflows/organizations/memberships/leaveRequest.js.map +1 -1
- package/workflows/organizations/memberships/leaveRequestMembersEvidence.js +80 -0
- package/workflows/organizations/memberships/leaveRequestMembersEvidence.js.map +1 -0
- package/workflows/organizations/memberships/leaveRequestMembership.d.ts +24 -0
- package/workflows/organizations/memberships/leaveRequestMembership.js +65 -0
- package/workflows/organizations/memberships/leaveRequestMembership.js.map +1 -0
- package/workflows/organizations/memberships/leaveRequestVerification.d.ts +15 -7
- package/workflows/organizations/memberships/leaveRequestVerification.js +18 -11
- package/workflows/organizations/memberships/leaveRequestVerification.js.map +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ProjectionDependencyUnavailableError } from "../../../data/keyingProjectionVerification/dependencyUnavailable.js";
|
|
2
|
+
import { assertProjectionVerificationCurrent } from "../../../data/keyingProjectionVerification/types.js";
|
|
3
|
+
import { requireOrganizationGroupHead } from "../../../data/principals/organizationAuthorityDescriptor.js";
|
|
4
|
+
import { createRuntimePrincipalPolicyCurrentResolver, } from "../../principals/runtimePolicyRecovery.js";
|
|
5
|
+
import { loadCurrentOrganizationAuthority } from "../currentOrganizationAuthority.js";
|
|
6
|
+
import { loadLocalOrganizationPolicyReference } from "../readModel/localReadModelDetails.js";
|
|
7
|
+
import { createLeaveRequestMembershipCheck, } from "./leaveRequestMembership.js";
|
|
8
|
+
/**
|
|
9
|
+
* The organization's current Members head as the member's own device
|
|
10
|
+
* discovers and verifies it now, for a leave request to cite.
|
|
11
|
+
*/
|
|
12
|
+
export async function loadCurrentMembersHead(input) {
|
|
13
|
+
const resolveCurrentPolicy = createRuntimePrincipalPolicyCurrentResolver(input.runtime);
|
|
14
|
+
if (!resolveCurrentPolicy)
|
|
15
|
+
throw new ProjectionDependencyUnavailableError("Asking to leave requires private paged recovery");
|
|
16
|
+
const authority = await loadCurrentOrganizationAuthority({
|
|
17
|
+
execSql: input.runtime.infra.execSql,
|
|
18
|
+
organizationId: input.organizationId,
|
|
19
|
+
resolveCurrentPolicy,
|
|
20
|
+
stillCurrent: input.stillCurrent,
|
|
21
|
+
});
|
|
22
|
+
assertProjectionVerificationCurrent(authority.stillCurrent);
|
|
23
|
+
const head = requireOrganizationGroupHead(authority.descriptor, authority.descriptor.memberGroupId);
|
|
24
|
+
return {
|
|
25
|
+
keyEpoch: head.keyEpoch,
|
|
26
|
+
keyFingerprint: head.keyFingerprint,
|
|
27
|
+
principalId: head.principalId,
|
|
28
|
+
stateHash: head.stateHash,
|
|
29
|
+
version: head.version,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Checks leave requests against the Members head in the organization policy
|
|
34
|
+
* this device's read model selected, verified exactly through the private
|
|
35
|
+
* paged verifier (from local evidence when it has it), and walks verified
|
|
36
|
+
* Members history the same way when a request cites an earlier key epoch.
|
|
37
|
+
*/
|
|
38
|
+
export function createRuntimeLeaveRequestMembershipCheck(input) {
|
|
39
|
+
const resolve = createRuntimePrincipalPolicyCurrentResolver(input.runtime);
|
|
40
|
+
return createLeaveRequestMembershipCheck({
|
|
41
|
+
async loadMembersHead() {
|
|
42
|
+
if (!resolve)
|
|
43
|
+
return null;
|
|
44
|
+
const organizationReference = await loadLocalOrganizationPolicyReference({
|
|
45
|
+
currentUserId: input.currentUserId,
|
|
46
|
+
execSql: input.runtime.infra.execSql,
|
|
47
|
+
organizationId: input.organizationId,
|
|
48
|
+
principalId: input.organizationId,
|
|
49
|
+
principalType: "organization",
|
|
50
|
+
});
|
|
51
|
+
if (!organizationReference)
|
|
52
|
+
return null;
|
|
53
|
+
const authority = await loadCurrentOrganizationAuthority({
|
|
54
|
+
execSql: input.runtime.infra.execSql,
|
|
55
|
+
organizationId: input.organizationId,
|
|
56
|
+
organizationReference,
|
|
57
|
+
resolveCurrentPolicy: resolve,
|
|
58
|
+
stillCurrent: input.stillCurrent,
|
|
59
|
+
});
|
|
60
|
+
assertProjectionVerificationCurrent(authority.stillCurrent);
|
|
61
|
+
return requireOrganizationGroupHead(authority.descriptor, authority.descriptor.memberGroupId);
|
|
62
|
+
},
|
|
63
|
+
loadHistoryPage: resolve
|
|
64
|
+
? async (head, beforeVersion) => {
|
|
65
|
+
const resolved = await resolve({
|
|
66
|
+
organizationId: input.organizationId,
|
|
67
|
+
reference: head,
|
|
68
|
+
preferLocalCurrent: true,
|
|
69
|
+
stillCurrent: input.stillCurrent,
|
|
70
|
+
historyPage: beforeVersion === undefined ? {} : { beforeVersion },
|
|
71
|
+
});
|
|
72
|
+
assertProjectionVerificationCurrent(resolved.stillCurrent);
|
|
73
|
+
if (!resolved.historyPage)
|
|
74
|
+
throw new Error("Verified Members history page is missing");
|
|
75
|
+
return resolved.historyPage;
|
|
76
|
+
}
|
|
77
|
+
: undefined,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=leaveRequestMembersEvidence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leaveRequestMembersEvidence.js","sourceRoot":"","sources":["leaveRequestMembersEvidence.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oCAAoC,EAAE,MAAM,kEAAkE,CAAC;AACxH,OAAO,EAAE,mCAAmC,EAAE,MAAM,kDAAkD,CAAC;AACvG,OAAO,EAAE,4BAA4B,EAAE,MAAM,0DAA0D,CAAC;AACxG,OAAO,EACL,2CAA2C,GAE5C,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,gCAAgC,EAAE,MAAM,iCAAiC,CAAC;AACnF,OAAO,EAAE,oCAAoC,EAAE,MAAM,oCAAoC,CAAC;AAC1F,OAAO,EACL,iCAAiC,GAElC,MAAM,0BAA0B,CAAC;AAQlC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,KAA2B;IAE3B,MAAM,oBAAoB,GAAG,2CAA2C,CACtE,KAAK,CAAC,OAAO,CACd,CAAC;IACF,IAAI,CAAC,oBAAoB;QACvB,MAAM,IAAI,oCAAoC,CAC5C,iDAAiD,CAClD,CAAC;IACJ,MAAM,SAAS,GAAG,MAAM,gCAAgC,CAAC;QACvD,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO;QACpC,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,oBAAoB;QACpB,YAAY,EAAE,KAAK,CAAC,YAAY;KACjC,CAAC,CAAC;IACH,mCAAmC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;IAC5D,MAAM,IAAI,GAAG,4BAA4B,CACvC,SAAS,CAAC,UAAU,EACpB,SAAS,CAAC,UAAU,CAAC,aAAa,CACnC,CAAC;IACF,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wCAAwC,CACtD,KAAgE;IAEhE,MAAM,OAAO,GAAG,2CAA2C,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC3E,OAAO,iCAAiC,CAAC;QACvC,KAAK,CAAC,eAAe;YACnB,IAAI,CAAC,OAAO;gBAAE,OAAO,IAAI,CAAC;YAC1B,MAAM,qBAAqB,GAAG,MAAM,oCAAoC,CAAC;gBACvE,aAAa,EAAE,KAAK,CAAC,aAAa;gBAClC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO;gBACpC,cAAc,EAAE,KAAK,CAAC,cAAc;gBACpC,WAAW,EAAE,KAAK,CAAC,cAAc;gBACjC,aAAa,EAAE,cAAc;aAC9B,CAAC,CAAC;YACH,IAAI,CAAC,qBAAqB;gBAAE,OAAO,IAAI,CAAC;YACxC,MAAM,SAAS,GAAG,MAAM,gCAAgC,CAAC;gBACvD,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO;gBACpC,cAAc,EAAE,KAAK,CAAC,cAAc;gBACpC,qBAAqB;gBACrB,oBAAoB,EAAE,OAAO;gBAC7B,YAAY,EAAE,KAAK,CAAC,YAAY;aACjC,CAAC,CAAC;YACH,mCAAmC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;YAC5D,OAAO,4BAA4B,CACjC,SAAS,CAAC,UAAU,EACpB,SAAS,CAAC,UAAU,CAAC,aAAa,CACnC,CAAC;QACJ,CAAC;QACD,eAAe,EAAE,OAAO;YACtB,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE;gBAC5B,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC;oBAC7B,cAAc,EAAE,KAAK,CAAC,cAAc;oBACpC,SAAS,EAAE,IAAI;oBACf,kBAAkB,EAAE,IAAI;oBACxB,YAAY,EAAE,KAAK,CAAC,YAAY;oBAChC,WAAW,EAAE,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE;iBAClE,CAAC,CAAC;gBACH,mCAAmC,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;gBAC3D,IAAI,CAAC,QAAQ,CAAC,WAAW;oBACvB,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;gBAC9D,OAAO,QAAQ,CAAC,WAAW,CAAC;YAC9B,CAAC;YACH,CAAC,CAAC,SAAS;KACd,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { OrganizationLeaveRequestMembersHead } from \"@tearleads/validators/util\";\nimport { ProjectionDependencyUnavailableError } from \"../../../data/keyingProjectionVerification/dependencyUnavailable\";\nimport { assertProjectionVerificationCurrent } from \"../../../data/keyingProjectionVerification/types\";\nimport { requireOrganizationGroupHead } from \"../../../data/principals/organizationAuthorityDescriptor\";\nimport {\n createRuntimePrincipalPolicyCurrentResolver,\n type PrincipalPolicyRecoveryRuntime,\n} from \"../../principals/runtimePolicyRecovery\";\nimport { loadCurrentOrganizationAuthority } from \"../currentOrganizationAuthority\";\nimport { loadLocalOrganizationPolicyReference } from \"../readModel/localReadModelDetails\";\nimport {\n createLeaveRequestMembershipCheck,\n type LeaveRequestMembershipCheck,\n} from \"./leaveRequestMembership\";\n\ninterface MembersEvidenceInput {\n readonly runtime: PrincipalPolicyRecoveryRuntime;\n readonly organizationId: string;\n readonly stillCurrent: () => boolean;\n}\n\n/**\n * The organization's current Members head as the member's own device\n * discovers and verifies it now, for a leave request to cite.\n */\nexport async function loadCurrentMembersHead(\n input: MembersEvidenceInput,\n): Promise<OrganizationLeaveRequestMembersHead> {\n const resolveCurrentPolicy = createRuntimePrincipalPolicyCurrentResolver(\n input.runtime,\n );\n if (!resolveCurrentPolicy)\n throw new ProjectionDependencyUnavailableError(\n \"Asking to leave requires private paged recovery\",\n );\n const authority = await loadCurrentOrganizationAuthority({\n execSql: input.runtime.infra.execSql,\n organizationId: input.organizationId,\n resolveCurrentPolicy,\n stillCurrent: input.stillCurrent,\n });\n assertProjectionVerificationCurrent(authority.stillCurrent);\n const head = requireOrganizationGroupHead(\n authority.descriptor,\n authority.descriptor.memberGroupId,\n );\n return {\n keyEpoch: head.keyEpoch,\n keyFingerprint: head.keyFingerprint,\n principalId: head.principalId,\n stateHash: head.stateHash,\n version: head.version,\n };\n}\n\n/**\n * Checks leave requests against the Members head in the organization policy\n * this device's read model selected, verified exactly through the private\n * paged verifier (from local evidence when it has it), and walks verified\n * Members history the same way when a request cites an earlier key epoch.\n */\nexport function createRuntimeLeaveRequestMembershipCheck(\n input: MembersEvidenceInput & { readonly currentUserId: string },\n): LeaveRequestMembershipCheck {\n const resolve = createRuntimePrincipalPolicyCurrentResolver(input.runtime);\n return createLeaveRequestMembershipCheck({\n async loadMembersHead() {\n if (!resolve) return null;\n const organizationReference = await loadLocalOrganizationPolicyReference({\n currentUserId: input.currentUserId,\n execSql: input.runtime.infra.execSql,\n organizationId: input.organizationId,\n principalId: input.organizationId,\n principalType: \"organization\",\n });\n if (!organizationReference) return null;\n const authority = await loadCurrentOrganizationAuthority({\n execSql: input.runtime.infra.execSql,\n organizationId: input.organizationId,\n organizationReference,\n resolveCurrentPolicy: resolve,\n stillCurrent: input.stillCurrent,\n });\n assertProjectionVerificationCurrent(authority.stillCurrent);\n return requireOrganizationGroupHead(\n authority.descriptor,\n authority.descriptor.memberGroupId,\n );\n },\n loadHistoryPage: resolve\n ? async (head, beforeVersion) => {\n const resolved = await resolve({\n organizationId: input.organizationId,\n reference: head,\n preferLocalCurrent: true,\n stillCurrent: input.stillCurrent,\n historyPage: beforeVersion === undefined ? {} : { beforeVersion },\n });\n assertProjectionVerificationCurrent(resolved.stillCurrent);\n if (!resolved.historyPage)\n throw new Error(\"Verified Members history page is missing\");\n return resolved.historyPage;\n }\n : undefined,\n });\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReferencedPrincipalHead } from "../../../internal/crypto/index.js";
|
|
2
|
+
import type { OrganizationLeaveRequest } from "../../../internal/validators/util/index.js";
|
|
3
|
+
import type { RecoveredPrincipalHistoryPage } from "../../principals/historyRecovery/loadRecoveredPrincipalHistoryPage.js";
|
|
4
|
+
export interface LeaveRequestMembershipEvidence {
|
|
5
|
+
/** The Members head this device has verified, or null when it has none. */
|
|
6
|
+
readonly loadMembersHead: () => Promise<ReferencedPrincipalHead | null>;
|
|
7
|
+
/**
|
|
8
|
+
* Verified Members history ending at `head`, one page at a time and in
|
|
9
|
+
* ascending order. Absent where this device cannot read history.
|
|
10
|
+
*/
|
|
11
|
+
readonly loadHistoryPage?: ((head: ReferencedPrincipalHead, beforeVersion: number | undefined) => Promise<RecoveredPrincipalHistoryPage>) | undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Whether a leave request still belongs to its member's current membership. */
|
|
14
|
+
export type LeaveRequestMembershipCheck = (request: OrganizationLeaveRequest) => Promise<boolean>;
|
|
15
|
+
/**
|
|
16
|
+
* Checks each leave request against this device's verified Members head.
|
|
17
|
+
* Removing anyone from Members rotates its key epoch and key, so a request
|
|
18
|
+
* citing the head's own epoch and key has seen no removal since. One citing an earlier epoch
|
|
19
|
+
* stands only if verified history shows its member in every Members state
|
|
20
|
+
* from the cited one on; a request from a membership a removal ended never
|
|
21
|
+
* does, whatever the server serves beside it. Display only: a request this
|
|
22
|
+
* device cannot place is shown as no request.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createLeaveRequestMembershipCheck(evidence: LeaveRequestMembershipEvidence): LeaveRequestMembershipCheck;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// How far back one request's check walks verified Members history: a request
|
|
2
|
+
// stays pending for days, not for hundreds of Members changes.
|
|
3
|
+
const MAX_MEMBERS_HISTORY_PAGES = 8;
|
|
4
|
+
function citesState(request, state) {
|
|
5
|
+
const cited = request.membersHead;
|
|
6
|
+
return (state.version === cited.version &&
|
|
7
|
+
state.stateHash === cited.stateHash &&
|
|
8
|
+
state.keyEpoch === cited.keyEpoch &&
|
|
9
|
+
state.keyFingerprint === cited.keyFingerprint);
|
|
10
|
+
}
|
|
11
|
+
// Walks verified Members history back from the head to the cited state,
|
|
12
|
+
// requiring the member in every state on the way.
|
|
13
|
+
async function memberStayedSinceCitedState(request, head, loadHistoryPage) {
|
|
14
|
+
let expectedVersion = head.version;
|
|
15
|
+
let beforeVersion;
|
|
16
|
+
for (let pages = 0; pages < MAX_MEMBERS_HISTORY_PAGES; pages += 1) {
|
|
17
|
+
const page = await loadHistoryPage(head, beforeVersion);
|
|
18
|
+
for (const entry of [...page.entries].reverse()) {
|
|
19
|
+
const { state } = entry;
|
|
20
|
+
if (state.principalType !== head.principalType ||
|
|
21
|
+
state.principalId !== head.principalId ||
|
|
22
|
+
state.version !== expectedVersion ||
|
|
23
|
+
(state.version === head.version && state.stateHash !== head.stateHash))
|
|
24
|
+
return false;
|
|
25
|
+
if (!entry.projection.some((member) => member.userId === request.userId))
|
|
26
|
+
return false;
|
|
27
|
+
if (state.version === request.membersHead.version)
|
|
28
|
+
return citesState(request, state);
|
|
29
|
+
expectedVersion -= 1;
|
|
30
|
+
}
|
|
31
|
+
if (page.nextBeforeVersion === null || page.entries.length === 0)
|
|
32
|
+
return false;
|
|
33
|
+
beforeVersion = page.nextBeforeVersion;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Checks each leave request against this device's verified Members head.
|
|
39
|
+
* Removing anyone from Members rotates its key epoch and key, so a request
|
|
40
|
+
* citing the head's own epoch and key has seen no removal since. One citing an earlier epoch
|
|
41
|
+
* stands only if verified history shows its member in every Members state
|
|
42
|
+
* from the cited one on; a request from a membership a removal ended never
|
|
43
|
+
* does, whatever the server serves beside it. Display only: a request this
|
|
44
|
+
* device cannot place is shown as no request.
|
|
45
|
+
*/
|
|
46
|
+
export function createLeaveRequestMembershipCheck(evidence) {
|
|
47
|
+
let membersHead;
|
|
48
|
+
return async (request) => {
|
|
49
|
+
membersHead ??= evidence.loadMembersHead().catch(() => null);
|
|
50
|
+
const head = await membersHead;
|
|
51
|
+
const cited = request.membersHead;
|
|
52
|
+
if (head?.principalType !== "group" ||
|
|
53
|
+
cited.principalId !== head.principalId ||
|
|
54
|
+
cited.keyEpoch > head.keyEpoch)
|
|
55
|
+
return false;
|
|
56
|
+
// One signed chain holds one key per epoch, so a matching epoch with a
|
|
57
|
+
// different key can only come from a fork, never from this history.
|
|
58
|
+
if (cited.keyEpoch === head.keyEpoch)
|
|
59
|
+
return cited.keyFingerprint === head.keyFingerprint;
|
|
60
|
+
if (!evidence.loadHistoryPage || cited.version >= head.version)
|
|
61
|
+
return false;
|
|
62
|
+
return memberStayedSinceCitedState(request, head, evidence.loadHistoryPage).catch(() => false);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=leaveRequestMembership.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leaveRequestMembership.js","sourceRoot":"","sources":["leaveRequestMembership.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ReferencedPrincipalHead } from \"@tearleads/crypto\";\nimport type { OrganizationLeaveRequest } from \"@tearleads/validators/util\";\nimport type { RecoveredPrincipalHistoryPage } from \"../../principals/historyRecovery/loadRecoveredPrincipalHistoryPage\";\n\n// How far back one request's check walks verified Members history: a request\n// stays pending for days, not for hundreds of Members changes.\nconst MAX_MEMBERS_HISTORY_PAGES = 8;\n\nexport interface LeaveRequestMembershipEvidence {\n /** The Members head this device has verified, or null when it has none. */\n readonly loadMembersHead: () => Promise<ReferencedPrincipalHead | null>;\n /**\n * Verified Members history ending at `head`, one page at a time and in\n * ascending order. Absent where this device cannot read history.\n */\n readonly loadHistoryPage?:\n | ((\n head: ReferencedPrincipalHead,\n beforeVersion: number | undefined,\n ) => Promise<RecoveredPrincipalHistoryPage>)\n | undefined;\n}\n\n/** Whether a leave request still belongs to its member's current membership. */\nexport type LeaveRequestMembershipCheck = (\n request: OrganizationLeaveRequest,\n) => Promise<boolean>;\n\nfunction citesState(\n request: OrganizationLeaveRequest,\n state: RecoveredPrincipalHistoryPage[\"entries\"][number][\"state\"],\n): boolean {\n const cited = request.membersHead;\n return (\n state.version === cited.version &&\n state.stateHash === cited.stateHash &&\n state.keyEpoch === cited.keyEpoch &&\n state.keyFingerprint === cited.keyFingerprint\n );\n}\n\n// Walks verified Members history back from the head to the cited state,\n// requiring the member in every state on the way.\nasync function memberStayedSinceCitedState(\n request: OrganizationLeaveRequest,\n head: ReferencedPrincipalHead,\n loadHistoryPage: NonNullable<\n LeaveRequestMembershipEvidence[\"loadHistoryPage\"]\n >,\n): Promise<boolean> {\n let expectedVersion = head.version;\n let beforeVersion: number | undefined;\n for (let pages = 0; pages < MAX_MEMBERS_HISTORY_PAGES; pages += 1) {\n const page = await loadHistoryPage(head, beforeVersion);\n for (const entry of [...page.entries].reverse()) {\n const { state } = entry;\n if (\n state.principalType !== head.principalType ||\n state.principalId !== head.principalId ||\n state.version !== expectedVersion ||\n (state.version === head.version && state.stateHash !== head.stateHash)\n )\n return false;\n if (!entry.projection.some((member) => member.userId === request.userId))\n return false;\n if (state.version === request.membersHead.version)\n return citesState(request, state);\n expectedVersion -= 1;\n }\n if (page.nextBeforeVersion === null || page.entries.length === 0)\n return false;\n beforeVersion = page.nextBeforeVersion;\n }\n return false;\n}\n\n/**\n * Checks each leave request against this device's verified Members head.\n * Removing anyone from Members rotates its key epoch and key, so a request\n * citing the head's own epoch and key has seen no removal since. One citing an earlier epoch\n * stands only if verified history shows its member in every Members state\n * from the cited one on; a request from a membership a removal ended never\n * does, whatever the server serves beside it. Display only: a request this\n * device cannot place is shown as no request.\n */\nexport function createLeaveRequestMembershipCheck(\n evidence: LeaveRequestMembershipEvidence,\n): LeaveRequestMembershipCheck {\n let membersHead: Promise<ReferencedPrincipalHead | null> | undefined;\n return async (request) => {\n membersHead ??= evidence.loadMembersHead().catch(() => null);\n const head = await membersHead;\n const cited = request.membersHead;\n if (\n head?.principalType !== \"group\" ||\n cited.principalId !== head.principalId ||\n cited.keyEpoch > head.keyEpoch\n )\n return false;\n // One signed chain holds one key per epoch, so a matching epoch with a\n // different key can only come from a fork, never from this history.\n if (cited.keyEpoch === head.keyEpoch)\n return cited.keyFingerprint === head.keyFingerprint;\n if (!evidence.loadHistoryPage || cited.version >= head.version)\n return false;\n return memberStayedSinceCitedState(\n request,\n head,\n evidence.loadHistoryPage,\n ).catch(() => false);\n };\n}\n"]}
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import type { TrustedUserIdentityResolver } from "../../../data/trustedUserIdentity/index.js";
|
|
2
2
|
import type { OrganizationDirectoryAndGroups, OrganizationDirectoryUser, OrganizationUserDetail } from "../readModel/index.js";
|
|
3
|
+
import type { LeaveRequestMembershipCheck } from "./leaveRequestMembership.js";
|
|
4
|
+
/** What a shown leave request is checked against. */
|
|
5
|
+
export interface LeaveRequestVerifier {
|
|
6
|
+
readonly resolveTrustedUserIdentity: TrustedUserIdentityResolver;
|
|
7
|
+
readonly membershipIsCurrent: LeaveRequestMembershipCheck;
|
|
8
|
+
}
|
|
3
9
|
/**
|
|
4
10
|
* The user as shown, with its leave request kept only when the member signed
|
|
5
|
-
* it. Every directory user a client presents
|
|
6
|
-
* read model or a mutation response carried
|
|
11
|
+
* it during their current membership. Every directory user a client presents
|
|
12
|
+
* passes through here, whether a read model or a mutation response carried
|
|
13
|
+
* it.
|
|
7
14
|
*/
|
|
8
|
-
export declare function withVerifiedDirectoryUser(organizationId: string, user: OrganizationDirectoryUser,
|
|
15
|
+
export declare function withVerifiedDirectoryUser(organizationId: string, user: OrganizationDirectoryUser, verifier: LeaveRequestVerifier): Promise<OrganizationDirectoryUser>;
|
|
9
16
|
/**
|
|
10
|
-
* Keeps only leave requests their member signed
|
|
11
|
-
*
|
|
17
|
+
* Keeps only leave requests their member signed during their current
|
|
18
|
+
* membership, checked against the member's trusted identity and this device's
|
|
19
|
+
* verified Members history; any other is shown as no request at all.
|
|
12
20
|
*/
|
|
13
|
-
export declare function withVerifiedLeaveRequests<Result extends OrganizationDirectoryAndGroups | null | undefined>(result: Result,
|
|
21
|
+
export declare function withVerifiedLeaveRequests<Result extends OrganizationDirectoryAndGroups | null | undefined>(result: Result, verifier: LeaveRequestVerifier): Promise<Result>;
|
|
14
22
|
/** The user detail counterpart of withVerifiedLeaveRequests. */
|
|
15
|
-
export declare function withVerifiedUserLeaveRequest(detail: OrganizationUserDetail | null, organizationId: string,
|
|
23
|
+
export declare function withVerifiedUserLeaveRequest(detail: OrganizationUserDetail | null, organizationId: string, verifier: LeaveRequestVerifier): Promise<OrganizationUserDetail | null>;
|
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
import { isValidOrganizationLeaveRequest } from "../../../internal/crypto/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* The user as shown, with its leave request kept only when the member signed
|
|
4
|
-
* it. Every directory user a client presents
|
|
5
|
-
* read model or a mutation response carried
|
|
4
|
+
* it during their current membership. Every directory user a client presents
|
|
5
|
+
* passes through here, whether a read model or a mutation response carried
|
|
6
|
+
* it.
|
|
6
7
|
*/
|
|
7
|
-
export async function withVerifiedDirectoryUser(organizationId, user,
|
|
8
|
+
export async function withVerifiedDirectoryUser(organizationId, user, verifier) {
|
|
8
9
|
const request = user.leaveRequest;
|
|
9
10
|
if (!request)
|
|
10
11
|
return user;
|
|
11
12
|
// The member's pinned identity, never the key the directory served beside
|
|
12
13
|
// the request: a server that could swap both could forge any member's ask.
|
|
13
|
-
const identity = await
|
|
14
|
-
|
|
14
|
+
const identity = await verifier
|
|
15
|
+
.resolveTrustedUserIdentity(user.userId)
|
|
16
|
+
.catch(() => null);
|
|
17
|
+
const signed = identity !== null &&
|
|
15
18
|
request.organizationId === organizationId &&
|
|
16
19
|
request.userId === user.userId &&
|
|
17
20
|
isValidOrganizationLeaveRequest(request, identity.signingPublicKey);
|
|
21
|
+
// A genuine request from a membership that has since ended would otherwise
|
|
22
|
+
// look current to an administrator once the member rejoined.
|
|
23
|
+
const verified = signed && (await verifier.membershipIsCurrent(request));
|
|
18
24
|
return verified ? user : { ...user, leaveRequest: null };
|
|
19
25
|
}
|
|
20
26
|
/**
|
|
21
|
-
* Keeps only leave requests their member signed
|
|
22
|
-
*
|
|
27
|
+
* Keeps only leave requests their member signed during their current
|
|
28
|
+
* membership, checked against the member's trusted identity and this device's
|
|
29
|
+
* verified Members history; any other is shown as no request at all.
|
|
23
30
|
*/
|
|
24
|
-
export async function withVerifiedLeaveRequests(result,
|
|
31
|
+
export async function withVerifiedLeaveRequests(result, verifier) {
|
|
25
32
|
if (!result?.directory.users.some((user) => user.leaveRequest)) {
|
|
26
33
|
return result;
|
|
27
34
|
}
|
|
@@ -30,17 +37,17 @@ export async function withVerifiedLeaveRequests(result, resolveTrustedUserIdenti
|
|
|
30
37
|
...result,
|
|
31
38
|
directory: {
|
|
32
39
|
...result.directory,
|
|
33
|
-
users: await Promise.all(result.directory.users.map((user) => withVerifiedDirectoryUser(organizationId, user,
|
|
40
|
+
users: await Promise.all(result.directory.users.map((user) => withVerifiedDirectoryUser(organizationId, user, verifier))),
|
|
34
41
|
},
|
|
35
42
|
};
|
|
36
43
|
}
|
|
37
44
|
/** The user detail counterpart of withVerifiedLeaveRequests. */
|
|
38
|
-
export async function withVerifiedUserLeaveRequest(detail, organizationId,
|
|
45
|
+
export async function withVerifiedUserLeaveRequest(detail, organizationId, verifier) {
|
|
39
46
|
if (!detail?.user.leaveRequest)
|
|
40
47
|
return detail;
|
|
41
48
|
return {
|
|
42
49
|
...detail,
|
|
43
|
-
user: await withVerifiedDirectoryUser(organizationId, detail.user,
|
|
50
|
+
user: await withVerifiedDirectoryUser(organizationId, detail.user, verifier),
|
|
44
51
|
};
|
|
45
52
|
}
|
|
46
53
|
//# sourceMappingURL=leaveRequestVerification.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"leaveRequestVerification.js","sourceRoot":"","sources":["leaveRequestVerification.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,+BAA+B,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"leaveRequestVerification.js","sourceRoot":"","sources":["leaveRequestVerification.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,+BAA+B,EAAE,MAAM,mBAAmB,CAAC;AAepE;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,cAAsB,EACtB,IAA+B,EAC/B,QAA8B;IAE9B,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC;IAClC,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,0EAA0E;IAC1E,2EAA2E;IAC3E,MAAM,QAAQ,GAAG,MAAM,QAAQ;SAC5B,0BAA0B,CAAC,IAAI,CAAC,MAAM,CAAC;SACvC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IACrB,MAAM,MAAM,GACV,QAAQ,KAAK,IAAI;QACjB,OAAO,CAAC,cAAc,KAAK,cAAc;QACzC,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM;QAC9B,+BAA+B,CAAC,OAAO,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IACtE,2EAA2E;IAC3E,6DAA6D;IAC7D,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,QAAQ,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC;IACzE,OAAO,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;AAC3D,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAE7C,MAAc,EAAE,QAA8B;IAC9C,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;QAC/D,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,CAAC,SAAS,CAAC;IAC5C,OAAO;QACL,GAAG,MAAM;QACT,SAAS,EAAE;YACT,GAAG,MAAM,CAAC,SAAS;YACnB,KAAK,EAAE,MAAM,OAAO,CAAC,GAAG,CACtB,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAClC,yBAAyB,CAAC,cAAc,EAAE,IAAI,EAAE,QAAQ,CAAC,CAC1D,CACF;SACF;KACF,CAAC;AACJ,CAAC;AAED,gEAAgE;AAChE,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,MAAqC,EACrC,cAAsB,EACtB,QAA8B;IAE9B,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY;QAAE,OAAO,MAAM,CAAC;IAC9C,OAAO;QACL,GAAG,MAAM;QACT,IAAI,EAAE,MAAM,yBAAyB,CACnC,cAAc,EACd,MAAM,CAAC,IAAI,EACX,QAAQ,CACT;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { isValidOrganizationLeaveRequest } from \"@tearleads/crypto\";\nimport type { TrustedUserIdentityResolver } from \"../../../data/trustedUserIdentity\";\nimport type {\n OrganizationDirectoryAndGroups,\n OrganizationDirectoryUser,\n OrganizationUserDetail,\n} from \"../readModel\";\nimport type { LeaveRequestMembershipCheck } from \"./leaveRequestMembership\";\n\n/** What a shown leave request is checked against. */\nexport interface LeaveRequestVerifier {\n readonly resolveTrustedUserIdentity: TrustedUserIdentityResolver;\n readonly membershipIsCurrent: LeaveRequestMembershipCheck;\n}\n\n/**\n * The user as shown, with its leave request kept only when the member signed\n * it during their current membership. Every directory user a client presents\n * passes through here, whether a read model or a mutation response carried\n * it.\n */\nexport async function withVerifiedDirectoryUser(\n organizationId: string,\n user: OrganizationDirectoryUser,\n verifier: LeaveRequestVerifier,\n): Promise<OrganizationDirectoryUser> {\n const request = user.leaveRequest;\n if (!request) return user;\n // The member's pinned identity, never the key the directory served beside\n // the request: a server that could swap both could forge any member's ask.\n const identity = await verifier\n .resolveTrustedUserIdentity(user.userId)\n .catch(() => null);\n const signed =\n identity !== null &&\n request.organizationId === organizationId &&\n request.userId === user.userId &&\n isValidOrganizationLeaveRequest(request, identity.signingPublicKey);\n // A genuine request from a membership that has since ended would otherwise\n // look current to an administrator once the member rejoined.\n const verified = signed && (await verifier.membershipIsCurrent(request));\n return verified ? user : { ...user, leaveRequest: null };\n}\n\n/**\n * Keeps only leave requests their member signed during their current\n * membership, checked against the member's trusted identity and this device's\n * verified Members history; any other is shown as no request at all.\n */\nexport async function withVerifiedLeaveRequests<\n Result extends OrganizationDirectoryAndGroups | null | undefined,\n>(result: Result, verifier: LeaveRequestVerifier): Promise<Result> {\n if (!result?.directory.users.some((user) => user.leaveRequest)) {\n return result;\n }\n const { organizationId } = result.directory;\n return {\n ...result,\n directory: {\n ...result.directory,\n users: await Promise.all(\n result.directory.users.map((user) =>\n withVerifiedDirectoryUser(organizationId, user, verifier),\n ),\n ),\n },\n };\n}\n\n/** The user detail counterpart of withVerifiedLeaveRequests. */\nexport async function withVerifiedUserLeaveRequest(\n detail: OrganizationUserDetail | null,\n organizationId: string,\n verifier: LeaveRequestVerifier,\n): Promise<OrganizationUserDetail | null> {\n if (!detail?.user.leaveRequest) return detail;\n return {\n ...detail,\n user: await withVerifiedDirectoryUser(\n organizationId,\n detail.user,\n verifier,\n ),\n };\n}\n"]}
|