@proteos/sdk 0.45.0 → 0.46.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/src/auth/index.ts CHANGED
@@ -2,6 +2,12 @@ import type { ProteosClient } from '../client.js'
2
2
  import { type MeService, MeServiceImpl } from './me.js'
3
3
  import { type OrganizationService, OrganizationServiceImpl } from './organizations.js'
4
4
  import { type RoleService, RoleServiceImpl } from './roles.js'
5
+ import { type ShareService, ShareServiceImpl } from './shares.js'
6
+ import { type TeamService, TeamServiceImpl } from './teams.js'
7
+ import {
8
+ type UserRoleAssignmentService,
9
+ UserRoleAssignmentServiceImpl,
10
+ } from './user-role-assignments.js'
5
11
  import { type UserService, UserServiceImpl } from './users.js'
6
12
 
7
13
  /**
@@ -41,11 +47,32 @@ export class AccountClient {
41
47
  */
42
48
  readonly roles: RoleService
43
49
 
50
+ /**
51
+ * Service for the org-wide user-role-assignment listing ("who holds which
52
+ * role" in one request).
53
+ */
54
+ readonly userRoleAssignments: UserRoleAssignmentService
55
+
44
56
  /**
45
57
  * Service for managing organizations.
46
58
  */
47
59
  readonly organizations: OrganizationService
48
60
 
61
+ /**
62
+ * Service for managing teams and their membership.
63
+ */
64
+ readonly teams: TeamService
65
+
66
+ /**
67
+ * Service for sharing individual resources with a principal.
68
+ *
69
+ * Lives here rather than on the owning resource's client because sharing is
70
+ * ONE concept across every resource type — a deal, a conversation, a
71
+ * knowledge space. Callers name an entity; which service handles the write is
72
+ * the SDK's problem, so centralizing it server-side later moves no caller.
73
+ */
74
+ readonly shares: ShareService
75
+
49
76
  /**
50
77
  * Creates a new AccountClient instance.
51
78
  *
@@ -55,7 +82,10 @@ export class AccountClient {
55
82
  this.me = new MeServiceImpl(client)
56
83
  this.users = new UserServiceImpl(client)
57
84
  this.roles = new RoleServiceImpl(client)
85
+ this.userRoleAssignments = new UserRoleAssignmentServiceImpl(client)
58
86
  this.organizations = new OrganizationServiceImpl(client)
87
+ this.teams = new TeamServiceImpl(client)
88
+ this.shares = new ShareServiceImpl(client)
59
89
  }
60
90
  }
61
91
 
@@ -63,12 +93,20 @@ export type { MeService } from './me.js'
63
93
  export type { OrganizationService } from './organizations.js'
64
94
  // Re-export platform entities (canonical mirror of the Go registry)
65
95
  export type { PlatformEntity } from './platform-entities.js'
66
- export { isPlatformEntity, PLATFORM_ENTITIES, PLATFORM_ENTITY_SLUGS } from './platform-entities.js'
96
+ export {
97
+ isPlatformEntity,
98
+ PLATFORM_ENTITIES,
99
+ PLATFORM_ENTITY_SLUGS,
100
+ SCOPED_PLATFORM_ENTITY_SLUGS,
101
+ } from './platform-entities.js'
67
102
  export type { RoleService } from './roles.js'
103
+ export type { TeamService } from './teams.js'
104
+ export type { UserRoleAssignmentService } from './user-role-assignments.js'
68
105
  // Re-export types
69
106
  export type {
70
107
  // Api key types
71
108
  ApiKey,
109
+ AddTeamMemberRequest,
72
110
  AssignPermissionRequest,
73
111
  AssignRoleRequest,
74
112
  // Shared types
@@ -77,10 +115,14 @@ export type {
77
115
  CreatedApiKey,
78
116
  CreateOrganizationRequest,
79
117
  CreateRoleRequest,
118
+ CreateTeamRequest,
80
119
  CreateUserRequest,
81
120
  ListOrganizationsOptions,
82
121
  ListRolePermissionsOptions,
83
122
  ListRolesOptions,
123
+ ListTeamMembersOptions,
124
+ ListTeamsOptions,
125
+ ListOrgUserRoleAssignmentsOptions,
84
126
  ListUserRoleAssignmentsOptions,
85
127
  ListUsersOptions,
86
128
  // Organization types
@@ -90,11 +132,20 @@ export type {
90
132
  // Role types
91
133
  Role,
92
134
  RoleEntityPermission,
135
+ // Team types
136
+ Team,
137
+ TeamMember,
138
+ UpdateMeRequest,
93
139
  UpdateOrganizationRequest,
94
140
  UpdateRoleRequest,
141
+ UpdateTeamRequest,
95
142
  UpdateUserRequest,
96
143
  // User types
97
144
  User,
145
+ // Share types
146
+ SharePermission,
147
+ ShareRequest,
148
+ ShareRow,
98
149
  // User role assignment types
99
150
  UserRoleAssignment,
100
151
  } from './types.js'
@@ -102,10 +153,16 @@ export type {
102
153
  export {
103
154
  ApiKeySchema,
104
155
  OrganizationSchema,
156
+ SharePermissionSchema,
157
+ ShareRequestSchema,
105
158
  RoleEntityPermissionSchema,
106
159
  RoleSchema,
160
+ TeamMemberSchema,
161
+ TeamSchema,
107
162
  UserRoleAssignmentSchema,
108
163
  UserSchema,
109
164
  } from './types.js'
110
165
  // Re-export service interfaces
111
166
  export type { UserService } from './users.js'
167
+ // Share routing — exported so a UI can hide a share affordance it cannot serve.
168
+ export { isShareable, type ShareService, shareRouteFor } from './shares.js'
package/src/auth/me.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ProteosClient } from '../client.js'
2
- import type { Organization, User } from './types.js'
2
+ import type { Organization, UpdateMeRequest, User } from './types.js'
3
3
 
4
4
  const ME_BASE_PATH = '/accounts/v1/me'
5
5
 
@@ -17,6 +17,15 @@ export interface MeService {
17
17
  */
18
18
  get(): Promise<User>
19
19
 
20
+ /**
21
+ * Updates the caller's own profile (name, photo). Requires a JWT session —
22
+ * requests authenticated with an API key are rejected.
23
+ *
24
+ * @param request - Fields to update; `profile_image: null` removes the photo
25
+ * @returns The updated user
26
+ */
27
+ update(request: UpdateMeRequest): Promise<User>
28
+
20
29
  /**
21
30
  * Lists the organizations the caller can act in (all orgs for a platform
22
31
  * admin; otherwise the orgs they're a member of). Powers the org-switcher.
@@ -36,6 +45,10 @@ export class MeServiceImpl implements MeService {
36
45
  return this.client.request<User>('GET', ME_BASE_PATH)
37
46
  }
38
47
 
48
+ async update(request: UpdateMeRequest): Promise<User> {
49
+ return this.client.request<User>('PATCH', ME_BASE_PATH, request)
50
+ }
51
+
39
52
  async organizations(): Promise<Organization[]> {
40
53
  const response = await this.client.request<{ data: Organization[] }>(
41
54
  'GET',
@@ -22,6 +22,12 @@ export const PLATFORM_ENTITIES: readonly PlatformEntity[] = [
22
22
  { slug: 'roles', name: 'Roles' },
23
23
  { slug: 'user-role-assignments', name: 'User Role Assignments' },
24
24
  { slug: 'role-entity-permissions', name: 'Role Entity Permissions' },
25
+ // Teams are the org's structure and a principal that can hold access anywhere
26
+ // a user can. Membership is a separate slug: seeing the org chart and changing
27
+ // who is in it are different decisions, and membership is what moves access.
28
+ { slug: 'teams', name: 'Teams' },
29
+ { slug: 'team-members', name: 'Team Members' },
30
+ // The share audit trail — a distinct grant from the resources it references.
25
31
  // Schema / content (metadata-service)
26
32
  { slug: 'entities', name: 'Entities' },
27
33
  { slug: 'pages', name: 'Pages' },
@@ -47,6 +53,7 @@ export const PLATFORM_ENTITIES: readonly PlatformEntity[] = [
47
53
  { slug: 'knowledge-nodes', name: 'Knowledge Nodes' },
48
54
  { slug: 'knowledge-links', name: 'Knowledge Links' },
49
55
  { slug: 'knowledge-labels', name: 'Knowledge Labels' },
56
+ { slug: 'knowledge-spaces', name: 'Knowledge Spaces' },
50
57
  // Agent suite (agent-service)
51
58
  { slug: 'agents', name: 'Agents' },
52
59
  { slug: 'prompts', name: 'Prompts' },
@@ -70,6 +77,17 @@ export const PLATFORM_ENTITIES: readonly PlatformEntity[] = [
70
77
  { slug: 'glossary-terms', name: 'Glossary Terms' },
71
78
  // Mistranscribed terms proposed by the post-transcription review pass.
72
79
  { slug: 'mistranscribed-terms', name: 'Mistranscribed Terms' },
80
+ // The person-level identity layer (contacts + addresses + merges + consent +
81
+ // erasure) — one grant governs the whole aggregate.
82
+ { slug: 'contacts', name: 'Contacts' },
83
+ // Ingest-time filter rules + their drop-audit events.
84
+ { slug: 'conversation-filters', name: 'Conversation Filters' },
85
+ // The org's conversation taxonomy (read by the pre-summary classifier).
86
+ { slug: 'conversation-types', name: 'Conversation Types' },
87
+ // Generic org-shared audience taxonomy; membership lives on the contact.
88
+ { slug: 'contact-groups', name: 'Contact Groups' },
89
+ // Tone-of-voice synthesis: per-user setups + generated instruction profiles.
90
+ { slug: 'tone-profiles', name: 'Tone Profiles' },
73
91
  // Connectors (connector-service). `connections` above is shared; this is the
74
92
  // manifest catalog.
75
93
  { slug: 'connectors', name: 'Connectors' },
@@ -80,6 +98,15 @@ export const PLATFORM_ENTITY_SLUGS: readonly string[] = PLATFORM_ENTITIES.map(
80
98
  (entity) => entity.slug,
81
99
  )
82
100
 
101
+ /**
102
+ * Platform entities whose owning service actually enforces the `*_scoped`
103
+ * permission verbs (record-level access grants). Granting `read_scoped` on any
104
+ * other platform entity is legal on the wire but inert — no service narrows
105
+ * by it — so permission UIs only offer the scoped column for these. Extend as
106
+ * services adopt scoped enforcement.
107
+ */
108
+ export const SCOPED_PLATFORM_ENTITY_SLUGS: readonly string[] = ['knowledge-nodes']
109
+
83
110
  /** True when `slug` belongs to a platform entity (reserved; can't be user-created). */
84
111
  export function isPlatformEntity(slug: string): boolean {
85
112
  return PLATFORM_ENTITY_SLUGS.includes(slug)
package/src/auth/roles.ts CHANGED
@@ -93,7 +93,9 @@ export interface RoleService {
93
93
  * Deletes a role.
94
94
  *
95
95
  * @param slug - Role slug
96
- * @throws {ProteosError} If role not found (404)
96
+ * @throws {ProteosError} If role not found (404), or if the role still has
97
+ * entity permissions attached (400 `role_deletion_not_allowed`) — remove
98
+ * the grants first
97
99
  */
98
100
  delete(slug: string): Promise<void>
99
101
 
@@ -0,0 +1,181 @@
1
+ import type { ProteosClient } from '../client.js'
2
+ import type { PrincipalRef } from '../meta/types.js'
3
+ import { ProteosError } from '../errors.js'
4
+ import type { SharePermission, ShareRequest, ShareRow } from './types.js'
5
+
6
+ /**
7
+ * Which service owns each shareable platform entity.
8
+ *
9
+ * Sharing is decided by the service that OWNS the resource — whether a caller
10
+ * may grant depends on what the row itself grants them (its granting
11
+ * attributes, or an owner column), which only that service can read. So the
12
+ * write endpoint is per-service by necessity.
13
+ *
14
+ * This map is the client-side seam that hides that. Callers name an entity, not
15
+ * a service, so if sharing is ever centralized behind one endpoint the change is
16
+ * a one-line edit here and no caller moves.
17
+ *
18
+ * Anything NOT listed is a user-defined entity, which data-service owns — that
19
+ * set is unbounded and appears at runtime, so it is the default rather than an
20
+ * enumeration.
21
+ */
22
+ const PLATFORM_ENTITY_SERVICE: Readonly<Record<string, string>> = {
23
+ // Agent suite (agent-service)
24
+ agents: '/agents',
25
+ prompts: '/agents',
26
+ skills: '/agents',
27
+ tools: '/agents',
28
+ toolsets: '/agents',
29
+ 'mcp-servers': '/agents',
30
+ // Knowledge (knowledge-service)
31
+ 'knowledge-spaces': '/knowledge',
32
+ 'knowledge-nodes': '/knowledge',
33
+ // Conversations (conversation-service)
34
+ conversations: '/conversations',
35
+ connections: '/conversations',
36
+ }
37
+
38
+ /**
39
+ * The services that actually serve `/v1/shares` today.
40
+ *
41
+ * Deliberately separate from the map above: the other services get the endpoint
42
+ * in later phases, and until then a share must fail with a clear message rather
43
+ * than a bare 404 that reads like the record does not exist.
44
+ */
45
+ const SERVICES_WITH_SHARING: ReadonlySet<string> = new Set(['/data', '/knowledge'])
46
+
47
+ /** Records live in data-service; user-defined entities are the unlisted majority. */
48
+ const DEFAULT_SERVICE = '/data'
49
+
50
+ /**
51
+ * Resolves the share endpoint for an entity slug.
52
+ *
53
+ * Exported for tests and for callers that want to check support before offering
54
+ * a share affordance in a UI.
55
+ */
56
+ export function shareRouteFor(entitySlug: string): string {
57
+ const service = PLATFORM_ENTITY_SERVICE[entitySlug] ?? DEFAULT_SERVICE
58
+ if (!SERVICES_WITH_SHARING.has(service)) {
59
+ throw new ProteosError(
60
+ `Sharing is not available for '${entitySlug}' yet — ${service.slice(1)}-service does not serve a share endpoint.`,
61
+ 501,
62
+ 'sharing_not_supported',
63
+ )
64
+ }
65
+ return `${service}/v1/shares`
66
+ }
67
+
68
+ /** True when `entitySlug` can be shared today. */
69
+ export function isShareable(entitySlug: string): boolean {
70
+ const service = PLATFORM_ENTITY_SERVICE[entitySlug] ?? DEFAULT_SERVICE
71
+ return SERVICES_WITH_SHARING.has(service)
72
+ }
73
+
74
+ /**
75
+ * Sharing an individual resource with a principal — a person, an agent, an API
76
+ * client, a team, or the whole organization.
77
+ *
78
+ * A share grants access to ONE resource, which is a different thing from a role:
79
+ * a role grants a permission on an entity TYPE (every deal), a share grants it
80
+ * on an INSTANCE (this deal). The two compose — a share gives nothing to someone
81
+ * holding no grant on the entity type at all, which bounds its blast radius to a
82
+ * population already authorized for that kind of resource.
83
+ *
84
+ * Callers name an ENTITY, never a service. Which service handles the write is
85
+ * this module's problem.
86
+ *
87
+ * Who may share: the resource's owner, or someone holding the plain (unscoped)
88
+ * permission on the entity. Notably NOT a share recipient — access held only
89
+ * through a share cannot be passed on, and cannot be turned into ownership.
90
+ */
91
+ export interface ShareService {
92
+ /**
93
+ * Grants a principal access to one resource.
94
+ *
95
+ * Granting `write` or `delete` implies `read`: a principal who cannot see a
96
+ * resource must never be able to edit it, so the read is granted alongside.
97
+ *
98
+ * @throws {ProteosError} 403 `share_not_permitted` when the caller holds their
99
+ * own access only through a share; 403 `principal_not_in_org` when the
100
+ * recipient is outside the caller's organization (rejected outright rather
101
+ * than written inert, which would become a live grant the moment they joined);
102
+ * 404 when the resource does not exist or is not visible to the caller;
103
+ * 501 `sharing_not_supported` when the owning service has no share endpoint.
104
+ */
105
+ share(
106
+ entitySlug: string,
107
+ recordId: string,
108
+ principal: PrincipalRef,
109
+ permission: SharePermission,
110
+ ): Promise<void>
111
+
112
+ /**
113
+ * Revokes a principal's access to one resource.
114
+ *
115
+ * Revoking `read` CASCADES to `write` and `delete` — an edit right on
116
+ * something someone can no longer see would be incoherent.
117
+ *
118
+ * Revocation is deliberately wider than granting: the owner, anyone holding
119
+ * the unscoped `write` permission, and platform admins may all revoke. Erring
120
+ * permissive costs nothing here, while erring strict would strand grants
121
+ * nobody could clean up.
122
+ */
123
+ unshare(
124
+ entitySlug: string,
125
+ recordId: string,
126
+ principal: PrincipalRef,
127
+ permission: SharePermission,
128
+ ): Promise<void>
129
+
130
+ /**
131
+ * Lists a resource's shares.
132
+ *
133
+ * Gated to the population that may REVOKE them — the owner, unscoped-write
134
+ * holders, and platform admins. Record read is deliberately not enough: a
135
+ * share list discloses org structure ("this deal is visible to team:legal").
136
+ *
137
+ * @throws {ProteosError} 403 `share_listing_not_permitted` otherwise.
138
+ */
139
+ listShares(entitySlug: string, recordId: string): Promise<ShareRow[]>
140
+ }
141
+
142
+ export class ShareServiceImpl implements ShareService {
143
+ constructor(private readonly client: ProteosClient) {}
144
+
145
+ async share(
146
+ entitySlug: string,
147
+ recordId: string,
148
+ principal: PrincipalRef,
149
+ permission: SharePermission,
150
+ ): Promise<void> {
151
+ await this.client.request<void>('POST', shareRouteFor(entitySlug), {
152
+ entity_slug: entitySlug,
153
+ record_id: recordId,
154
+ principal,
155
+ permission,
156
+ } satisfies ShareRequest)
157
+ }
158
+
159
+ async unshare(
160
+ entitySlug: string,
161
+ recordId: string,
162
+ principal: PrincipalRef,
163
+ permission: SharePermission,
164
+ ): Promise<void> {
165
+ await this.client.request<void>('DELETE', shareRouteFor(entitySlug), {
166
+ entity_slug: entitySlug,
167
+ record_id: recordId,
168
+ principal,
169
+ permission,
170
+ } satisfies ShareRequest)
171
+ }
172
+
173
+ async listShares(entitySlug: string, recordId: string): Promise<ShareRow[]> {
174
+ const query = new URLSearchParams({ entity_slug: entitySlug, record_id: recordId })
175
+ const response = await this.client.request<{ shares: ShareRow[] }>(
176
+ 'GET',
177
+ `${shareRouteFor(entitySlug)}?${query.toString()}`,
178
+ )
179
+ return response.shares ?? []
180
+ }
181
+ }
@@ -0,0 +1,135 @@
1
+ import type { ProteosClient } from '../client.js'
2
+ import { PageIterator } from '../iterator.js'
3
+ import type { ListResult } from '../types/common.js'
4
+ import type {
5
+ AddTeamMemberRequest,
6
+ CreateTeamRequest,
7
+ ListTeamMembersOptions,
8
+ ListTeamsOptions,
9
+ Team,
10
+ TeamMember,
11
+ UpdateTeamRequest,
12
+ } from './types.js'
13
+
14
+ const TEAMS_BASE_PATH = '/accounts/v1/teams'
15
+
16
+ /**
17
+ * Service for managing teams — the org's structure, and a principal that can
18
+ * hold access anywhere a user can.
19
+ *
20
+ * Team hierarchy rolls UP: a member of a child team counts as a member of every
21
+ * ancestor **for grants made to that ancestor**. Granting `sales` reaches the
22
+ * members of `sales-emea`; granting `sales-emea` never reaches someone who is
23
+ * only in `sales`. This is the direction people get backwards.
24
+ */
25
+ export interface TeamService {
26
+ /**
27
+ * Lists teams with optional filtering.
28
+ * Returns an async iterator that automatically handles pagination.
29
+ */
30
+ list(options?: ListTeamsOptions): PageIterator<Team, ListTeamsOptions>
31
+
32
+ /** Fetches a single page of teams. */
33
+ listPage(options?: ListTeamsOptions): Promise<ListResult<Team>>
34
+
35
+ /**
36
+ * Gets a single team by slug.
37
+ * @throws {ProteosError} If the team is not found (404)
38
+ */
39
+ get(slug: string): Promise<Team>
40
+
41
+ /**
42
+ * Creates a team, optionally nested under an existing one.
43
+ * @throws {ProteosError} If the parent would create a cycle or exceed the
44
+ * maximum depth (400 `team_hierarchy_invalid`)
45
+ */
46
+ create(request: CreateTeamRequest): Promise<Team>
47
+
48
+ /**
49
+ * Updates a team's name, description or parent.
50
+ *
51
+ * The slug cannot be changed: it is half the primary key and every grant
52
+ * references `team:<orgId>/<slug>`, so a rename would orphan them. Pass an
53
+ * empty `parent_team_slug` to promote the team to a root.
54
+ */
55
+ update(slug: string, request: UpdateTeamRequest): Promise<Team>
56
+
57
+ /**
58
+ * Deletes a team.
59
+ * @throws {ProteosError} If the team still has members or child teams
60
+ * (400 `team_deletion_not_allowed`)
61
+ */
62
+ delete(slug: string): Promise<void>
63
+
64
+ /**
65
+ * Lists a team's DIRECT members. Membership of ancestor teams is derived, not
66
+ * stored, so it does not appear here.
67
+ */
68
+ getMembers(
69
+ slug: string,
70
+ options?: ListTeamMembersOptions,
71
+ ): PageIterator<TeamMember, ListTeamMembersOptions>
72
+
73
+ /** Adds a user to a team. */
74
+ addMember(slug: string, request: AddTeamMemberRequest): Promise<TeamMember>
75
+
76
+ /** Removes a user from a team. */
77
+ removeMember(slug: string, userId: string): Promise<void>
78
+ }
79
+
80
+ /**
81
+ * Implementation of TeamService.
82
+ */
83
+ export class TeamServiceImpl implements TeamService {
84
+ constructor(private readonly client: ProteosClient) {}
85
+
86
+ list(options: ListTeamsOptions = {}): PageIterator<Team, ListTeamsOptions> {
87
+ return new PageIterator((opts) => this.listPage(opts), options)
88
+ }
89
+
90
+ async listPage(options: ListTeamsOptions = {}): Promise<ListResult<Team>> {
91
+ return this.client.requestWithQuery<ListResult<Team>>('GET', TEAMS_BASE_PATH, options)
92
+ }
93
+
94
+ async get(slug: string): Promise<Team> {
95
+ return this.client.request<Team>('GET', `${TEAMS_BASE_PATH}/${slug}`)
96
+ }
97
+
98
+ async create(request: CreateTeamRequest): Promise<Team> {
99
+ return this.client.request<Team>('POST', TEAMS_BASE_PATH, request)
100
+ }
101
+
102
+ async update(slug: string, request: UpdateTeamRequest): Promise<Team> {
103
+ return this.client.request<Team>('PATCH', `${TEAMS_BASE_PATH}/${slug}`, request)
104
+ }
105
+
106
+ async delete(slug: string): Promise<void> {
107
+ await this.client.request<void>('DELETE', `${TEAMS_BASE_PATH}/${slug}`)
108
+ }
109
+
110
+ getMembers(
111
+ slug: string,
112
+ options: ListTeamMembersOptions = {},
113
+ ): PageIterator<TeamMember, ListTeamMembersOptions> {
114
+ return new PageIterator((opts) => this.fetchMembersPage(slug, opts), options)
115
+ }
116
+
117
+ async addMember(slug: string, request: AddTeamMemberRequest): Promise<TeamMember> {
118
+ return this.client.request<TeamMember>('POST', `${TEAMS_BASE_PATH}/${slug}/members`, request)
119
+ }
120
+
121
+ async removeMember(slug: string, userId: string): Promise<void> {
122
+ await this.client.request<void>('DELETE', `${TEAMS_BASE_PATH}/${slug}/members/${userId}`)
123
+ }
124
+
125
+ private async fetchMembersPage(
126
+ slug: string,
127
+ options: ListTeamMembersOptions,
128
+ ): Promise<ListResult<TeamMember>> {
129
+ return this.client.requestWithQuery<ListResult<TeamMember>>(
130
+ 'GET',
131
+ `${TEAMS_BASE_PATH}/${slug}/members`,
132
+ options,
133
+ )
134
+ }
135
+ }