@cat-factory/workspaces 0.6.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Igor Savin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ export { WorkspaceService, type WorkspaceServiceDependencies, } from './modules/workspaces/WorkspaceService.js';
2
+ export { AccountService, type AccountServiceDependencies, type AccountUser, } from './modules/accounts/AccountService.js';
3
+ export { UserService, type UserServiceDependencies, type IdentityProfile, } from './modules/users/UserService.js';
4
+ export { InvitationService, type InvitationServiceDependencies, type CreatedInvitation, } from './modules/invitations/InvitationService.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,gBAAgB,EAChB,KAAK,4BAA4B,GAClC,MAAM,0CAA0C,CAAA;AACjD,OAAO,EACL,cAAc,EACd,KAAK,0BAA0B,EAC/B,KAAK,WAAW,GACjB,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EACL,WAAW,EACX,KAAK,uBAAuB,EAC5B,KAAK,eAAe,GACrB,MAAM,gCAAgC,CAAA;AACvC,OAAO,EACL,iBAAiB,EACjB,KAAK,6BAA6B,EAClC,KAAK,iBAAiB,GACvB,MAAM,4CAA4C,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // Public surface of the tenancy base layer.
2
+ export { WorkspaceService, } from './modules/workspaces/WorkspaceService.js';
3
+ export { AccountService, } from './modules/accounts/AccountService.js';
4
+ export { UserService, } from './modules/users/UserService.js';
5
+ export { InvitationService, } from './modules/invitations/InvitationService.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAE5C,OAAO,EACL,gBAAgB,GAEjB,MAAM,0CAA0C,CAAA;AACjD,OAAO,EACL,cAAc,GAGf,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EACL,WAAW,GAGZ,MAAM,gCAAgC,CAAA;AACvC,OAAO,EACL,iBAAiB,GAGlB,MAAM,4CAA4C,CAAA"}
@@ -0,0 +1,68 @@
1
+ import type { Account, AccountMember, AccountRole, CreateAccountInput, UpdateAccountInput } from '@cat-factory/kernel';
2
+ import type { AccountRecord, AccountRepository, Membership, MembershipRepository, UserRepository } from '@cat-factory/kernel';
3
+ import type { Clock, IdGenerator } from '@cat-factory/kernel';
4
+ export interface AccountServiceDependencies {
5
+ accountRepository: AccountRepository;
6
+ membershipRepository: MembershipRepository;
7
+ idGenerator: IdGenerator;
8
+ clock: Clock;
9
+ /** Optional: resolve member display details (name/email/avatar) for the roster. */
10
+ userRepository?: UserRepository;
11
+ }
12
+ /** The signed-in identity the tenancy decisions are made against. */
13
+ export interface AccountUser {
14
+ /** Internal user id (`usr_*`). */
15
+ id: string;
16
+ /** GitHub login, when the user signed in via GitHub (else any display handle). */
17
+ login: string;
18
+ name: string | null;
19
+ }
20
+ export declare class AccountService {
21
+ private readonly deps;
22
+ constructor(deps: AccountServiceDependencies);
23
+ /**
24
+ * Ensure a user has a personal account (account-of-one) with an owner
25
+ * membership, creating it on first sign-in. Idempotent: keyed by the user's
26
+ * internal id, so repeated calls return the same account.
27
+ */
28
+ ensurePersonalAccount(user: AccountUser): Promise<AccountRecord>;
29
+ /** Create a shared org account; the creator becomes its first owner. */
30
+ createOrg(user: AccountUser, input: CreateAccountInput): Promise<Account>;
31
+ /**
32
+ * Every account the user can see and switch between (personal first), each
33
+ * annotated with the caller's role. Ensures the personal account exists.
34
+ */
35
+ listForUser(user: AccountUser): Promise<Account[]>;
36
+ /** The set of account ids a user belongs to (used to scope board visibility). */
37
+ accessibleAccountIds(userId: string): Promise<string[]>;
38
+ isMember(accountId: string, userId: string): Promise<boolean>;
39
+ /** Resolve the membership or throw 404 — the guard mutating routes use. */
40
+ requireMember(accountId: string, userId: string): Promise<Membership>;
41
+ /** A user's roles in an account (empty when not a member). */
42
+ rolesFor(accountId: string, userId: string): Promise<AccountRole[]>;
43
+ /** Whether a user holds a role in an account. */
44
+ hasRole(accountId: string, userId: string, role: AccountRole): Promise<boolean>;
45
+ /**
46
+ * Resolve the membership and require the `admin` role — the guard every
47
+ * org-account-modifying route uses (settings, members, invitations, account-scoped
48
+ * credentials). Throws 404 for a non-member, 409 for a member without `admin`.
49
+ */
50
+ requireAdmin(accountId: string, userId: string): Promise<Membership>;
51
+ get(accountId: string): Promise<AccountRecord | null>;
52
+ members(accountId: string): Promise<AccountMember[]>;
53
+ /**
54
+ * Update an account's settings (today: the default cloud provider new services
55
+ * inherit). Owner-only. Returns the updated wire account so the caller can patch
56
+ * its switcher in place.
57
+ */
58
+ updateSettings(accountId: string, actingUserId: string, input: UpdateAccountInput): Promise<Account>;
59
+ /**
60
+ * Add a member to an account. Only an admin may add, and only into an `org` account
61
+ * (a personal account stays an account-of-one). Defaults to the `developer` role.
62
+ */
63
+ addMember(accountId: string, actingUserId: string, userId: string, roles?: AccountRole[]): Promise<AccountMember>;
64
+ /** Set a member's role set (admin-only). The acting admin cannot drop their OWN admin. */
65
+ setMemberRoles(accountId: string, actingUserId: string, targetUserId: string, roles: AccountRole[]): Promise<AccountMember>;
66
+ private ensureMembership;
67
+ }
68
+ //# sourceMappingURL=AccountService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AccountService.d.ts","sourceRoot":"","sources":["../../../src/modules/accounts/AccountService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,201 @@
1
+ import { ConflictError, NotFoundError, ValidationError, assertFound } from '@cat-factory/kernel';
2
+ function toWire(account, roles) {
3
+ return {
4
+ id: account.id,
5
+ type: account.type,
6
+ name: account.name,
7
+ githubAccountLogin: account.githubAccountLogin,
8
+ createdAt: account.createdAt,
9
+ roles,
10
+ ...(account.defaultCloudProvider ? { defaultCloudProvider: account.defaultCloudProvider } : {}),
11
+ };
12
+ }
13
+ function toMember(m) {
14
+ return { accountId: m.accountId, userId: m.userId, roles: m.roles, createdAt: m.createdAt };
15
+ }
16
+ export class AccountService {
17
+ deps;
18
+ constructor(deps) {
19
+ this.deps = deps;
20
+ }
21
+ /**
22
+ * Ensure a user has a personal account (account-of-one) with an owner
23
+ * membership, creating it on first sign-in. Idempotent: keyed by the user's
24
+ * internal id, so repeated calls return the same account.
25
+ */
26
+ async ensurePersonalAccount(user) {
27
+ const existing = await this.deps.accountRepository.findPersonalByUser(user.id);
28
+ if (existing) {
29
+ // Self-heal a missing admin membership (e.g. partially-applied prior run).
30
+ await this.ensureMembership(existing.id, user.id, ['admin']);
31
+ return existing;
32
+ }
33
+ const account = {
34
+ id: this.deps.idGenerator.next('acc'),
35
+ type: 'personal',
36
+ name: user.name?.trim() || user.login,
37
+ githubAccountLogin: user.login,
38
+ ownerUserId: user.id,
39
+ createdAt: this.deps.clock.now(),
40
+ };
41
+ await this.deps.accountRepository.create(account);
42
+ await this.ensureMembership(account.id, user.id, ['admin']);
43
+ return account;
44
+ }
45
+ /** Create a shared org account; the creator becomes its first owner. */
46
+ async createOrg(user, input) {
47
+ const account = {
48
+ id: this.deps.idGenerator.next('acc'),
49
+ type: 'org',
50
+ name: input.name.trim(),
51
+ githubAccountLogin: input.githubAccountLogin?.trim() || null,
52
+ ownerUserId: null,
53
+ createdAt: this.deps.clock.now(),
54
+ };
55
+ await this.deps.accountRepository.create(account);
56
+ await this.ensureMembership(account.id, user.id, ['admin']);
57
+ return toWire(account, ['admin']);
58
+ }
59
+ /**
60
+ * Every account the user can see and switch between (personal first), each
61
+ * annotated with the caller's role. Ensures the personal account exists.
62
+ */
63
+ async listForUser(user) {
64
+ await this.ensurePersonalAccount(user);
65
+ const memberships = await this.deps.membershipRepository.listByUser(user.id);
66
+ const accounts = [];
67
+ for (const m of memberships) {
68
+ const account = await this.deps.accountRepository.get(m.accountId);
69
+ if (account)
70
+ accounts.push(toWire(account, m.roles));
71
+ }
72
+ // Personal accounts first, then orgs, each alphabetical — a stable switcher order.
73
+ return accounts.sort((a, b) => (a.type === 'personal' ? 0 : 1) - (b.type === 'personal' ? 0 : 1) ||
74
+ a.name.localeCompare(b.name));
75
+ }
76
+ /** The set of account ids a user belongs to (used to scope board visibility). */
77
+ async accessibleAccountIds(userId) {
78
+ const memberships = await this.deps.membershipRepository.listByUser(userId);
79
+ return memberships.map((m) => m.accountId);
80
+ }
81
+ async isMember(accountId, userId) {
82
+ return (await this.deps.membershipRepository.get(accountId, userId)) !== null;
83
+ }
84
+ /** Resolve the membership or throw 404 — the guard mutating routes use. */
85
+ async requireMember(accountId, userId) {
86
+ const membership = await this.deps.membershipRepository.get(accountId, userId);
87
+ if (!membership)
88
+ throw new NotFoundError('Account', accountId);
89
+ return membership;
90
+ }
91
+ /** A user's roles in an account (empty when not a member). */
92
+ async rolesFor(accountId, userId) {
93
+ const membership = await this.deps.membershipRepository.get(accountId, userId);
94
+ return membership?.roles ?? [];
95
+ }
96
+ /** Whether a user holds a role in an account. */
97
+ async hasRole(accountId, userId, role) {
98
+ return (await this.rolesFor(accountId, userId)).includes(role);
99
+ }
100
+ /**
101
+ * Resolve the membership and require the `admin` role — the guard every
102
+ * org-account-modifying route uses (settings, members, invitations, account-scoped
103
+ * credentials). Throws 404 for a non-member, 409 for a member without `admin`.
104
+ */
105
+ async requireAdmin(accountId, userId) {
106
+ const membership = await this.requireMember(accountId, userId);
107
+ if (!membership.roles.includes('admin')) {
108
+ throw new ConflictError('Only an account admin can modify the organization account');
109
+ }
110
+ return membership;
111
+ }
112
+ get(accountId) {
113
+ return this.deps.accountRepository.get(accountId);
114
+ }
115
+ async members(accountId) {
116
+ const list = await this.deps.membershipRepository.listByAccount(accountId);
117
+ const users = this.deps.userRepository;
118
+ if (!users)
119
+ return list.map(toMember);
120
+ // Enrich the roster with each member's display details for the UI — one bulk load
121
+ // rather than a query per member.
122
+ const records = await users.listByIds(list.map((m) => m.userId));
123
+ const byId = new Map(records.map((u) => [u.id, u]));
124
+ return list.map((m) => {
125
+ const user = byId.get(m.userId);
126
+ return {
127
+ ...toMember(m),
128
+ name: user?.name ?? null,
129
+ email: user?.email ?? null,
130
+ avatarUrl: user?.avatarUrl ?? null,
131
+ };
132
+ });
133
+ }
134
+ /**
135
+ * Update an account's settings (today: the default cloud provider new services
136
+ * inherit). Owner-only. Returns the updated wire account so the caller can patch
137
+ * its switcher in place.
138
+ */
139
+ async updateSettings(accountId, actingUserId, input) {
140
+ const acting = await this.requireAdmin(accountId, actingUserId);
141
+ // An explicit key (even `undefined`) means "clear"; an absent key leaves it.
142
+ if ('defaultCloudProvider' in input) {
143
+ await this.deps.accountRepository.updateSettings(accountId, {
144
+ defaultCloudProvider: input.defaultCloudProvider ?? null,
145
+ });
146
+ }
147
+ const account = assertFound(await this.deps.accountRepository.get(accountId), 'Account', accountId);
148
+ return toWire(account, acting.roles);
149
+ }
150
+ /**
151
+ * Add a member to an account. Only an admin may add, and only into an `org` account
152
+ * (a personal account stays an account-of-one). Defaults to the `developer` role.
153
+ */
154
+ async addMember(accountId, actingUserId, userId, roles = ['developer']) {
155
+ // Authorize first so a non-admin/non-member can't probe an account's existence or
156
+ // type (404 before any ValidationError leak).
157
+ await this.requireAdmin(accountId, actingUserId);
158
+ const account = assertFound(await this.deps.accountRepository.get(accountId), 'Account', accountId);
159
+ if (account.type === 'personal') {
160
+ throw new ValidationError('Cannot add members to a personal account');
161
+ }
162
+ const membership = {
163
+ accountId,
164
+ userId,
165
+ roles: normalizeRoles(roles),
166
+ createdAt: this.deps.clock.now(),
167
+ };
168
+ await this.deps.membershipRepository.upsert(membership);
169
+ return toMember(membership);
170
+ }
171
+ /** Set a member's role set (admin-only). The acting admin cannot drop their OWN admin. */
172
+ async setMemberRoles(accountId, actingUserId, targetUserId, roles) {
173
+ await this.requireAdmin(accountId, actingUserId);
174
+ const target = await this.requireMember(accountId, targetUserId);
175
+ const next = normalizeRoles(roles);
176
+ if (actingUserId === targetUserId && !next.includes('admin')) {
177
+ throw new ConflictError('You cannot remove your own admin role');
178
+ }
179
+ const membership = { ...target, roles: next };
180
+ await this.deps.membershipRepository.upsert(membership);
181
+ return toMember(membership);
182
+ }
183
+ async ensureMembership(accountId, userId, roles) {
184
+ if (await this.deps.membershipRepository.get(accountId, userId))
185
+ return;
186
+ await this.deps.membershipRepository.upsert({
187
+ accountId,
188
+ userId,
189
+ roles: normalizeRoles(roles),
190
+ createdAt: this.deps.clock.now(),
191
+ });
192
+ }
193
+ }
194
+ /** De-duplicate a role set, preserving order, defaulting to `developer` when empty. */
195
+ function normalizeRoles(roles) {
196
+ const seen = new Set();
197
+ for (const r of roles)
198
+ seen.add(r);
199
+ return seen.size > 0 ? [...seen] : ['developer'];
200
+ }
201
+ //# sourceMappingURL=AccountService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AccountService.js","sourceRoot":"","sources":["../../../src/modules/accounts/AccountService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ import type { AccountInvitation, AccountRole } from '@cat-factory/contracts';
2
+ import type { AccountInvitationRecord, AccountInvitationRepository, AccountRepository, Clock, EmailSender, IdGenerator, MembershipRepository } from '@cat-factory/kernel';
3
+ export interface InvitationServiceDependencies {
4
+ invitationRepository: AccountInvitationRepository;
5
+ accountRepository: AccountRepository;
6
+ membershipRepository: MembershipRepository;
7
+ idGenerator: IdGenerator;
8
+ clock: Clock;
9
+ /**
10
+ * Optional: resolve the account's configured (DB-stored, per-account) email sender
11
+ * at send time. Absent or returning null ⇒ the accept link is returned for manual
12
+ * sharing instead of being emailed.
13
+ */
14
+ resolveEmailSender?: (accountId: string) => Promise<EmailSender | null>;
15
+ /** Base URL the accept link points at (the SPA origin). */
16
+ appBaseUrl?: string;
17
+ }
18
+ export interface CreatedInvitation {
19
+ invitation: AccountInvitation;
20
+ /** The raw accept token + link (only available at creation; never re-derivable). */
21
+ token: string;
22
+ acceptUrl: string | null;
23
+ /** Whether the invite email was actually sent (vs. needing manual link sharing). */
24
+ emailed: boolean;
25
+ }
26
+ export declare class InvitationService {
27
+ private readonly deps;
28
+ constructor(deps: InvitationServiceDependencies);
29
+ /** Invite a teammate by email. Admin-only, org accounts only. */
30
+ invite(accountId: string, actingUserId: string, email: string, roles?: AccountRole[]): Promise<CreatedInvitation>;
31
+ list(accountId: string): Promise<AccountInvitation[]>;
32
+ /** Revoke a pending invitation (admin-only). */
33
+ revoke(accountId: string, actingUserId: string, invitationId: string): Promise<void>;
34
+ /**
35
+ * Peek at a pending invitation by token (no side effects) — lets the SPA show the
36
+ * org name on the accept screen and decide whether the user must sign up first.
37
+ */
38
+ peek(token: string): Promise<AccountInvitationRecord | null>;
39
+ /**
40
+ * Redeem an invitation: grant the user membership in the org and mark it accepted.
41
+ * Returns the account id joined.
42
+ *
43
+ * The redemption is bound to the invited email: the accepting user's verified email
44
+ * must match the address the invite was sent to. This stops a leaked accept link from
45
+ * admitting an arbitrary account (the invite token also short-circuits the sign-in
46
+ * allowlist, so without this binding any leaked link would be a private-deployment
47
+ * bypass). A user with no known email cannot redeem (fail closed).
48
+ */
49
+ accept(token: string, userId: string, userEmail: string | null): Promise<string>;
50
+ }
51
+ //# sourceMappingURL=InvitationService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InvitationService.d.ts","sourceRoot":"","sources":["../../../src/modules/invitations/InvitationService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAC5E,OAAO,KAAK,EACV,uBAAuB,EACvB,2BAA2B,EAC3B,iBAAiB,EACjB,KAAK,EACL,WAAW,EACX,WAAW,EAEX,oBAAoB,EACrB,MAAM,qBAAqB,CAAA;AAa5B,MAAM,WAAW,6BAA6B;IAC5C,oBAAoB,EAAE,2BAA2B,CAAA;IACjD,iBAAiB,EAAE,iBAAiB,CAAA;IACpC,oBAAoB,EAAE,oBAAoB,CAAA;IAC1C,WAAW,EAAE,WAAW,CAAA;IACxB,KAAK,EAAE,KAAK,CAAA;IACZ;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IACvE,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAqBD,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,iBAAiB,CAAA;IAC7B,oFAAoF;IACpF,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,oFAAoF;IACpF,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,qBAAa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,6BAA6B,EAAI;IAEpE,iEAAiE;IAC3D,MAAM,CACV,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,WAAW,EAAkB,GACnC,OAAO,CAAC,iBAAiB,CAAC,CAuD5B;IAED,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAEpD;IAED,gDAAgD;IAC1C,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYzF;IAED;;;OAGG;IACG,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAKjE;IAED;;;;;;;;;OASG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAoBrF;CACF"}
@@ -0,0 +1,154 @@
1
+ import { ConflictError, NotFoundError, ValidationError, assertFound } from '@cat-factory/kernel';
2
+ // ---------------------------------------------------------------------------
3
+ // InvitationService: invite teammates into an org account by email. An owner mints
4
+ // an invitation; the invitee redeems an opaque token (delivered by email) to gain
5
+ // membership. Only the token's SHA-256 hash is stored — the raw token lives only in
6
+ // the emailed accept link. Redeeming for a brand-new email is what lets a person
7
+ // without a GitHub account join (their user is created by the auth signup path,
8
+ // then this grants the org membership).
9
+ // ---------------------------------------------------------------------------
10
+ const INVITE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
11
+ /** SHA-256 hex digest — Web Crypto, runs on both runtimes. */
12
+ async function sha256Hex(input) {
13
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));
14
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
15
+ }
16
+ function toWire(record) {
17
+ return {
18
+ id: record.id,
19
+ accountId: record.accountId,
20
+ email: record.email,
21
+ roles: record.roles,
22
+ status: record.status,
23
+ invitedBy: record.invitedBy,
24
+ expiresAt: record.expiresAt,
25
+ createdAt: record.createdAt,
26
+ };
27
+ }
28
+ export class InvitationService {
29
+ deps;
30
+ constructor(deps) {
31
+ this.deps = deps;
32
+ }
33
+ /** Invite a teammate by email. Admin-only, org accounts only. */
34
+ async invite(accountId, actingUserId, email, roles = ['developer']) {
35
+ const account = assertFound(await this.deps.accountRepository.get(accountId), 'Account', accountId);
36
+ if (account.type === 'personal') {
37
+ throw new ValidationError('Cannot invite members to a personal account');
38
+ }
39
+ const acting = await this.deps.membershipRepository.get(accountId, actingUserId);
40
+ if (!acting)
41
+ throw new NotFoundError('Account', accountId);
42
+ if (!acting.roles.includes('admin')) {
43
+ throw new ConflictError('Only an account admin can invite members');
44
+ }
45
+ const normalizedEmail = email.toLowerCase().trim();
46
+ // Supersede any still-pending invite to the same address in this account, so only
47
+ // the freshly-minted token stays live (no pile-up of redeemable links per email).
48
+ const pending = await this.deps.invitationRepository.listByAccount(accountId);
49
+ for (const prior of pending) {
50
+ if (prior.status === 'pending' && prior.email === normalizedEmail) {
51
+ await this.deps.invitationRepository.setStatus(prior.id, 'revoked');
52
+ }
53
+ }
54
+ const token = `${crypto.randomUUID()}${crypto.randomUUID()}`.replace(/-/g, '');
55
+ const record = {
56
+ id: this.deps.idGenerator.next('inv'),
57
+ accountId,
58
+ email: normalizedEmail,
59
+ roles: roles.length > 0 ? roles : ['developer'],
60
+ tokenHash: await sha256Hex(token),
61
+ invitedBy: actingUserId,
62
+ status: 'pending',
63
+ expiresAt: this.deps.clock.now() + INVITE_TTL_MS,
64
+ createdAt: this.deps.clock.now(),
65
+ };
66
+ await this.deps.invitationRepository.create(record);
67
+ const acceptUrl = this.deps.appBaseUrl
68
+ ? `${this.deps.appBaseUrl.replace(/\/$/, '')}/invite?token=${token}`
69
+ : null;
70
+ const sender = this.deps.resolveEmailSender
71
+ ? await this.deps.resolveEmailSender(accountId)
72
+ : null;
73
+ let emailed = false;
74
+ if (sender && acceptUrl) {
75
+ await sender.send({
76
+ to: normalizedEmail,
77
+ subject: `You've been invited to ${account.name} on Cat Factory`,
78
+ text: `You've been invited to join ${account.name}. Accept: ${acceptUrl}`,
79
+ html: invitationEmailHtml(account.name, acceptUrl),
80
+ });
81
+ emailed = true;
82
+ }
83
+ return { invitation: toWire(record), token, acceptUrl, emailed };
84
+ }
85
+ list(accountId) {
86
+ return this.deps.invitationRepository.listByAccount(accountId).then((rows) => rows.map(toWire));
87
+ }
88
+ /** Revoke a pending invitation (admin-only). */
89
+ async revoke(accountId, actingUserId, invitationId) {
90
+ const acting = await this.deps.membershipRepository.get(accountId, actingUserId);
91
+ if (!acting?.roles.includes('admin')) {
92
+ throw new ConflictError('Only an account admin can revoke invitations');
93
+ }
94
+ const invitation = assertFound(await this.deps.invitationRepository.get(invitationId), 'Invitation', invitationId);
95
+ if (invitation.accountId !== accountId)
96
+ throw new NotFoundError('Invitation', invitationId);
97
+ await this.deps.invitationRepository.setStatus(invitationId, 'revoked');
98
+ }
99
+ /**
100
+ * Peek at a pending invitation by token (no side effects) — lets the SPA show the
101
+ * org name on the accept screen and decide whether the user must sign up first.
102
+ */
103
+ async peek(token) {
104
+ const record = await this.deps.invitationRepository.findByTokenHash(await sha256Hex(token));
105
+ if (!record || record.status !== 'pending')
106
+ return null;
107
+ if (record.expiresAt < this.deps.clock.now())
108
+ return null;
109
+ return record;
110
+ }
111
+ /**
112
+ * Redeem an invitation: grant the user membership in the org and mark it accepted.
113
+ * Returns the account id joined.
114
+ *
115
+ * The redemption is bound to the invited email: the accepting user's verified email
116
+ * must match the address the invite was sent to. This stops a leaked accept link from
117
+ * admitting an arbitrary account (the invite token also short-circuits the sign-in
118
+ * allowlist, so without this binding any leaked link would be a private-deployment
119
+ * bypass). A user with no known email cannot redeem (fail closed).
120
+ */
121
+ async accept(token, userId, userEmail) {
122
+ const record = await this.deps.invitationRepository.findByTokenHash(await sha256Hex(token));
123
+ if (!record || record.status !== 'pending') {
124
+ throw new NotFoundError('Invitation', 'token');
125
+ }
126
+ if (record.expiresAt < this.deps.clock.now()) {
127
+ throw new ConflictError('This invitation has expired');
128
+ }
129
+ if (!userEmail || userEmail.toLowerCase().trim() !== record.email) {
130
+ throw new ConflictError('This invitation was sent to a different email address');
131
+ }
132
+ const membership = {
133
+ accountId: record.accountId,
134
+ userId,
135
+ roles: record.roles,
136
+ createdAt: this.deps.clock.now(),
137
+ };
138
+ await this.deps.membershipRepository.upsert(membership);
139
+ await this.deps.invitationRepository.setStatus(record.id, 'accepted');
140
+ return record.accountId;
141
+ }
142
+ }
143
+ function invitationEmailHtml(accountName, acceptUrl) {
144
+ return `<!doctype html><html><body style="font-family:sans-serif">
145
+ <h2>You've been invited to ${escapeHtml(accountName)}</h2>
146
+ <p>You've been invited to collaborate on <strong>${escapeHtml(accountName)}</strong> in Cat Factory.</p>
147
+ <p><a href="${acceptUrl}" style="display:inline-block;padding:10px 18px;background:#111;color:#fff;border-radius:6px;text-decoration:none">Accept invitation</a></p>
148
+ <p>Or paste this link into your browser:<br>${acceptUrl}</p>
149
+ </body></html>`;
150
+ }
151
+ function escapeHtml(s) {
152
+ return s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
153
+ }
154
+ //# sourceMappingURL=InvitationService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InvitationService.js","sourceRoot":"","sources":["../../../src/modules/invitations/InvitationService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ import type { Clock, IdGenerator, IdentityProvider, PasswordHasher, UserIdentityRecord, UserRecord, UserRepository } from '@cat-factory/kernel';
2
+ export interface UserServiceDependencies {
3
+ userRepository: UserRepository;
4
+ passwordHasher: PasswordHasher;
5
+ idGenerator: IdGenerator;
6
+ clock: Clock;
7
+ }
8
+ /** Profile details captured from an external identity provider. */
9
+ export interface IdentityProfile {
10
+ name?: string | null;
11
+ email?: string | null;
12
+ avatarUrl?: string | null;
13
+ /**
14
+ * Whether the provider has verified the user owns `email`. Only a verified email is
15
+ * trusted to link this identity onto an existing same-email user (else two people
16
+ * who happen to share an email could merge into one account).
17
+ */
18
+ emailVerified?: boolean;
19
+ /** Provider-specific extras to persist as identity metadata (e.g. github login). */
20
+ metadata?: Record<string, unknown>;
21
+ }
22
+ export declare class UserService {
23
+ private readonly deps;
24
+ constructor(deps: UserServiceDependencies);
25
+ get(id: string): Promise<UserRecord | null>;
26
+ /** The user behind an external identity, or null (no side effects). */
27
+ findByIdentity(provider: IdentityProvider, subject: string): Promise<UserRecord | null>;
28
+ /** Every linked login identity for a user (the account-settings "connected logins"). */
29
+ listIdentities(userId: string): Promise<UserIdentityRecord[]>;
30
+ /**
31
+ * Resolve the user behind an external identity, creating the user + linking the
32
+ * identity on first sight. Idempotent on `(provider, subject)`: repeated logins
33
+ * return the same `usr_*` id. Used by all GitHub/Google login paths.
34
+ */
35
+ findOrCreateByIdentity(provider: IdentityProvider, subject: string, profile?: IdentityProfile): Promise<UserRecord>;
36
+ /**
37
+ * Whether a user's primary email was proven by an OAuth provider rather than a
38
+ * self-asserted password signup. `users.email` is only ever written by (a) a
39
+ * verified OAuth create/link or (b) a `password` signup; password is the sole
40
+ * unverified writer. So "owns an email AND has a non-password identity" is a sound
41
+ * proxy for "email is provider-verified" — used to refuse merging a verified login
42
+ * onto a squatted, password-only account.
43
+ */
44
+ private emailIsProviderVerified;
45
+ /** Link an additional identity to an existing user (onboarding follow-up). */
46
+ linkIdentity(userId: string, provider: IdentityProvider, subject: string, profile?: IdentityProfile): Promise<void>;
47
+ /**
48
+ * Register a new email/password user. The email is the password identity's
49
+ * subject; rejects when one already exists. Returns the created user.
50
+ */
51
+ signupWithPassword(input: {
52
+ email: string;
53
+ password: string;
54
+ name?: string | null;
55
+ }): Promise<UserRecord>;
56
+ /**
57
+ * Verify an email/password login, returning the user on success or null on a bad
58
+ * email/password (the caller maps null → 401, never leaking which was wrong).
59
+ */
60
+ verifyPassword(input: {
61
+ email: string;
62
+ password: string;
63
+ }): Promise<UserRecord | null>;
64
+ }
65
+ //# sourceMappingURL=UserService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserService.d.ts","sourceRoot":"","sources":["../../../src/modules/users/UserService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,KAAK,EACL,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,UAAU,EACV,cAAc,EACf,MAAM,qBAAqB,CAAA;AAU5B,MAAM,WAAW,uBAAuB;IACtC,cAAc,EAAE,cAAc,CAAA;IAC9B,cAAc,EAAE,cAAc,CAAA;IAC9B,WAAW,EAAE,WAAW,CAAA;IACxB,KAAK,EAAE,KAAK,CAAA;CACb;AAED,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAcD,qBAAa,WAAW;IACV,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,uBAAuB,EAAI;IAE9D,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAE1C;IAED,uEAAuE;IACvE,cAAc,CAAC,QAAQ,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAEtF;IAED,wFAAwF;IACxF,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAE5D;IAED;;;;OAIG;IACG,sBAAsB,CAC1B,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,UAAU,CAAC,CAwDrB;IAED;;;;;;;OAOG;YACW,uBAAuB;IAKrC,8EAA8E;IACxE,YAAY,CAChB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CASf;IAED;;;OAGG;IACG,kBAAkB,CAAC,KAAK,EAAE;QAC9B,KAAK,EAAE,MAAM,CAAA;QACb,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KACrB,GAAG,OAAO,CAAC,UAAU,CAAC,CA8BtB;IAED;;;OAGG;IACG,cAAc,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAqB3F;CACF"}
@@ -0,0 +1,175 @@
1
+ import { ConflictError, ValidationError } from '@cat-factory/kernel';
2
+ // A dummy PHC hash the password verify path runs against when no password identity
3
+ // exists, so a miss costs the same PBKDF2 work as a hit and response time can't be used
4
+ // to enumerate which emails are registered. It is computed once per process from the
5
+ // REAL hasher (over a random, unguessable input) so its cost always tracks the hasher's
6
+ // current iteration count — a hardcoded string would silently drift if the default cost
7
+ // were raised, reopening the timing oracle. Cached process-wide (not per instance) so
8
+ // every miss pays exactly one PBKDF2, matching the single derivation a hit performs.
9
+ let dummyHashPromise;
10
+ function dummyPasswordHash(hasher) {
11
+ return (dummyHashPromise ??= hasher.hash(crypto.randomUUID()));
12
+ }
13
+ export class UserService {
14
+ deps;
15
+ constructor(deps) {
16
+ this.deps = deps;
17
+ }
18
+ get(id) {
19
+ return this.deps.userRepository.get(id);
20
+ }
21
+ /** The user behind an external identity, or null (no side effects). */
22
+ findByIdentity(provider, subject) {
23
+ return this.deps.userRepository.findByIdentity(provider, subject);
24
+ }
25
+ /** Every linked login identity for a user (the account-settings "connected logins"). */
26
+ listIdentities(userId) {
27
+ return this.deps.userRepository.listIdentities(userId);
28
+ }
29
+ /**
30
+ * Resolve the user behind an external identity, creating the user + linking the
31
+ * identity on first sight. Idempotent on `(provider, subject)`: repeated logins
32
+ * return the same `usr_*` id. Used by all GitHub/Google login paths.
33
+ */
34
+ async findOrCreateByIdentity(provider, subject, profile = {}) {
35
+ const existing = await this.deps.userRepository.findByIdentity(provider, subject);
36
+ if (existing)
37
+ return existing;
38
+ let email = profile.email?.toLowerCase().trim() || null;
39
+ // Is this email already owned by another user? (Unique-index-safe handling below.)
40
+ const emailOwner = email ? await this.deps.userRepository.findByEmail(email) : null;
41
+ if (emailOwner) {
42
+ if (profile.emailVerified && (await this.emailIsProviderVerified(emailOwner.id))) {
43
+ // A second login provider for the same person — attach this identity to the
44
+ // existing same-email user instead of creating a duplicate (which would collide
45
+ // on the unique email index and 500). Only safe when the existing owner's email
46
+ // was itself proven by an OAuth provider, never by a self-asserted signup.
47
+ await this.deps.userRepository.linkIdentity({
48
+ userId: emailOwner.id,
49
+ provider,
50
+ subject,
51
+ secret: null,
52
+ metadata: profile.metadata ? JSON.stringify(profile.metadata) : null,
53
+ createdAt: this.deps.clock.now(),
54
+ });
55
+ return emailOwner;
56
+ }
57
+ if (profile.emailVerified) {
58
+ // The email is owned only via an UNVERIFIED password signup (the owner has no
59
+ // OAuth identity). Password signup never proves email ownership, so that claim
60
+ // can't block the genuinely-verified party from this provider. Release the email
61
+ // from the squatting account and let the verified login take it on a fresh user.
62
+ // (The squatter keeps its now-emailless, password-only account; acceptable per
63
+ // the pre-1.0 policy — and it stops a pre-registration account-hijack.)
64
+ await this.deps.userRepository.update(emailOwner.id, { email: null });
65
+ }
66
+ else {
67
+ // Unverified and the email belongs to someone else: never trusted to claim or
68
+ // merge it, and storing it would collide on the unique index. Create a distinct
69
+ // user with no email rather than failing the login.
70
+ email = null;
71
+ }
72
+ }
73
+ const user = {
74
+ id: this.deps.idGenerator.next('usr'),
75
+ name: profile.name?.trim() || null,
76
+ email,
77
+ avatarUrl: profile.avatarUrl || null,
78
+ createdAt: this.deps.clock.now(),
79
+ };
80
+ await this.deps.userRepository.create(user);
81
+ await this.deps.userRepository.linkIdentity({
82
+ userId: user.id,
83
+ provider,
84
+ subject,
85
+ secret: null,
86
+ metadata: profile.metadata ? JSON.stringify(profile.metadata) : null,
87
+ createdAt: this.deps.clock.now(),
88
+ });
89
+ return user;
90
+ }
91
+ /**
92
+ * Whether a user's primary email was proven by an OAuth provider rather than a
93
+ * self-asserted password signup. `users.email` is only ever written by (a) a
94
+ * verified OAuth create/link or (b) a `password` signup; password is the sole
95
+ * unverified writer. So "owns an email AND has a non-password identity" is a sound
96
+ * proxy for "email is provider-verified" — used to refuse merging a verified login
97
+ * onto a squatted, password-only account.
98
+ */
99
+ async emailIsProviderVerified(userId) {
100
+ const identities = await this.deps.userRepository.listIdentities(userId);
101
+ return identities.some((i) => i.provider !== 'password');
102
+ }
103
+ /** Link an additional identity to an existing user (onboarding follow-up). */
104
+ async linkIdentity(userId, provider, subject, profile = {}) {
105
+ await this.deps.userRepository.linkIdentity({
106
+ userId,
107
+ provider,
108
+ subject,
109
+ secret: null,
110
+ metadata: profile.metadata ? JSON.stringify(profile.metadata) : null,
111
+ createdAt: this.deps.clock.now(),
112
+ });
113
+ }
114
+ /**
115
+ * Register a new email/password user. The email is the password identity's
116
+ * subject; rejects when one already exists. Returns the created user.
117
+ */
118
+ async signupWithPassword(input) {
119
+ const email = input.email.toLowerCase().trim();
120
+ if (!email)
121
+ throw new ValidationError('Email is required');
122
+ if (input.password.length < 8) {
123
+ throw new ValidationError('Password must be at least 8 characters');
124
+ }
125
+ // Reject if ANY user already owns this email (not just a prior password identity):
126
+ // attaching a password to an existing OAuth-only account via an unauthenticated
127
+ // signup would be account takeover, and a duplicate would collide on the unique
128
+ // email index. The owner can add a password later from an authenticated session.
129
+ if (await this.deps.userRepository.findByEmail(email)) {
130
+ throw new ConflictError('An account with that email already exists');
131
+ }
132
+ const user = {
133
+ id: this.deps.idGenerator.next('usr'),
134
+ name: input.name?.trim() || null,
135
+ email,
136
+ avatarUrl: null,
137
+ createdAt: this.deps.clock.now(),
138
+ };
139
+ await this.deps.userRepository.create(user);
140
+ await this.deps.userRepository.linkIdentity({
141
+ userId: user.id,
142
+ provider: 'password',
143
+ subject: email,
144
+ secret: await this.deps.passwordHasher.hash(input.password),
145
+ metadata: null,
146
+ createdAt: this.deps.clock.now(),
147
+ });
148
+ return user;
149
+ }
150
+ /**
151
+ * Verify an email/password login, returning the user on success or null on a bad
152
+ * email/password (the caller maps null → 401, never leaking which was wrong).
153
+ */
154
+ async verifyPassword(input) {
155
+ const email = input.email.toLowerCase().trim();
156
+ const identity = await this.deps.userRepository.getIdentity('password', email);
157
+ if (!identity?.secret) {
158
+ // Equalise timing with the hit path so the response time can't reveal whether the
159
+ // email is registered (PBKDF2 against a dummy hash that can never match).
160
+ await this.deps.passwordHasher.verify(input.password, await dummyPasswordHash(this.deps.passwordHasher));
161
+ return null;
162
+ }
163
+ if (!(await this.deps.passwordHasher.verify(input.password, identity.secret)))
164
+ return null;
165
+ // Transparently upgrade a weaker-cost hash now that we hold the plaintext.
166
+ if (this.deps.passwordHasher.needsRehash(identity.secret)) {
167
+ await this.deps.userRepository.linkIdentity({
168
+ ...identity,
169
+ secret: await this.deps.passwordHasher.hash(input.password),
170
+ });
171
+ }
172
+ return this.deps.userRepository.get(identity.userId);
173
+ }
174
+ }
175
+ //# sourceMappingURL=UserService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserService.js","sourceRoot":"","sources":["../../../src/modules/users/UserService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ import type { CreateWorkspaceInput } from '@cat-factory/contracts';
2
+ import type { Workspace, WorkspaceSnapshot } from '@cat-factory/kernel';
3
+ import type { BlockRepository, ExecutionRepository, PipelineRepository, ServiceRepository, WorkspaceMountRepository, WorkspaceRepository, WorkspaceVisibility } from '@cat-factory/kernel';
4
+ import type { Clock, IdGenerator } from '@cat-factory/kernel';
5
+ export { requireWorkspace } from '@cat-factory/kernel';
6
+ export interface WorkspaceServiceDependencies {
7
+ workspaceRepository: WorkspaceRepository;
8
+ blockRepository: BlockRepository;
9
+ pipelineRepository: PipelineRepository;
10
+ executionRepository: ExecutionRepository;
11
+ idGenerator: IdGenerator;
12
+ clock: Clock;
13
+ /**
14
+ * In-org shared services. When wired, a board snapshot is composed from the services
15
+ * the workspace mounts: its own frames plus any service mounted from another
16
+ * workspace in the same org, with each frame's layout taken from its mount.
17
+ */
18
+ serviceRepository?: ServiceRepository;
19
+ workspaceMountRepository?: WorkspaceMountRepository;
20
+ }
21
+ /** Creates, reads and deletes boards (workspaces) and assembles snapshots. */
22
+ export declare class WorkspaceService {
23
+ private readonly workspaceRepository;
24
+ private readonly blockRepository;
25
+ private readonly pipelineRepository;
26
+ private readonly executionRepository;
27
+ private readonly idGenerator;
28
+ private readonly clock;
29
+ private readonly serviceRepository?;
30
+ private readonly workspaceMountRepository?;
31
+ constructor({ workspaceRepository, blockRepository, pipelineRepository, executionRepository, idGenerator, clock, serviceRepository, workspaceMountRepository, }: WorkspaceServiceDependencies);
32
+ /**
33
+ * Boards visible to a user (see {@link WorkspaceVisibility}). A `null` scope
34
+ * means auth is disabled and all boards are returned.
35
+ */
36
+ list(scope: WorkspaceVisibility): Promise<Workspace[]>;
37
+ /** Owning user id for a board (string/owned, null/none, undefined/missing). */
38
+ ownerOf(id: string): Promise<string | null | undefined>;
39
+ /** Owning account id for a board (string/scoped, null/legacy, undefined/missing). */
40
+ accountOf(id: string): Promise<string | null | undefined>;
41
+ create(input: CreateWorkspaceInput, ownerUserId: string | null, accountId: string | null): Promise<WorkspaceSnapshot>;
42
+ /**
43
+ * Seed the demo architecture, registering each top-level frame as an account-owned service
44
+ * (so seeded frames are shareable across the org exactly like ones created on the board) and
45
+ * stamping every seeded block with its frame's service. A no-op service registration when
46
+ * in-org sharing isn't wired leaves plain workspace-local blocks (legacy behaviour).
47
+ */
48
+ private seedBoard;
49
+ /** Rename a board and/or update its description. */
50
+ update(id: string, patch: {
51
+ name?: string;
52
+ description?: string | null;
53
+ }): Promise<Workspace>;
54
+ require(id: string): Promise<Workspace>;
55
+ snapshot(id: string): Promise<WorkspaceSnapshot>;
56
+ /**
57
+ * Compose a workspace's board from the services it mounts: its own (locally created)
58
+ * blocks plus the full subtree of any service mounted from another workspace in the
59
+ * same org — so a shared service renders identically on every board, with one physical
60
+ * copy (and therefore one shared task list + status). Each mounted frame's board
61
+ * position/size is taken from the mount (the per-workspace layout override) — for a home
62
+ * frame as much as one mounted from elsewhere, since a service frame's position is always
63
+ * carried on the mount (that is what `moveBlock` writes). When the service repositories
64
+ * aren't wired (or nothing is mounted) this is a no-op and the local blocks stand.
65
+ */
66
+ private composeBoard;
67
+ /**
68
+ * Compose a workspace's executions from the services it mounts: its own runs plus those of
69
+ * any service mounted from another workspace, so a shared service's run progress/status
70
+ * renders on every board that mounts it — not just on its home workspace. Deduplicated by
71
+ * run id (a home service's runs already appear in the local list). No-op when sharing isn't
72
+ * wired or nothing is mounted.
73
+ */
74
+ private composeExecutions;
75
+ delete(id: string): Promise<void>;
76
+ }
77
+ //# sourceMappingURL=WorkspaceService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkspaceService.d.ts","sourceRoot":"","sources":["../../../src/modules/workspaces/WorkspaceService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAOlE,OAAO,KAAK,EAGV,SAAS,EAET,iBAAiB,EAClB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,wBAAwB,EACxB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAE7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAEtD,MAAM,WAAW,4BAA4B;IAC3C,mBAAmB,EAAE,mBAAmB,CAAA;IACxC,eAAe,EAAE,eAAe,CAAA;IAChC,kBAAkB,EAAE,kBAAkB,CAAA;IACtC,mBAAmB,EAAE,mBAAmB,CAAA;IACxC,WAAW,EAAE,WAAW,CAAA;IACxB,KAAK,EAAE,KAAK,CAAA;IACZ;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC,wBAAwB,CAAC,EAAE,wBAAwB,CAAA;CACpD;AAED,8EAA8E;AAC9E,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiB;IACjD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAoB;IACvD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAa;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAO;IAC7B,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAmB;IACtD,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAC,CAA0B;IAEpE,YAAY,EACV,mBAAmB,EACnB,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,WAAW,EACX,KAAK,EACL,iBAAiB,EACjB,wBAAwB,GACzB,EAAE,4BAA4B,EAS9B;IAED;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAErD;IAED,+EAA+E;IAC/E,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAEtD;IAED,qFAAqF;IACrF,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAExD;IAEK,MAAM,CACV,KAAK,EAAE,oBAAoB,EAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,OAAO,CAAC,iBAAiB,CAAC,CAqB5B;IAED;;;;;OAKG;YACW,SAAS;IAmCvB,oDAAoD;IAC9C,MAAM,CACV,EAAE,EAAE,MAAM,EACV,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GACpD,OAAO,CAAC,SAAS,CAAC,CAQpB;IAED,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAEtC;IAEK,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAcrD;IAED;;;;;;;;;OASG;YACW,YAAY;IAqC1B;;;;;;OAMG;YACW,iBAAiB;IAazB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtC;CACF"}
@@ -0,0 +1,193 @@
1
+ import { registerServiceForFrame, requireWorkspace, seedBlocks, seedPipelines, } from '@cat-factory/kernel';
2
+ export { requireWorkspace } from '@cat-factory/kernel';
3
+ /** Creates, reads and deletes boards (workspaces) and assembles snapshots. */
4
+ export class WorkspaceService {
5
+ workspaceRepository;
6
+ blockRepository;
7
+ pipelineRepository;
8
+ executionRepository;
9
+ idGenerator;
10
+ clock;
11
+ serviceRepository;
12
+ workspaceMountRepository;
13
+ constructor({ workspaceRepository, blockRepository, pipelineRepository, executionRepository, idGenerator, clock, serviceRepository, workspaceMountRepository, }) {
14
+ this.workspaceRepository = workspaceRepository;
15
+ this.blockRepository = blockRepository;
16
+ this.pipelineRepository = pipelineRepository;
17
+ this.executionRepository = executionRepository;
18
+ this.idGenerator = idGenerator;
19
+ this.clock = clock;
20
+ this.serviceRepository = serviceRepository;
21
+ this.workspaceMountRepository = workspaceMountRepository;
22
+ }
23
+ /**
24
+ * Boards visible to a user (see {@link WorkspaceVisibility}). A `null` scope
25
+ * means auth is disabled and all boards are returned.
26
+ */
27
+ list(scope) {
28
+ return this.workspaceRepository.listVisible(scope);
29
+ }
30
+ /** Owning user id for a board (string/owned, null/none, undefined/missing). */
31
+ ownerOf(id) {
32
+ return this.workspaceRepository.ownerOf(id);
33
+ }
34
+ /** Owning account id for a board (string/scoped, null/legacy, undefined/missing). */
35
+ accountOf(id) {
36
+ return this.workspaceRepository.accountOf(id);
37
+ }
38
+ async create(input, ownerUserId, accountId) {
39
+ const workspace = {
40
+ id: this.idGenerator.next('ws'),
41
+ name: input.name?.trim() || 'Untitled board',
42
+ description: input.description?.trim() || null,
43
+ createdAt: this.clock.now(),
44
+ accountId,
45
+ };
46
+ await this.workspaceRepository.create(workspace, ownerUserId, accountId);
47
+ // The built-in pipeline catalog is product configuration, not sample data, so
48
+ // every board gets it — including the empty boards real users start with.
49
+ for (const pipeline of seedPipelines()) {
50
+ await this.pipelineRepository.insert(workspace.id, pipeline);
51
+ }
52
+ // The sample architecture blocks are opt-in (demo boards + the test fixtures);
53
+ // production boards start empty (the SPA creates with `seed: false`).
54
+ if (input.seed ?? true) {
55
+ await this.seedBoard(workspace.id);
56
+ }
57
+ return this.snapshot(workspace.id);
58
+ }
59
+ /**
60
+ * Seed the demo architecture, registering each top-level frame as an account-owned service
61
+ * (so seeded frames are shareable across the org exactly like ones created on the board) and
62
+ * stamping every seeded block with its frame's service. A no-op service registration when
63
+ * in-org sharing isn't wired leaves plain workspace-local blocks (legacy behaviour).
64
+ */
65
+ async seedBoard(workspaceId) {
66
+ const blocks = seedBlocks();
67
+ const byId = new Map(blocks.map((b) => [b.id, b]));
68
+ const topFrameOf = (b) => {
69
+ let cur = b;
70
+ while (cur && !(cur.level === 'frame' && cur.parentId === null)) {
71
+ cur = cur.parentId ? byId.get(cur.parentId) : undefined;
72
+ }
73
+ return cur;
74
+ };
75
+ const serviceByFrame = new Map();
76
+ for (const b of blocks) {
77
+ if (b.level === 'frame' && b.parentId === null) {
78
+ serviceByFrame.set(b.id, await registerServiceForFrame({
79
+ serviceRepository: this.serviceRepository,
80
+ workspaceMountRepository: this.workspaceMountRepository,
81
+ workspaceRepository: this.workspaceRepository,
82
+ idGenerator: this.idGenerator,
83
+ clock: this.clock,
84
+ }, workspaceId, b));
85
+ }
86
+ }
87
+ for (const b of blocks) {
88
+ const frame = topFrameOf(b);
89
+ await this.blockRepository.insert(workspaceId, b, frame ? serviceByFrame.get(frame.id) : null);
90
+ }
91
+ }
92
+ /** Rename a board and/or update its description. */
93
+ async update(id, patch) {
94
+ await this.require(id);
95
+ if (patch.name !== undefined)
96
+ await this.workspaceRepository.rename(id, patch.name.trim());
97
+ if ('description' in patch) {
98
+ const desc = patch.description == null ? null : patch.description.trim() || null;
99
+ await this.workspaceRepository.setDescription(id, desc);
100
+ }
101
+ return this.require(id);
102
+ }
103
+ require(id) {
104
+ return requireWorkspace(this.workspaceRepository, id);
105
+ }
106
+ async snapshot(id) {
107
+ const workspace = await this.require(id);
108
+ const [localBlocks, pipelines, localExecutions] = await Promise.all([
109
+ this.blockRepository.listByWorkspace(id),
110
+ this.pipelineRepository.listByWorkspace(id),
111
+ this.executionRepository.listByWorkspace(id),
112
+ ]);
113
+ const mounts = this.workspaceMountRepository && this.serviceRepository
114
+ ? await this.workspaceMountRepository.listByWorkspace(id)
115
+ : [];
116
+ const blocks = await this.composeBoard(localBlocks, mounts);
117
+ const executions = await this.composeExecutions(localExecutions, mounts);
118
+ return { workspace, blocks, pipelines, executions };
119
+ }
120
+ /**
121
+ * Compose a workspace's board from the services it mounts: its own (locally created)
122
+ * blocks plus the full subtree of any service mounted from another workspace in the
123
+ * same org — so a shared service renders identically on every board, with one physical
124
+ * copy (and therefore one shared task list + status). Each mounted frame's board
125
+ * position/size is taken from the mount (the per-workspace layout override) — for a home
126
+ * frame as much as one mounted from elsewhere, since a service frame's position is always
127
+ * carried on the mount (that is what `moveBlock` writes). When the service repositories
128
+ * aren't wired (or nothing is mounted) this is a no-op and the local blocks stand.
129
+ */
130
+ async composeBoard(localBlocks, mounts) {
131
+ if (!this.serviceRepository || mounts.length === 0)
132
+ return localBlocks;
133
+ const byId = new Map(localBlocks.map((b) => [b.id, b]));
134
+ const localIds = new Set(byId.keys());
135
+ // The per-workspace layout override for each mounted service's frame.
136
+ const frameLayout = new Map();
137
+ // Resolve every mounted service in one batched query (not a `get` per mount).
138
+ const services = await this.serviceRepository.listByIds(mounts.map((m) => m.serviceId));
139
+ const frameOf = new Map(services.map((s) => [s.id, s.frameBlockId]));
140
+ const foreignServiceIds = [];
141
+ for (const mount of mounts) {
142
+ const frameBlockId = frameOf.get(mount.serviceId);
143
+ if (!frameBlockId)
144
+ continue;
145
+ frameLayout.set(frameBlockId, {
146
+ x: mount.position.x,
147
+ y: mount.position.y,
148
+ ...(mount.size ? { w: mount.size.w, h: mount.size.h } : {}),
149
+ });
150
+ // Pull in the subtree only for services homed in ANOTHER workspace — a local service's
151
+ // blocks are already in `localBlocks`.
152
+ if (!localIds.has(frameBlockId))
153
+ foreignServiceIds.push(mount.serviceId);
154
+ }
155
+ // One batched query for all foreign subtrees (not one per service).
156
+ for (const b of await this.blockRepository.listByServices(foreignServiceIds)) {
157
+ if (!byId.has(b.id))
158
+ byId.set(b.id, b);
159
+ }
160
+ return [...byId.values()].map((b) => {
161
+ const layout = frameLayout.get(b.id);
162
+ if (!layout)
163
+ return b;
164
+ const next = { ...b, position: { x: layout.x, y: layout.y } };
165
+ if (layout.w !== undefined && layout.h !== undefined)
166
+ next.size = { w: layout.w, h: layout.h };
167
+ return next;
168
+ });
169
+ }
170
+ /**
171
+ * Compose a workspace's executions from the services it mounts: its own runs plus those of
172
+ * any service mounted from another workspace, so a shared service's run progress/status
173
+ * renders on every board that mounts it — not just on its home workspace. Deduplicated by
174
+ * run id (a home service's runs already appear in the local list). No-op when sharing isn't
175
+ * wired or nothing is mounted.
176
+ */
177
+ async composeExecutions(localExecutions, mounts) {
178
+ if (mounts.length === 0)
179
+ return localExecutions;
180
+ const byId = new Map(localExecutions.map((e) => [e.id, e]));
181
+ // One batched query for every mounted service's runs (not one round-trip per mount).
182
+ for (const e of await this.executionRepository.listByServices(mounts.map((m) => m.serviceId))) {
183
+ if (!byId.has(e.id))
184
+ byId.set(e.id, e);
185
+ }
186
+ return [...byId.values()];
187
+ }
188
+ async delete(id) {
189
+ await this.require(id);
190
+ await this.workspaceRepository.delete(id);
191
+ }
192
+ }
193
+ //# sourceMappingURL=WorkspaceService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkspaceService.js","sourceRoot":"","sources":["../../../src/modules/workspaces/WorkspaceService.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@cat-factory/workspaces",
3
+ "version": "0.6.0",
4
+ "description": "Tenancy base services (workspaces and accounts) for the Agent Architecture Board.",
5
+ "files": [
6
+ "dist"
7
+ ],
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "dependencies": {
22
+ "@cat-factory/contracts": "0.6.0",
23
+ "@cat-factory/kernel": "0.6.0"
24
+ },
25
+ "devDependencies": {
26
+ "typescript": "7.0.1-rc"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc -b tsconfig.build.json",
30
+ "typecheck": "tsc -p tsconfig.json --noEmit"
31
+ }
32
+ }