@lenso/organization 0.0.0-stage → 0.2.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.
@@ -0,0 +1,67 @@
1
+ import {
2
+ assertSnapshotCapacity
3
+ } from "./index-qgnmjsxx.js";
4
+
5
+ // src/drizzle/shared.ts
6
+ function assertValidVersion(value, label) {
7
+ if (!Number.isSafeInteger(value) || value < 1) {
8
+ throw new Error(`Invalid organization ${label}`);
9
+ }
10
+ }
11
+ function validateState(state) {
12
+ if (!state || typeof state !== "object" || !state.organization || !validText(state.organization.id, 256) || !validText(state.organization.name, 200) || !validTime(state.organization.createdAt) || !validTime(state.organization.updatedAt)) {
13
+ throw new Error("Invalid persisted organization state");
14
+ }
15
+ assertValidVersion(state.organization.version, "state version");
16
+ if (!Array.isArray(state.members) || !Array.isArray(state.invitations) || state.members.length > 1000 || state.invitations.length > 1000 || !state.members.some((member) => member?.role === "owner")) {
17
+ throw new Error("Invalid persisted organization state");
18
+ }
19
+ const subjects = new Set;
20
+ for (const member of state.members) {
21
+ if (!member || !validSubject(member.subject) || !["member", "admin", "owner"].includes(member.role) || !validTime(member.joinedAt))
22
+ throw new Error("Invalid persisted organization member");
23
+ const key = JSON.stringify([member.subject.realmId, member.subject.subjectId]);
24
+ if (subjects.has(key))
25
+ throw new Error("Duplicate persisted organization member");
26
+ subjects.add(key);
27
+ }
28
+ const invitations = new Set;
29
+ for (const invite of state.invitations) {
30
+ if (!invite || !validText(invite.id, 256) || invite.organizationId !== state.organization.id || !validSubject(invite.target) || !validSubject(invite.createdBy) || invite.role !== "member" || !validTime(invite.createdAt) || !validTime(invite.expiresAt) || invite.expiresAt <= invite.createdAt || !["pending", "accepted", "revoked"].includes(invite.status) || (invite.status === "pending" ? invite.resolvedAt !== null : !validTime(invite.resolvedAt)) || typeof invite.tokenDigest !== "string" || !/^[a-f0-9]{64}$/.test(invite.tokenDigest) || invitations.has(invite.id))
31
+ throw new Error("Invalid persisted organization invitation");
32
+ invitations.add(invite.id);
33
+ }
34
+ }
35
+ function validText(value, max) {
36
+ return typeof value === "string" && !!value.trim() && value.length <= max;
37
+ }
38
+ function validTime(value) {
39
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
40
+ }
41
+ function validSubject(value) {
42
+ return !!value && validText(value.realmId, 256) && validText(value.subjectId, 512);
43
+ }
44
+ function encodeState(state) {
45
+ validateState(state);
46
+ assertSnapshotCapacity(state);
47
+ return JSON.stringify(state);
48
+ }
49
+ function decodeSnapshot(id, version, encoded, observedAt) {
50
+ assertValidVersion(version, "row version");
51
+ let state;
52
+ try {
53
+ state = JSON.parse(encoded);
54
+ } catch {
55
+ throw new Error("Invalid persisted organization state");
56
+ }
57
+ validateState(state);
58
+ const decoded = state;
59
+ if (decoded.organization.id !== id || decoded.organization.version !== version) {
60
+ throw new Error("Organization row and state are inconsistent");
61
+ }
62
+ if (!validTime(observedAt))
63
+ throw new Error("Invalid organization storage clock");
64
+ return { state: decoded, observedAt };
65
+ }
66
+
67
+ export { assertValidVersion, validateState, encodeState, decodeSnapshot };
@@ -0,0 +1,370 @@
1
+ import {
2
+ sameSubject2,
3
+ preserveOwners
4
+ } from "./index-kdetdk6w.js";
5
+ import {
6
+ assertSnapshotCapacity
7
+ } from "./index-qgnmjsxx.js";
8
+ import {
9
+ OrganizationError2
10
+ } from "./index-tsx842dx.js";
11
+
12
+ // src/service.ts
13
+ var defaults = {
14
+ invitationLifetimeMs: 7 * 24 * 60 * 60 * 1000,
15
+ maxMembers: 1000,
16
+ maxInvitations: 1000,
17
+ conflictRetries: 8
18
+ };
19
+ function fail(code) {
20
+ throw new OrganizationError2(code);
21
+ }
22
+ function object(value, keys) {
23
+ if (!value || typeof value !== "object" || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value)) || Object.keys(value).some((key) => !keys.includes(key)))
24
+ fail("INVALID_INPUT");
25
+ return value;
26
+ }
27
+ function text(value, maximum = 512) {
28
+ if (typeof value !== "string" || !value.trim() || value.length > maximum)
29
+ fail("INVALID_INPUT");
30
+ return value;
31
+ }
32
+ function subject(value) {
33
+ const input = object(value, ["realmId", "subjectId"]);
34
+ return { realmId: text(input.realmId, 256), subjectId: text(input.subjectId) };
35
+ }
36
+ function integer(value, min, max) {
37
+ if (!Number.isSafeInteger(value) || value < min || value > max) {
38
+ fail("INVALID_INPUT");
39
+ }
40
+ return value;
41
+ }
42
+ function resolveOrganizationConfig2(input = {}) {
43
+ const values = { ...defaults, ...object(input, Object.keys(defaults)) };
44
+ return Object.freeze({
45
+ invitationLifetimeMs: integer(values.invitationLifetimeMs, 1000, 30 * 24 * 60 * 60 * 1000),
46
+ maxMembers: integer(values.maxMembers, 1, 1000),
47
+ maxInvitations: integer(values.maxInvitations, 1, 1000),
48
+ conflictRetries: integer(values.conflictRetries, 0, 32)
49
+ });
50
+ }
51
+ function scoped(input, extra = []) {
52
+ const parsed = object(input, ["organizationId", ...extra]);
53
+ return { parsed, organizationId: text(parsed.organizationId, 256) };
54
+ }
55
+ function member(state, target) {
56
+ return state.members.find((item) => sameSubject2(item.subject, target)) ?? fail("NOT_FOUND");
57
+ }
58
+ function invitation(state, id) {
59
+ return state.invitations.find((item) => item.id === id) ?? fail("NOT_FOUND");
60
+ }
61
+ function present(invite, now) {
62
+ const { tokenDigest: _digest, ...result } = invite;
63
+ return {
64
+ ...result,
65
+ status: result.status === "pending" && result.expiresAt <= now ? "expired" : result.status
66
+ };
67
+ }
68
+ function pending(invite, now) {
69
+ if (invite.status === "accepted")
70
+ fail("INVITATION_ACCEPTED");
71
+ if (invite.status === "revoked")
72
+ fail("INVITATION_REVOKED");
73
+ if (invite.expiresAt <= now)
74
+ fail("INVITATION_EXPIRED");
75
+ }
76
+ function revokePendingFor(state, target, now) {
77
+ for (const invite of state.invitations) {
78
+ if (invite.status === "pending" && sameSubject2(invite.target, target)) {
79
+ invite.status = "revoked";
80
+ invite.resolvedAt = now;
81
+ }
82
+ }
83
+ }
84
+ async function digest(invite, bearer) {
85
+ const data = JSON.stringify([
86
+ invite.organizationId,
87
+ invite.id,
88
+ invite.target.realmId,
89
+ invite.target.subjectId,
90
+ invite.role,
91
+ invite.expiresAt,
92
+ bearer
93
+ ]);
94
+ return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(data))), (byte) => byte.toString(16).padStart(2, "0")).join("");
95
+ }
96
+ function token() {
97
+ return Array.from(crypto.getRandomValues(new Uint8Array(32)), (byte) => byte.toString(16).padStart(2, "0")).join("");
98
+ }
99
+ function equalDigest(a, b) {
100
+ let difference = a.length ^ b.length;
101
+ for (let i = 0;i < Math.max(a.length, b.length); i++) {
102
+ difference |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
103
+ }
104
+ return difference === 0;
105
+ }
106
+ function createOrganizationService2(options) {
107
+ const config = resolveOrganizationConfig2(options.config);
108
+ const { store, access } = options;
109
+ async function authorize(actor, action, organizationId, state, target, details) {
110
+ const verified = await access.check(actor, structuredClone({
111
+ action,
112
+ organizationId,
113
+ organization: state?.organization ?? null,
114
+ members: state?.members ?? [],
115
+ ...target ? { target } : {},
116
+ ...details
117
+ }));
118
+ return subject({ realmId: verified?.realmId, subjectId: verified?.subjectId });
119
+ }
120
+ async function load(actor, action, organizationId, target, details) {
121
+ const snapshot = await store.read(organizationId);
122
+ const invite = details?.invitationId ? snapshot?.state.invitations.find((item) => item.id === details.invitationId) : undefined;
123
+ const principal = await authorize(actor, action, organizationId, snapshot?.state ?? null, invite?.target ?? target, {
124
+ ...details,
125
+ ...details?.invitationId ? { invitation: invite && snapshot ? present(invite, snapshot.observedAt) : null } : {}
126
+ });
127
+ if (!snapshot)
128
+ fail("NOT_FOUND");
129
+ if (snapshot.state.organization.id !== organizationId)
130
+ fail("NOT_FOUND");
131
+ return { ...snapshot, principal };
132
+ }
133
+ function capacity(state, previous) {
134
+ preserveOwners(state.members);
135
+ if (state.members.length > Math.max(config.maxMembers, previous?.members.length ?? 0) || state.invitations.length > Math.max(config.maxInvitations, previous?.invitations.length ?? 0))
136
+ fail("CAPACITY");
137
+ assertSnapshotCapacity(state);
138
+ }
139
+ async function mutate(actor, action, organizationId, target, change, details) {
140
+ for (let attempt = 0;attempt <= config.conflictRetries; attempt++) {
141
+ const snapshot = await load(actor, action, organizationId, target, details);
142
+ const next = structuredClone(snapshot.state);
143
+ const result = await change(next, snapshot.principal, snapshot.observedAt);
144
+ const expected = snapshot.state.organization.version;
145
+ integer(expected, 1, Number.MAX_SAFE_INTEGER - 1);
146
+ next.organization.version = expected + 1;
147
+ next.organization.updatedAt = snapshot.observedAt;
148
+ capacity(next, snapshot.state);
149
+ if (await store.compareAndSwap(expected, next, result.validUntil)) {
150
+ const changeInfo = {
151
+ organizationId,
152
+ version: next.organization.version,
153
+ action,
154
+ subjects: structuredClone(result.subjects ?? [])
155
+ };
156
+ return { value: structuredClone(result.value), change: changeInfo };
157
+ }
158
+ }
159
+ return fail("CONFLICT");
160
+ }
161
+ return {
162
+ async createOrganization(actor, input) {
163
+ const name = text(object(input, ["name"]).name, 200).trim();
164
+ const principal = await authorize(actor, "organization.create", null, null);
165
+ const now = Date.now();
166
+ const organization = {
167
+ id: crypto.randomUUID(),
168
+ name,
169
+ version: 1,
170
+ createdAt: now,
171
+ updatedAt: now
172
+ };
173
+ const state = {
174
+ organization,
175
+ members: [{ subject: principal, role: "owner", joinedAt: now }],
176
+ invitations: []
177
+ };
178
+ capacity(state);
179
+ await store.create(state);
180
+ return {
181
+ value: structuredClone(organization),
182
+ change: {
183
+ organizationId: organization.id,
184
+ version: 1,
185
+ action: "organization.create",
186
+ subjects: [principal]
187
+ }
188
+ };
189
+ },
190
+ async readOrganization(actor, input) {
191
+ const { organizationId } = scoped(input);
192
+ return structuredClone((await load(actor, "organization.read", organizationId)).state.organization);
193
+ },
194
+ async updateOrganization(actor, input) {
195
+ const { organizationId, parsed } = scoped(input, ["name"]);
196
+ const name = text(parsed.name, 200).trim();
197
+ const result = await mutate(actor, "organization.update", organizationId, undefined, (state) => {
198
+ state.organization.name = name;
199
+ return { value: state.organization };
200
+ });
201
+ return result;
202
+ },
203
+ async listMembers(actor, input) {
204
+ const { organizationId } = scoped(input);
205
+ const { state } = await load(actor, "member.list", organizationId);
206
+ return {
207
+ items: structuredClone(state.members),
208
+ total: state.members.length,
209
+ version: state.organization.version
210
+ };
211
+ },
212
+ async addMember(actor, input) {
213
+ const { organizationId, parsed } = scoped(input, ["subject"]);
214
+ const target = subject(parsed.subject);
215
+ return mutate(actor, "member.add", organizationId, target, (state, _principal, now) => {
216
+ if (state.members.some((item) => sameSubject2(item.subject, target)))
217
+ fail("ALREADY_MEMBER");
218
+ const added = { subject: target, role: "member", joinedAt: now };
219
+ state.members.push(added);
220
+ return { value: added, subjects: [target] };
221
+ });
222
+ },
223
+ async leaveOrganization(actor, input) {
224
+ const { organizationId } = scoped(input);
225
+ return mutate(actor, "member.leave", organizationId, undefined, (state, principal, now) => {
226
+ member(state, principal);
227
+ state.members = state.members.filter((item) => !sameSubject2(item.subject, principal));
228
+ preserveOwners(state.members);
229
+ revokePendingFor(state, principal, now);
230
+ return { value: true, subjects: [principal] };
231
+ });
232
+ },
233
+ async removeMember(actor, input) {
234
+ const { organizationId, parsed } = scoped(input, ["subject"]);
235
+ const target = subject(parsed.subject);
236
+ return mutate(actor, "member.remove", organizationId, target, (state, _principal, now) => {
237
+ member(state, target);
238
+ state.members = state.members.filter((item) => !sameSubject2(item.subject, target));
239
+ preserveOwners(state.members);
240
+ revokePendingFor(state, target, now);
241
+ return { value: true, subjects: [target] };
242
+ });
243
+ },
244
+ async setMemberRole(actor, input) {
245
+ const { organizationId, parsed } = scoped(input, ["subject", "role"]);
246
+ const target = subject(parsed.subject);
247
+ if (parsed.role !== "member" && parsed.role !== "admin")
248
+ fail("INVALID_INPUT");
249
+ const role = parsed.role;
250
+ return mutate(actor, "member.role", organizationId, target, (state, principal) => {
251
+ const item = member(state, target);
252
+ if (sameSubject2(principal, target) && item.role === "member" && role === "admin")
253
+ fail("FORBIDDEN");
254
+ item.role = role;
255
+ preserveOwners(state.members);
256
+ return { value: item, subjects: [target] };
257
+ }, { requestedRole: role });
258
+ },
259
+ async grantOwner(actor, input) {
260
+ const { organizationId, parsed } = scoped(input, ["subject"]);
261
+ const target = subject(parsed.subject);
262
+ return mutate(actor, "owner.grant", organizationId, target, (state, principal) => {
263
+ if (sameSubject2(principal, target))
264
+ fail("FORBIDDEN");
265
+ const item = member(state, target);
266
+ item.role = "owner";
267
+ return { value: item, subjects: [target] };
268
+ }, { requestedRole: "owner" });
269
+ },
270
+ async transferOwner(actor, input) {
271
+ const { organizationId, parsed } = scoped(input, ["subject"]);
272
+ const target = subject(parsed.subject);
273
+ return mutate(actor, "owner.transfer", organizationId, target, (state, principal) => {
274
+ if (sameSubject2(principal, target))
275
+ fail("INVALID_INPUT");
276
+ const previous = member(state, principal);
277
+ if (previous.role !== "owner")
278
+ fail("FORBIDDEN");
279
+ const next = member(state, target);
280
+ previous.role = "member";
281
+ next.role = "owner";
282
+ return { value: next, subjects: [principal, target] };
283
+ }, { requestedRole: "owner" });
284
+ },
285
+ async createInvitation(actor, input) {
286
+ const { organizationId, parsed } = scoped(input, ["target"]);
287
+ const target = subject(parsed.target);
288
+ const bearer = token();
289
+ const id = crypto.randomUUID();
290
+ return mutate(actor, "invitation.create", organizationId, target, async (state, principal, now) => {
291
+ if (state.members.some((item) => sameSubject2(item.subject, target)))
292
+ fail("ALREADY_MEMBER");
293
+ const invite = {
294
+ id,
295
+ organizationId,
296
+ target,
297
+ role: "member",
298
+ createdBy: principal,
299
+ createdAt: now,
300
+ expiresAt: now + config.invitationLifetimeMs,
301
+ status: "pending",
302
+ resolvedAt: null,
303
+ tokenDigest: ""
304
+ };
305
+ invite.tokenDigest = await digest(invite, bearer);
306
+ state.invitations.push(invite);
307
+ return { value: { invitation: present(invite, now), token: bearer } };
308
+ });
309
+ },
310
+ async readInvitation(actor, input) {
311
+ const { organizationId, parsed } = scoped(input, ["invitationId"]);
312
+ const id = text(parsed.invitationId, 256);
313
+ const { state, observedAt } = await load(actor, "invitation.read", organizationId, undefined, { invitationId: id });
314
+ return present(invitation(state, id), observedAt);
315
+ },
316
+ async listInvitations(actor, input) {
317
+ const { organizationId } = scoped(input);
318
+ const { state, observedAt } = await load(actor, "invitation.list", organizationId);
319
+ return {
320
+ items: state.invitations.map((invite) => present(invite, observedAt)),
321
+ total: state.invitations.length,
322
+ version: state.organization.version
323
+ };
324
+ },
325
+ async acceptInvitation(actor, input) {
326
+ const { organizationId, parsed } = scoped(input, ["invitationId", "token"]);
327
+ const id = text(parsed.invitationId, 256);
328
+ const bearer = text(parsed.token, 64);
329
+ if (!/^[a-f0-9]{64}$/.test(bearer))
330
+ fail("INVALID_INPUT");
331
+ return mutate(actor, "invitation.accept", organizationId, undefined, async (state, principal, now) => {
332
+ const invite = invitation(state, id);
333
+ if (!sameSubject2(invite.target, principal) || !equalDigest(invite.tokenDigest, await digest(invite, bearer)))
334
+ fail("NOT_FOUND");
335
+ pending(invite, now);
336
+ if (state.members.some((item) => sameSubject2(item.subject, principal)))
337
+ fail("ALREADY_MEMBER");
338
+ const joined = { subject: principal, role: "member", joinedAt: now };
339
+ state.members.push(joined);
340
+ invite.status = "accepted";
341
+ invite.resolvedAt = now;
342
+ return { value: joined, subjects: [principal], validUntil: invite.expiresAt };
343
+ }, { invitationId: id });
344
+ },
345
+ async revokeInvitation(actor, input) {
346
+ const { organizationId, parsed } = scoped(input, ["invitationId"]);
347
+ const id = text(parsed.invitationId, 256);
348
+ return mutate(actor, "invitation.revoke", organizationId, undefined, (state, _principal, now) => {
349
+ const invite = invitation(state, id);
350
+ pending(invite, now);
351
+ invite.status = "revoked";
352
+ invite.resolvedAt = now;
353
+ return { value: present(invite, now) };
354
+ }, { invitationId: id });
355
+ },
356
+ async pruneInvitations(actor, input) {
357
+ const { organizationId, parsed } = scoped(input, ["before"]);
358
+ const before = integer(parsed.before, 0, Number.MAX_SAFE_INTEGER);
359
+ return mutate(actor, "invitation.prune", organizationId, undefined, (state, _principal, now) => {
360
+ if (before > now)
361
+ fail("INVALID_INPUT");
362
+ const previous = state.invitations.length;
363
+ state.invitations = state.invitations.filter((invite) => (invite.status === "pending" ? invite.expiresAt : invite.resolvedAt) >= before);
364
+ return { value: previous - state.invitations.length };
365
+ });
366
+ }
367
+ };
368
+ }
369
+
370
+ export { resolveOrganizationConfig2, createOrganizationService2 };
@@ -0,0 +1,39 @@
1
+ import {
2
+ OrganizationError2
3
+ } from "./index-tsx842dx.js";
4
+
5
+ // src/policy.ts
6
+ function sameSubject2(a, b) {
7
+ return a.realmId === b.realmId && a.subjectId === b.subjectId;
8
+ }
9
+ function memberRelationshipPolicy2(subject, request) {
10
+ if (request.action === "organization.create")
11
+ return true;
12
+ if (!request.organization)
13
+ return false;
14
+ if (request.action === "invitation.accept")
15
+ return true;
16
+ const member = request.members.find((item) => sameSubject2(item.subject, subject));
17
+ if (!member)
18
+ return false;
19
+ if (request.action === "member.leave")
20
+ return true;
21
+ if (request.action === "organization.read" || request.action === "member.list")
22
+ return true;
23
+ if (request.action === "owner.transfer" || request.action === "owner.grant" || request.action === "member.role") {
24
+ return member.role === "owner";
25
+ }
26
+ if (request.target) {
27
+ const target = request.members.find((item) => sameSubject2(item.subject, request.target));
28
+ if (target && target.role !== "member" && member.role !== "owner")
29
+ return false;
30
+ }
31
+ return member.role === "owner" || member.role === "admin";
32
+ }
33
+ function preserveOwners(members) {
34
+ if (!members.some((member) => member.role === "owner")) {
35
+ throw new OrganizationError2("LAST_OWNER");
36
+ }
37
+ }
38
+
39
+ export { sameSubject2, memberRelationshipPolicy2, preserveOwners };
@@ -0,0 +1,15 @@
1
+ import {
2
+ OrganizationError2
3
+ } from "./index-tsx842dx.js";
4
+
5
+ // src/capacity.ts
6
+ var maximumSnapshotBytes = 512000;
7
+ function assertSnapshotCapacity(state) {
8
+ const bytes = new TextEncoder().encode(JSON.stringify(state)).byteLength;
9
+ const pending = state.invitations.filter((invite) => invite.status === "pending").length;
10
+ const headroom = pending * 12 + 16 - String(state.organization.updatedAt).length + 16 - String(state.organization.version).length;
11
+ if (bytes + headroom > maximumSnapshotBytes)
12
+ throw new OrganizationError2("CAPACITY");
13
+ }
14
+
15
+ export { assertSnapshotCapacity };
@@ -0,0 +1,11 @@
1
+ // src/contracts.ts
2
+ class OrganizationError2 extends Error {
3
+ code;
4
+ constructor(code) {
5
+ super(code);
6
+ this.code = code;
7
+ this.name = "OrganizationError";
8
+ }
9
+ }
10
+
11
+ export { OrganizationError2 };
@@ -0,0 +1,9 @@
1
+ // src/drizzle/schema-pg.ts
2
+ import { bigint, pgTable, text } from "drizzle-orm/pg-core";
3
+ var organizations2 = pgTable("organizations", {
4
+ id: text("id").primaryKey(),
5
+ version: bigint("version", { mode: "number" }).notNull(),
6
+ state: text("state").notNull()
7
+ });
8
+
9
+ export { organizations2 };
@@ -0,0 +1,4 @@
1
+ export * from "./contracts";
2
+ export { createOrganizationService, resolveOrganizationConfig } from "./service";
3
+ export type { OrganizationService } from "./service";
4
+ export { memberRelationshipPolicy, sameSubject } from "./policy";
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ import {
2
+ resolveOrganizationConfig2,
3
+ createOrganizationService2
4
+ } from "./index-ar1vwj4s.js";
5
+ import {
6
+ sameSubject2,
7
+ memberRelationshipPolicy2
8
+ } from "./index-kdetdk6w.js";
9
+ import {
10
+ OrganizationError2
11
+ } from "./index-tsx842dx.js";
12
+ export {
13
+ OrganizationError2 as OrganizationError,
14
+ createOrganizationService2 as createOrganizationService,
15
+ memberRelationshipPolicy2 as memberRelationshipPolicy,
16
+ resolveOrganizationConfig2 as resolveOrganizationConfig,
17
+ sameSubject2 as sameSubject
18
+ };
@@ -0,0 +1,11 @@
1
+ import { type Plugin } from "@lenso/core/plugin";
2
+ import type { OrganizationAccess, OrganizationConfig, OrganizationStore } from "./contracts";
3
+ import { type OrganizationService } from "./service";
4
+ /** Borrow exact database/access instances; their owners retain cleanup. */
5
+ export declare function createOrganizationPlugin<Database, Actor>(options: {
6
+ id: string;
7
+ database: Plugin<Database>;
8
+ access: Plugin<OrganizationAccess<Actor>>;
9
+ store(database: Database): OrganizationStore;
10
+ config?: Partial<OrganizationConfig>;
11
+ }): Plugin<OrganizationService<Actor>>;
package/dist/plugin.js ADDED
@@ -0,0 +1,22 @@
1
+ import {
2
+ createOrganizationService2
3
+ } from "./index-ar1vwj4s.js";
4
+
5
+ // src/plugin.ts
6
+ import { definePlugin } from "@lenso/core/plugin";
7
+ function createOrganizationPlugin(options) {
8
+ return definePlugin({
9
+ id: options.id,
10
+ requires: [options.database, options.access],
11
+ setup(context) {
12
+ return createOrganizationService2({
13
+ store: options.store(context.get(options.database)),
14
+ access: context.get(options.access),
15
+ config: options.config
16
+ });
17
+ }
18
+ });
19
+ }
20
+ export {
21
+ createOrganizationPlugin
22
+ };
@@ -0,0 +1,9 @@
1
+ import { type AccessRequest, type Member, type SubjectRef } from "./contracts";
2
+ export declare function sameSubject(a: SubjectRef, b: SubjectRef): boolean;
3
+ /**
4
+ * Opt-in business relationship policy, never Console/platform admission.
5
+ * Caller must authenticate the subject first. No role implies permission unless
6
+ * the application chooses this policy or another explicit authorization policy.
7
+ */
8
+ export declare function memberRelationshipPolicy(subject: SubjectRef, request: AccessRequest): boolean;
9
+ export declare function preserveOwners(members: readonly Member[]): void;
@@ -0,0 +1,86 @@
1
+ import { type Invitation, type Member, type MutationResult, type Organization, type OrganizationAccess, type OrganizationConfig, type OrganizationStore, type SubjectRef } from "./contracts";
2
+ export declare function resolveOrganizationConfig(input?: Partial<OrganizationConfig>): OrganizationConfig;
3
+ /**
4
+ * Ordinary async service. Stores/access providers are borrowed. Notification is
5
+ * deliberately outside this commit boundary: send only after createInvitation
6
+ * returns, and never log its returned bearer token.
7
+ */
8
+ export declare function createOrganizationService<Actor>(options: {
9
+ store: OrganizationStore;
10
+ access: OrganizationAccess<Actor>;
11
+ config?: Partial<OrganizationConfig>;
12
+ }): {
13
+ createOrganization(actor: Actor, input: {
14
+ name: string;
15
+ }): Promise<MutationResult<Organization>>;
16
+ readOrganization(actor: Actor, input: {
17
+ organizationId: string;
18
+ }): Promise<Organization>;
19
+ updateOrganization(actor: Actor, input: {
20
+ organizationId: string;
21
+ name: string;
22
+ }): Promise<MutationResult<Organization>>;
23
+ listMembers(actor: Actor, input: {
24
+ organizationId: string;
25
+ }): Promise<{
26
+ items: Member[];
27
+ total: number;
28
+ version: number;
29
+ }>;
30
+ addMember(actor: Actor, input: {
31
+ organizationId: string;
32
+ subject: SubjectRef;
33
+ }): Promise<MutationResult<Member>>;
34
+ leaveOrganization(actor: Actor, input: {
35
+ organizationId: string;
36
+ }): Promise<MutationResult<boolean>>;
37
+ removeMember(actor: Actor, input: {
38
+ organizationId: string;
39
+ subject: SubjectRef;
40
+ }): Promise<MutationResult<boolean>>;
41
+ setMemberRole(actor: Actor, input: {
42
+ organizationId: string;
43
+ subject: SubjectRef;
44
+ role: "member" | "admin";
45
+ }): Promise<MutationResult<Member>>;
46
+ grantOwner(actor: Actor, input: {
47
+ organizationId: string;
48
+ subject: SubjectRef;
49
+ }): Promise<MutationResult<Member>>;
50
+ transferOwner(actor: Actor, input: {
51
+ organizationId: string;
52
+ subject: SubjectRef;
53
+ }): Promise<MutationResult<Member>>;
54
+ createInvitation(actor: Actor, input: {
55
+ organizationId: string;
56
+ target: SubjectRef;
57
+ }): Promise<MutationResult<{
58
+ invitation: Invitation;
59
+ token: string;
60
+ }>>;
61
+ readInvitation(actor: Actor, input: {
62
+ organizationId: string;
63
+ invitationId: string;
64
+ }): Promise<Invitation>;
65
+ listInvitations(actor: Actor, input: {
66
+ organizationId: string;
67
+ }): Promise<{
68
+ items: Invitation[];
69
+ total: number;
70
+ version: number;
71
+ }>;
72
+ acceptInvitation(actor: Actor, input: {
73
+ organizationId: string;
74
+ invitationId: string;
75
+ token: string;
76
+ }): Promise<MutationResult<Member>>;
77
+ revokeInvitation(actor: Actor, input: {
78
+ organizationId: string;
79
+ invitationId: string;
80
+ }): Promise<MutationResult<Invitation>>;
81
+ pruneInvitations(actor: Actor, input: {
82
+ organizationId: string;
83
+ before: number;
84
+ }): Promise<MutationResult<number>>;
85
+ };
86
+ export type OrganizationService<Actor> = ReturnType<typeof createOrganizationService<Actor>>;
@@ -0,0 +1,5 @@
1
+ CREATE TABLE organizations (
2
+ id text PRIMARY KEY,
3
+ version bigint NOT NULL CHECK (version BETWEEN 1 AND 9007199254740991),
4
+ state text NOT NULL CHECK (jsonb_typeof(state::jsonb) = 'object')
5
+ );