@house-rules/party 0.0.0-stage → 0.1.0

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 (69) hide show
  1. package/AGENTS.md +67 -0
  2. package/LICENSE +21 -0
  3. package/README.md +142 -2
  4. package/dist/adapters/memory/party.d.ts +3 -0
  5. package/dist/adapters/memory/party.js +170 -0
  6. package/dist/adapters/memory/person.d.ts +3 -0
  7. package/dist/adapters/memory/person.js +87 -0
  8. package/dist/adapters/postgres/party.d.ts +4 -0
  9. package/dist/adapters/postgres/party.js +73 -0
  10. package/dist/adapters/postgres/person.d.ts +4 -0
  11. package/dist/adapters/postgres/person.js +63 -0
  12. package/dist/assignment/assignment.d.ts +13 -0
  13. package/dist/assignment/assignment.js +9 -0
  14. package/dist/assignment/holders.d.ts +11 -0
  15. package/dist/assignment/holders.js +7 -0
  16. package/dist/contract.d.ts +9 -0
  17. package/dist/contract.js +9 -0
  18. package/dist/facade.d.ts +22 -0
  19. package/dist/facade.js +35 -0
  20. package/dist/index.d.ts +294 -0
  21. package/dist/index.js +20 -0
  22. package/dist/invitation/invitation.d.ts +36 -0
  23. package/dist/invitation/invitation.js +17 -0
  24. package/dist/invitation/invitations.d.ts +34 -0
  25. package/dist/invitation/invitations.js +70 -0
  26. package/dist/organisation/organisation.d.ts +59 -0
  27. package/dist/organisation/organisation.js +46 -0
  28. package/dist/person/bind-party.d.ts +7 -0
  29. package/dist/person/bind-party.js +42 -0
  30. package/dist/person/operations.d.ts +306 -0
  31. package/dist/person/operations.js +92 -0
  32. package/dist/person/party.d.ts +36 -0
  33. package/dist/person/party.js +55 -0
  34. package/dist/person/permissions.d.ts +6 -0
  35. package/dist/person/permissions.js +5 -0
  36. package/dist/person/person-store.d.ts +21 -0
  37. package/dist/person/person-store.js +3 -0
  38. package/dist/person/person.d.ts +43 -0
  39. package/dist/person/person.js +25 -0
  40. package/dist/person/policy.d.ts +1 -0
  41. package/dist/person/policy.js +6 -0
  42. package/dist/person/principal-operations.d.ts +110 -0
  43. package/dist/person/principal-operations.js +77 -0
  44. package/dist/person/principal.d.ts +126 -0
  45. package/dist/person/principal.js +49 -0
  46. package/dist/person/principals.d.ts +102 -0
  47. package/dist/person/principals.js +73 -0
  48. package/dist/relationship/relationship.d.ts +30 -0
  49. package/dist/relationship/relationship.js +12 -0
  50. package/dist/relationship/removal.d.ts +101 -0
  51. package/dist/relationship/removal.js +22 -0
  52. package/dist/role/access.d.ts +171 -0
  53. package/dist/role/access.js +252 -0
  54. package/dist/role/operations.d.ts +145 -0
  55. package/dist/role/operations.js +215 -0
  56. package/dist/role/people.d.ts +45 -0
  57. package/dist/role/people.js +57 -0
  58. package/dist/role/permission-catalogue.d.ts +2 -0
  59. package/dist/role/permission-catalogue.js +1 -0
  60. package/dist/role/role.d.ts +31 -0
  61. package/dist/role/role.js +12 -0
  62. package/dist/storage.d.ts +38 -0
  63. package/dist/storage.js +4 -0
  64. package/migrations/0001_party_person.sql +5 -0
  65. package/migrations/0002_party_model.sql +27 -0
  66. package/migrations/0003_party_invitation.sql +11 -0
  67. package/migrations/0004_publish_staff.sql +4 -0
  68. package/migrations/0005_principals.sql +18 -0
  69. package/package.json +57 -3
@@ -0,0 +1,49 @@
1
+ import { ActorId, errorKind } from "@house-rules/domain/contract";
2
+ import { Schema } from "effect";
3
+ import { Person } from "./person.js";
4
+ export const PrincipalKind = Schema.Literals(["person", "agent"]);
5
+ export const PrincipalHandle = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(120));
6
+ export const Principal = Schema.Struct({
7
+ ...Person.fields,
8
+ kind: PrincipalKind,
9
+ handle: Schema.String,
10
+ createdBy: Schema.NullOr(ActorId),
11
+ disabledAt: Schema.NullOr(Schema.Int),
12
+ });
13
+ export const CreatePrincipal = Schema.Struct({
14
+ id: Schema.optionalKey(ActorId),
15
+ displayName: Schema.String,
16
+ kind: PrincipalKind,
17
+ handle: Schema.String,
18
+ createdBy: Schema.NullOr(ActorId),
19
+ });
20
+ export const PrincipalCreated = Schema.Struct({
21
+ kind: Schema.Literal("principal.created"),
22
+ after: Principal,
23
+ });
24
+ export const PrincipalDisabled = Schema.Struct({
25
+ kind: Schema.Literal("principal.disabled"),
26
+ before: Principal,
27
+ after: Principal,
28
+ });
29
+ export const CreatedPrincipal = Schema.Struct({ principal: Principal, fact: PrincipalCreated });
30
+ export const DisabledPrincipal = Schema.Struct({
31
+ principal: Principal,
32
+ fact: Schema.NullOr(PrincipalDisabled),
33
+ });
34
+ export class PrincipalDisabledRefused extends Schema.TaggedError()("PrincipalDisabledRefused", { kind: errorKind("forbidden") }) {
35
+ }
36
+ export class PrincipalKindRefused extends Schema.TaggedError()("PrincipalKindRefused", { kind: errorKind("forbidden") }) {
37
+ }
38
+ export const legacyPrincipal = (person) => ({
39
+ ...person,
40
+ kind: "person",
41
+ handle: person.id,
42
+ createdBy: null,
43
+ disabledAt: null,
44
+ });
45
+ export const personOf = ({ id, displayName, createdAt }) => ({
46
+ id,
47
+ displayName,
48
+ createdAt,
49
+ });
@@ -0,0 +1,102 @@
1
+ import { ActorId, type NoOpenUnit } from "@house-rules/domain";
2
+ import { Effect } from "effect";
3
+ import type { PartyOptions } from "../role/role.js";
4
+ import type { PersonStore } from "../storage.js";
5
+ import { PersonAlreadyExists, PersonInputInvalid, PersonNotFound, type PersonStoreUnavailable } from "./person.js";
6
+ import { PrincipalDisabledRefused, PrincipalKindRefused, type PrincipalKind } from "./principal.js";
7
+ export declare const makePrincipals: (store: PersonStore["Service"], options: PartyOptions) => {
8
+ createPrincipal: (input: {
9
+ readonly id?: string & import("effect/Brand").Brand<"ActorId">;
10
+ readonly displayName: string;
11
+ readonly kind: "agent" | "person";
12
+ readonly handle: string;
13
+ readonly createdBy: (string & import("effect/Brand").Brand<"ActorId">) | null;
14
+ }) => Effect.Effect<{
15
+ readonly principal: {
16
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
17
+ readonly displayName: string;
18
+ readonly createdAt: number;
19
+ readonly kind: "agent" | "person";
20
+ readonly handle: string;
21
+ readonly createdBy: (string & import("effect/Brand").Brand<"ActorId">) | null;
22
+ readonly disabledAt: number | null;
23
+ };
24
+ readonly fact: {
25
+ readonly kind: "principal.created";
26
+ readonly after: {
27
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
28
+ readonly displayName: string;
29
+ readonly createdAt: number;
30
+ readonly kind: "agent" | "person";
31
+ readonly handle: string;
32
+ readonly createdBy: (string & import("effect/Brand").Brand<"ActorId">) | null;
33
+ readonly disabledAt: number | null;
34
+ };
35
+ };
36
+ }, NoOpenUnit | PersonAlreadyExists | PersonInputInvalid | PersonStoreUnavailable, never>;
37
+ standingOf: (id: string & import("effect/Brand").Brand<"ActorId">) => Effect.Effect<"active" | "disabled" | "missing", PersonStoreUnavailable, never>;
38
+ lockActive: (input: {
39
+ readonly handle: string;
40
+ readonly kind: PrincipalKind;
41
+ }) => Effect.Effect<{
42
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
43
+ readonly displayName: string;
44
+ readonly createdAt: number;
45
+ readonly kind: "agent" | "person";
46
+ readonly handle: string;
47
+ readonly createdBy: (string & import("effect/Brand").Brand<"ActorId">) | null;
48
+ readonly disabledAt: number | null;
49
+ }, NoOpenUnit | PersonNotFound | PersonStoreUnavailable | PrincipalDisabledRefused | PrincipalKindRefused, never>;
50
+ disable: (id: string & import("effect/Brand").Brand<"ActorId">) => Effect.Effect<{
51
+ readonly principal: {
52
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
53
+ readonly displayName: string;
54
+ readonly createdAt: number;
55
+ readonly kind: "agent" | "person";
56
+ readonly handle: string;
57
+ readonly createdBy: (string & import("effect/Brand").Brand<"ActorId">) | null;
58
+ readonly disabledAt: number | null;
59
+ };
60
+ readonly fact: {
61
+ readonly kind: "principal.disabled";
62
+ readonly before: {
63
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
64
+ readonly displayName: string;
65
+ readonly createdAt: number;
66
+ readonly kind: "agent" | "person";
67
+ readonly handle: string;
68
+ readonly createdBy: (string & import("effect/Brand").Brand<"ActorId">) | null;
69
+ readonly disabledAt: number | null;
70
+ };
71
+ readonly after: {
72
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
73
+ readonly displayName: string;
74
+ readonly createdAt: number;
75
+ readonly kind: "agent" | "person";
76
+ readonly handle: string;
77
+ readonly createdBy: (string & import("effect/Brand").Brand<"ActorId">) | null;
78
+ readonly disabledAt: number | null;
79
+ };
80
+ } | null;
81
+ }, NoOpenUnit | PersonNotFound | PersonStoreUnavailable, never>;
82
+ findPrincipal: (input: {
83
+ readonly handle: string;
84
+ }) => Effect.Effect<{
85
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
86
+ readonly displayName: string;
87
+ readonly createdAt: number;
88
+ readonly kind: "agent" | "person";
89
+ readonly handle: string;
90
+ readonly createdBy: (string & import("effect/Brand").Brand<"ActorId">) | null;
91
+ readonly disabledAt: number | null;
92
+ }, PersonNotFound | PersonStoreUnavailable, never>;
93
+ principalsByIds: (ids: readonly ActorId[]) => Effect.Effect<readonly {
94
+ readonly id: string & import("effect/Brand").Brand<"ActorId">;
95
+ readonly displayName: string;
96
+ readonly createdAt: number;
97
+ readonly kind: "agent" | "person";
98
+ readonly handle: string;
99
+ readonly createdBy: (string & import("effect/Brand").Brand<"ActorId">) | null;
100
+ readonly disabledAt: number | null;
101
+ }[], PersonStoreUnavailable, never>;
102
+ };
@@ -0,0 +1,73 @@
1
+ import { ActorId, UnitOfWork } from "@house-rules/domain";
2
+ import { DateTime, Effect, Random, Schema } from "effect";
3
+ import { DisplayName, PersonAlreadyExists, PersonInputInvalid, PersonNotFound, } from "./person.js";
4
+ import { CreatePrincipal, PrincipalDisabledRefused, PrincipalHandle, PrincipalKindRefused, } from "./principal.js";
5
+ export const makePrincipals = (store, options) => {
6
+ const handle = options.handles?.pattern
7
+ ? PrincipalHandle.check(Schema.isPattern(options.handles.pattern))
8
+ : PrincipalHandle;
9
+ const decodeInput = Schema.decodeUnknownEffect(CreatePrincipal);
10
+ const decodeHandle = Schema.decodeUnknownEffect(handle);
11
+ const decodeName = Schema.decodeUnknownEffect(DisplayName);
12
+ const invalid = () => new PersonInputInvalid({ message: "Invalid principal input." });
13
+ return {
14
+ createPrincipal: Effect.fn("Party.createPrincipal")(function* (input) {
15
+ yield* UnitOfWork.required;
16
+ const decoded = yield* decodeInput(input).pipe(Effect.mapError(invalid));
17
+ const handle = yield* decodeHandle(decoded.handle).pipe(Effect.mapError(invalid));
18
+ if (options.handles?.reserved?.includes(handle))
19
+ return yield* invalid();
20
+ const displayName = yield* decodeName(decoded.displayName).pipe(Effect.mapError(invalid));
21
+ const words = yield* Effect.all(Array.from({ length: 4 }, () => Random.nextIntBetween(0, 0xffff_ffff)));
22
+ const principal = {
23
+ ...decoded,
24
+ id: decoded.id ?? ActorId.make(words.map((w) => w.toString(16).padStart(8, "0")).join("")),
25
+ handle,
26
+ displayName,
27
+ createdAt: DateTime.toEpochMillis(yield* DateTime.now),
28
+ disabledAt: null,
29
+ };
30
+ if (!(yield* store.insertPrincipal(principal)))
31
+ return yield* new PersonAlreadyExists();
32
+ return { principal, fact: { kind: "principal.created", after: principal } };
33
+ }),
34
+ standingOf: Effect.fn("Party.standingOf")(function* (id) {
35
+ const principal = yield* store.principal(id);
36
+ return principal === undefined
37
+ ? "missing"
38
+ : principal.disabledAt === null
39
+ ? "active"
40
+ : "disabled";
41
+ }),
42
+ lockActive: Effect.fn("Party.lockActive")(function* (input) {
43
+ yield* UnitOfWork.required;
44
+ const principal = yield* store.lockPrincipal(input.handle);
45
+ if (!principal)
46
+ return yield* new PersonNotFound();
47
+ if (principal.kind !== input.kind)
48
+ return yield* new PrincipalKindRefused();
49
+ if (principal.disabledAt !== null)
50
+ return yield* new PrincipalDisabledRefused();
51
+ return principal;
52
+ }),
53
+ disable: Effect.fn("Party.disable")(function* (id) {
54
+ yield* UnitOfWork.required;
55
+ const before = yield* store.principalForUpdate(id);
56
+ if (!before)
57
+ return yield* new PersonNotFound();
58
+ if (before.disabledAt !== null)
59
+ return { principal: before, fact: null };
60
+ const after = yield* store.disable(id, DateTime.toEpochMillis(yield* DateTime.now));
61
+ if (!after)
62
+ return yield* new PersonNotFound();
63
+ return { principal: after, fact: { kind: "principal.disabled", before, after } };
64
+ }),
65
+ findPrincipal: Effect.fn("Party.findPrincipal")(function* (input) {
66
+ const principal = yield* store.principalByHandle(input.handle);
67
+ if (!principal)
68
+ return yield* new PersonNotFound();
69
+ return principal;
70
+ }),
71
+ principalsByIds: (ids) => store.principalsByIds(ids),
72
+ };
73
+ };
@@ -0,0 +1,30 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { PartyInputInvalid } from "../organisation/organisation.js";
3
+ export declare const PartyReference: Schema.Struct<{
4
+ readonly kind: Schema.Literals<readonly ["person", "organisation"]>;
5
+ readonly id: Schema.NonEmptyString;
6
+ }>;
7
+ export type PartyReference = typeof PartyReference.Type;
8
+ export declare const Relationship: Schema.Struct<{
9
+ readonly from: Schema.Struct<{
10
+ readonly kind: Schema.Literals<readonly ["person", "organisation"]>;
11
+ readonly id: Schema.NonEmptyString;
12
+ }>;
13
+ readonly to: Schema.Struct<{
14
+ readonly kind: Schema.Literals<readonly ["person", "organisation"]>;
15
+ readonly id: Schema.NonEmptyString;
16
+ }>;
17
+ readonly kind: Schema.NonEmptyString;
18
+ }>;
19
+ export type Relationship = typeof Relationship.Type;
20
+ export declare const validateRelationship: (input: Relationship) => Effect.Effect<{
21
+ readonly from: {
22
+ readonly kind: "organisation" | "person";
23
+ readonly id: string;
24
+ };
25
+ readonly to: {
26
+ readonly kind: "organisation" | "person";
27
+ readonly id: string;
28
+ };
29
+ readonly kind: string;
30
+ }, PartyInputInvalid, never>;
@@ -0,0 +1,12 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { PartyInputInvalid } from "../organisation/organisation.js";
3
+ export const PartyReference = Schema.Struct({
4
+ kind: Schema.Literals(["person", "organisation"]),
5
+ id: Schema.NonEmptyString,
6
+ });
7
+ export const Relationship = Schema.Struct({
8
+ from: PartyReference,
9
+ to: PartyReference,
10
+ kind: Schema.NonEmptyString,
11
+ });
12
+ export const validateRelationship = (input) => Schema.decodeEffect(Relationship)(input).pipe(Effect.mapError(() => new PartyInputInvalid({ message: "Invalid relationship." })));
@@ -0,0 +1,101 @@
1
+ import { Effect } from "effect";
2
+ import { PartyLastAdmin, type PartyUnavailable } from "../organisation/organisation.js";
3
+ import type { Relationship } from "./relationship.js";
4
+ export declare const removeStaffAssignments: (store: {
5
+ readonly organisation: (id: string) => Effect.Effect<{
6
+ readonly id: string;
7
+ readonly name: string;
8
+ } | undefined, PartyUnavailable, never>;
9
+ readonly organisationsFor: (personId: import("@house-rules/domain").ActorId) => Effect.Effect<readonly {
10
+ readonly id: string;
11
+ readonly name: string;
12
+ readonly relation: string | null;
13
+ }[], PartyUnavailable, never>;
14
+ readonly relationsFor: (personId: import("@house-rules/domain").ActorId, organisationIds: readonly string[] | null) => Effect.Effect<readonly {
15
+ readonly id: string;
16
+ readonly name: string;
17
+ readonly relation: string;
18
+ }[], PartyUnavailable, never>;
19
+ readonly scopedAssignments: (personId: import("@house-rules/domain").ActorId, organisationIds: readonly string[] | null) => Effect.Effect<readonly {
20
+ readonly personId: string & import("effect/Brand").Brand<"ActorId">;
21
+ readonly roleKey: string;
22
+ readonly organisationId: string | null;
23
+ }[], PartyUnavailable, never>;
24
+ readonly assignmentsInOrganisation: (organisationId: string) => Effect.Effect<readonly {
25
+ readonly personId: string & import("effect/Brand").Brand<"ActorId">;
26
+ readonly roleKey: string;
27
+ readonly organisationId: string | null;
28
+ }[], PartyUnavailable, never>;
29
+ readonly holderIds: (role: string, scope: string | null, take: number) => Effect.Effect<readonly (string & import("effect/Brand").Brand<"ActorId">)[], PartyUnavailable, never>;
30
+ readonly lockOrganisation: (organisationId: string) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | PartyUnavailable, never>;
31
+ readonly memberIds: (organisationIds: readonly string[]) => Effect.Effect<readonly (string & import("effect/Brand").Brand<"ActorId">)[], PartyUnavailable, never>;
32
+ readonly insertInvitation: (value: import("../invitation/invitation.ts").StoredInvitation) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | PartyUnavailable, never>;
33
+ readonly consumeInvitation: (hash: string, now: number) => Effect.Effect<{
34
+ readonly id: string;
35
+ readonly organisationId: string;
36
+ readonly roleKey: string | null;
37
+ readonly createdBy: string & import("effect/Brand").Brand<"ActorId">;
38
+ readonly expiresAt: number;
39
+ readonly state: "pending" | "revoked" | "used";
40
+ readonly codeHash: string;
41
+ } | undefined, import("@house-rules/domain").NoOpenUnit | PartyUnavailable, never>;
42
+ readonly invitation: (id: string) => Effect.Effect<{
43
+ readonly id: string;
44
+ readonly organisationId: string;
45
+ readonly roleKey: string | null;
46
+ readonly createdBy: string & import("effect/Brand").Brand<"ActorId">;
47
+ readonly expiresAt: number;
48
+ readonly state: "pending" | "revoked" | "used";
49
+ readonly codeHash: string;
50
+ } | undefined, PartyUnavailable, never>;
51
+ readonly invitations: (organisationId: string) => Effect.Effect<readonly {
52
+ readonly id: string;
53
+ readonly organisationId: string;
54
+ readonly roleKey: string | null;
55
+ readonly createdBy: string & import("effect/Brand").Brand<"ActorId">;
56
+ readonly expiresAt: number;
57
+ readonly state: "pending" | "revoked" | "used";
58
+ readonly codeHash: string;
59
+ }[], PartyUnavailable, never>;
60
+ readonly revokeInvitation: (id: string) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | PartyUnavailable, never>;
61
+ readonly insertOrganisation: (organisation: import("../organisation/organisation.js").Organisation) => Effect.Effect<boolean, import("@house-rules/domain").NoOpenUnit | PartyUnavailable, never>;
62
+ readonly relate: (relationship: Relationship) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | PartyUnavailable, never>;
63
+ readonly unrelate: (relationship: Relationship) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | PartyUnavailable, never>;
64
+ readonly isRelated: (relationship: Relationship) => Effect.Effect<boolean, PartyUnavailable, never>;
65
+ readonly role: (key: string) => Effect.Effect<{
66
+ readonly key: string;
67
+ readonly name: string;
68
+ readonly permissions: readonly string[];
69
+ readonly organisationId: string | null;
70
+ readonly builtIn: boolean;
71
+ } | undefined, PartyUnavailable, never>;
72
+ readonly roles: (organisationIds?: readonly string[] | null) => Effect.Effect<readonly {
73
+ readonly key: string;
74
+ readonly name: string;
75
+ readonly permissions: readonly string[];
76
+ readonly organisationId: string | null;
77
+ readonly builtIn: boolean;
78
+ }[], PartyUnavailable, never>;
79
+ readonly putRole: (role: import("../contract.ts").Role) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | PartyUnavailable, never>;
80
+ readonly deleteRole: (key: string) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | PartyUnavailable, never>;
81
+ readonly assign: (assignment: import("../contract.ts").Assignment) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | PartyUnavailable, never>;
82
+ readonly revoke: (assignment: import("../contract.ts").Assignment) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | PartyUnavailable, never>;
83
+ readonly assignments: (personId: import("@house-rules/domain").ActorId, organisationIds?: readonly string[] | null, builtInKeys?: readonly string[]) => Effect.Effect<readonly {
84
+ readonly personId: string & import("effect/Brand").Brand<"ActorId">;
85
+ readonly roleKey: string;
86
+ readonly organisationId: string | null;
87
+ }[], PartyUnavailable, never>;
88
+ }, role: (key: string) => Effect.Effect<{
89
+ readonly organisationId: string | null;
90
+ readonly permissions: readonly string[];
91
+ } | undefined, PartyUnavailable>, value: {
92
+ readonly from: {
93
+ readonly kind: "organisation" | "person";
94
+ readonly id: string;
95
+ };
96
+ readonly to: {
97
+ readonly kind: "organisation" | "person";
98
+ readonly id: string;
99
+ };
100
+ readonly kind: string;
101
+ }) => Effect.Effect<undefined, import("@house-rules/domain").NoOpenUnit | PartyLastAdmin | PartyUnavailable, never>;
@@ -0,0 +1,22 @@
1
+ import { Effect } from "effect";
2
+ import { PartyLastAdmin } from "../organisation/organisation.js";
3
+ export const removeStaffAssignments = Effect.fn("Party.removeStaffAssignments")(function* (store, role, value) {
4
+ if (value.from.kind !== "person" || value.to.kind !== "organisation" || value.kind !== "staff")
5
+ return;
6
+ // Serialize removals so two admins cannot each see the other one and both leave.
7
+ yield* store.lockOrganisation(value.to.id);
8
+ const assignments = yield* store.assignmentsInOrganisation(value.to.id);
9
+ const admins = new Set();
10
+ for (const assignment of assignments) {
11
+ const held = yield* role(assignment.roleKey);
12
+ if (held &&
13
+ (held.organisationId === null || held.organisationId === value.to.id) &&
14
+ held.permissions.includes("party:admin"))
15
+ admins.add(assignment.personId);
16
+ }
17
+ if (admins.has(value.from.id) && admins.size === 1)
18
+ return yield* new PartyLastAdmin();
19
+ for (const assignment of assignments)
20
+ if (assignment.personId === value.from.id)
21
+ yield* store.revoke(assignment);
22
+ });
@@ -0,0 +1,171 @@
1
+ import { type Caller, type ActorId } from "@house-rules/domain";
2
+ import { Effect } from "effect";
3
+ import { PartyConflict, PartyDenied, PartyInputInvalid, PartyNotFound, PartyUnavailable } from "../organisation/organisation.js";
4
+ import { type Relationship } from "../relationship/relationship.js";
5
+ import type { PartyStore, PersonStore } from "../storage.js";
6
+ import type { RoleCatalogue } from "./permission-catalogue.js";
7
+ import { type PartyOptions } from "./role.js";
8
+ export declare const makeAccess: (store: PartyStore["Service"], persons: {
9
+ readonly find: (id: ActorId) => Effect.Effect<unknown, PartyUnavailable>;
10
+ readonly principal: (id: ActorId) => Effect.Effect<{
11
+ readonly disabledAt: number | null;
12
+ } | undefined, PartyUnavailable>;
13
+ readonly list: (ids: readonly ActorId[] | null) => Effect.Effect<Effect.Success<ReturnType<PersonStore["Service"]["list"]>>, PartyUnavailable>;
14
+ }, options: PartyOptions, catalogue: RoleCatalogue | undefined) => {
15
+ listPeople: (caller: Caller) => Effect.Effect<{
16
+ id: string & import("effect/Brand").Brand<"ActorId">;
17
+ displayName: string;
18
+ createdAt: number;
19
+ assignments: {
20
+ roleKey: string;
21
+ roleName: string;
22
+ organisationId: string | null;
23
+ organisationName: string | null;
24
+ }[];
25
+ organisations: readonly {
26
+ readonly id: string;
27
+ readonly name: string;
28
+ readonly relation: string;
29
+ }[];
30
+ }[], PartyDenied | PartyUnavailable, never>;
31
+ listMembers: (caller: Caller, organisationId: string) => Effect.Effect<{
32
+ id: string & import("effect/Brand").Brand<"ActorId">;
33
+ displayName: string;
34
+ createdAt: number;
35
+ relation: string | null;
36
+ roles: {
37
+ roleKey: string;
38
+ roleName: string;
39
+ }[];
40
+ }[], PartyNotFound | PartyUnavailable, never>;
41
+ myOrganisations: (caller: Caller) => Effect.Effect<{
42
+ id: string;
43
+ name: string;
44
+ relation: string | null;
45
+ roles: {
46
+ roleKey: string;
47
+ roleName: string;
48
+ permissions: readonly string[];
49
+ }[];
50
+ }[], PartyDenied | PartyUnavailable, never>;
51
+ holdersOf: (input: {
52
+ readonly role: string;
53
+ readonly scope?: string | null;
54
+ }) => Effect.Effect<{
55
+ ids: (string & import("effect/Brand").Brand<"ActorId">)[];
56
+ truncated: boolean;
57
+ }, PartyUnavailable, never>;
58
+ createInvitation: (caller: Caller, input: {
59
+ readonly organisationId: string;
60
+ readonly roleKey: string | null;
61
+ }) => Effect.Effect<{
62
+ invitation: {
63
+ id: string;
64
+ organisationId: string;
65
+ roleKey: string | null;
66
+ createdBy: string & import("effect/Brand").Brand<"ActorId">;
67
+ expiresAt: number;
68
+ state: "pending" | "revoked" | "used";
69
+ };
70
+ code: string;
71
+ }, import("@house-rules/domain").NoOpenUnit | PartyDenied | PartyNotFound | PartyUnavailable, never>;
72
+ acceptInvitation: (caller: Caller, code: string) => Effect.Effect<{
73
+ organisationId: string;
74
+ roleKey: string | null;
75
+ personId: string & import("effect/Brand").Brand<"ActorId">;
76
+ }, import("@house-rules/domain").NoOpenUnit | PartyDenied | PartyNotFound | PartyUnavailable, never>;
77
+ revokeInvitation: (caller: Caller, id: string) => Effect.Effect<undefined, import("@house-rules/domain").NoOpenUnit | PartyDenied | PartyNotFound | PartyUnavailable, never>;
78
+ listInvitations: (caller: Caller, organisationId: string) => Effect.Effect<{
79
+ id: string;
80
+ organisationId: string;
81
+ roleKey: string | null;
82
+ createdBy: string & import("effect/Brand").Brand<"ActorId">;
83
+ expiresAt: number;
84
+ state: "pending" | "revoked" | "used";
85
+ }[], PartyDenied | PartyUnavailable, never>;
86
+ permissionsOf: (caller: Readonly<{
87
+ kind: "person";
88
+ actor: import("@house-rules/domain").Actor;
89
+ via: string;
90
+ }>) => Effect.Effect<`${string}:${string}`[], PartyUnavailable, never>;
91
+ canAdminOrganisation: (caller: Caller, organisationId: string) => Effect.Effect<boolean, PartyUnavailable, never>;
92
+ myAccess: (caller: Caller) => Effect.Effect<{
93
+ roles: readonly {
94
+ readonly personId: string & import("effect/Brand").Brand<"ActorId">;
95
+ readonly roleKey: string;
96
+ readonly organisationId: string | null;
97
+ }[];
98
+ permissions: `${string}:${string}`[];
99
+ catalogue: `${string}:${string}`[];
100
+ }, PartyDenied | PartyUnavailable, never>;
101
+ createOrganisation: (caller: Caller, input: {
102
+ readonly id: string;
103
+ readonly name: string;
104
+ }) => Effect.Effect<{
105
+ readonly id: string;
106
+ readonly name: string;
107
+ }, import("@house-rules/domain").NoOpenUnit | PartyConflict | PartyDenied | PartyInputInvalid | PartyUnavailable, never>;
108
+ showOrganisation: (id: string) => Effect.Effect<{
109
+ readonly id: string;
110
+ readonly name: string;
111
+ }, PartyNotFound | PartyUnavailable, never>;
112
+ relate: (caller: Caller, input: {
113
+ readonly from: {
114
+ readonly kind: "organisation" | "person";
115
+ readonly id: string;
116
+ };
117
+ readonly to: {
118
+ readonly kind: "organisation" | "person";
119
+ readonly id: string;
120
+ };
121
+ readonly kind: string;
122
+ }) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | PartyDenied | PartyInputInvalid | PartyNotFound | PartyUnavailable, never>;
123
+ unrelate: (caller: Caller, input: {
124
+ readonly from: {
125
+ readonly kind: "organisation" | "person";
126
+ readonly id: string;
127
+ };
128
+ readonly to: {
129
+ readonly kind: "organisation" | "person";
130
+ readonly id: string;
131
+ };
132
+ readonly kind: string;
133
+ }) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | PartyDenied | PartyInputInvalid | import("../organisation/organisation.js").PartyLastAdmin | PartyUnavailable, never>;
134
+ isRelated: (input: Relationship) => Effect.Effect<boolean, PartyInputInvalid | PartyUnavailable, never>;
135
+ defineRole: (caller: Caller, input: Omit<{
136
+ readonly key: string;
137
+ readonly name: string;
138
+ readonly permissions: readonly string[];
139
+ readonly organisationId: string | null;
140
+ readonly builtIn: boolean;
141
+ }, "builtIn">) => Effect.Effect<{
142
+ readonly key: string;
143
+ readonly name: string;
144
+ readonly permissions: readonly string[];
145
+ readonly organisationId: string | null;
146
+ readonly builtIn: boolean;
147
+ }, import("@house-rules/domain").NoOpenUnit | PartyConflict | PartyDenied | PartyInputInvalid | PartyNotFound | PartyUnavailable, never>;
148
+ deleteRole: (caller: Caller, key: string) => Effect.Effect<undefined, import("@house-rules/domain").NoOpenUnit | PartyConflict | PartyDenied | PartyNotFound | PartyUnavailable, never>;
149
+ listRoles: (caller: Caller) => Effect.Effect<{
150
+ readonly key: string;
151
+ readonly name: string;
152
+ readonly permissions: readonly string[];
153
+ readonly organisationId: string | null;
154
+ readonly builtIn: boolean;
155
+ }[], PartyUnavailable, never>;
156
+ assignRole: (caller: Caller, input: {
157
+ readonly personId: string & import("effect/Brand").Brand<"ActorId">;
158
+ readonly roleKey: string;
159
+ readonly organisationId: string | null;
160
+ }) => Effect.Effect<undefined, import("@house-rules/domain").NoOpenUnit | PartyDenied | PartyInputInvalid | PartyNotFound | PartyUnavailable, never>;
161
+ revokeRole: (caller: Caller, input: {
162
+ readonly personId: string & import("effect/Brand").Brand<"ActorId">;
163
+ readonly roleKey: string;
164
+ readonly organisationId: string | null;
165
+ }) => Effect.Effect<void, import("@house-rules/domain").NoOpenUnit | PartyDenied | PartyInputInvalid | PartyUnavailable, never>;
166
+ rolesOf: (caller: Caller, personId: string & import("effect/Brand").Brand<"ActorId">) => Effect.Effect<readonly {
167
+ readonly personId: string & import("effect/Brand").Brand<"ActorId">;
168
+ readonly roleKey: string;
169
+ readonly organisationId: string | null;
170
+ }[], PartyUnavailable, never>;
171
+ };