@axiom-lattice/protocols 4.1.1 → 4.1.3
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +720 -19
- package/dist/index.d.ts +720 -19
- package/dist/index.js +409 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +388 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/BindingProtocol.ts +87 -11
- package/src/ChannelInstallationStoreProtocol.ts +23 -4
- package/src/ExactDataSnapshot.ts +119 -0
- package/src/ProjectBotMembershipStoreProtocol.ts +48 -0
- package/src/ProjectMembershipStoreProtocol.ts +58 -0
- package/src/ProjectRoomMessageStoreProtocol.ts +26 -0
- package/src/ProjectRoomProtocol.ts +143 -0
- package/src/ProjectRoomRealtimeProtocol.ts +349 -0
- package/src/ProjectRoomStoreProtocol.ts +16 -0
- package/src/TaskStoreProtocol.ts +66 -2
- package/src/TaskWorkItemProtocol.ts +86 -0
- package/src/TrustedRunContextProtocol.ts +119 -0
- package/src/WorkspaceStoreProtocol.ts +3 -1
- package/src/__tests__/BindingProtocol.test.ts +36 -0
- package/src/__tests__/ExactDataSnapshot.test.ts +105 -0
- package/src/__tests__/ProjectRoomProtocol.test.ts +48 -0
- package/src/__tests__/ProjectRoomRealtimeProtocol.test.ts +185 -0
- package/src/__tests__/ProjectRoomStores.test.ts +363 -0
- package/src/__tests__/ProjectTaskProtocol.test.ts +29 -0
- package/src/__tests__/TaskWorkItemProtocol.test.ts +42 -0
- package/src/__tests__/TrustedRunContextProtocol.test.ts +265 -0
- package/src/index.ts +8 -0
- package/type-tests/task-work-item-store-compatibility.ts +14 -2
package/src/BindingProtocol.ts
CHANGED
|
@@ -27,14 +27,97 @@ export interface CreateBindingInput {
|
|
|
27
27
|
tenantId: string;
|
|
28
28
|
senderId: string;
|
|
29
29
|
agentId: string;
|
|
30
|
+
threadId?: string;
|
|
30
31
|
threadMode?: "fixed" | "per_conversation";
|
|
31
32
|
senderDisplayName?: string;
|
|
32
33
|
senderMetadata?: Record<string, unknown>;
|
|
33
34
|
workspaceId?: string;
|
|
34
35
|
projectId?: string;
|
|
36
|
+
/** Whether the binding is eligible for inbound resolution immediately after creation. */
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Filters binding records before pagination is applied. */
|
|
41
|
+
export interface BindingListParams {
|
|
42
|
+
tenantId: string;
|
|
43
|
+
channel?: string;
|
|
44
|
+
agentId?: string;
|
|
45
|
+
channelInstallationId?: string;
|
|
46
|
+
/** Installation ID prefixes excluded before pagination, for internal namespaces. */
|
|
47
|
+
excludeInstallationIdPrefixes?: string[];
|
|
48
|
+
excludeChannels?: string[];
|
|
49
|
+
limit?: number;
|
|
50
|
+
offset?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fields that may change after a binding is created.
|
|
55
|
+
*
|
|
56
|
+
* Binding identity (`id`, tenant, channel, installation, sender, and timestamps) is intentionally
|
|
57
|
+
* absent so every persistence backend can enforce tenant-scoped mutation without identity drift.
|
|
58
|
+
*/
|
|
59
|
+
export interface BindingMutablePatch {
|
|
60
|
+
/** Agent that receives messages for this subject. */
|
|
61
|
+
agentId?: string;
|
|
62
|
+
/** Fixed thread used when `threadMode` is `fixed`. */
|
|
63
|
+
threadId?: string;
|
|
64
|
+
/** Optional workspace execution scope. */
|
|
65
|
+
workspaceId?: string;
|
|
66
|
+
/** Optional project execution scope. */
|
|
67
|
+
projectId?: string;
|
|
68
|
+
/** Whether messages share one thread or create one per conversation. */
|
|
69
|
+
threadMode?: "fixed" | "per_conversation";
|
|
70
|
+
/** Human-readable sender label. */
|
|
71
|
+
senderDisplayName?: string;
|
|
72
|
+
/** Mutable sender metadata supplied by trusted internal callers. */
|
|
73
|
+
senderMetadata?: Record<string, unknown>;
|
|
74
|
+
/** Whether inbound resolution may use this binding. */
|
|
75
|
+
enabled?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Raised when a channel installation already has a binding for the same tenant and sender. */
|
|
79
|
+
export class DuplicateChannelBindingSubjectError extends Error {
|
|
80
|
+
constructor() {
|
|
81
|
+
super("A binding already exists for this channel subject");
|
|
82
|
+
this.name = "DuplicateChannelBindingSubjectError";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A duplicate subject found while upgrading a local channel-binding database. */
|
|
87
|
+
export interface ChannelBindingMigrationConflict {
|
|
88
|
+
tenantId: string;
|
|
89
|
+
channel: string;
|
|
90
|
+
channelInstallationId: string;
|
|
91
|
+
senderId: string;
|
|
92
|
+
count: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Raised when a local binding uniqueness migration requires operator reconciliation.
|
|
97
|
+
*
|
|
98
|
+
* The conflict list contains only subject identifiers and row counts; binding metadata and other
|
|
99
|
+
* potentially sensitive payloads are never included.
|
|
100
|
+
*/
|
|
101
|
+
export class ChannelBindingMigrationConflictError extends Error {
|
|
102
|
+
readonly conflicts: ChannelBindingMigrationConflict[];
|
|
103
|
+
|
|
104
|
+
constructor(conflicts: ChannelBindingMigrationConflict[]) {
|
|
105
|
+
super(`Channel binding migration found ${conflicts.length} duplicate subject(s)`);
|
|
106
|
+
this.name = "ChannelBindingMigrationConflictError";
|
|
107
|
+
this.conflicts = conflicts;
|
|
108
|
+
}
|
|
35
109
|
}
|
|
36
110
|
|
|
37
111
|
export interface BindingRegistry {
|
|
112
|
+
findById(tenantId: string, id: string): Promise<Binding | null>;
|
|
113
|
+
|
|
114
|
+
findBySubject(params: {
|
|
115
|
+
tenantId: string;
|
|
116
|
+
channel: string;
|
|
117
|
+
channelInstallationId: string;
|
|
118
|
+
senderId: string;
|
|
119
|
+
}): Promise<Binding | null>;
|
|
120
|
+
|
|
38
121
|
resolve(params: {
|
|
39
122
|
channel: string;
|
|
40
123
|
senderId: string;
|
|
@@ -43,18 +126,11 @@ export interface BindingRegistry {
|
|
|
43
126
|
}): Promise<Binding | null>;
|
|
44
127
|
|
|
45
128
|
create(binding: CreateBindingInput): Promise<Binding>;
|
|
46
|
-
update(id: string, patch:
|
|
47
|
-
delete(id: string): Promise<void>;
|
|
129
|
+
update(tenantId: string, id: string, patch: BindingMutablePatch): Promise<Binding>;
|
|
130
|
+
delete(tenantId: string, id: string): Promise<void>;
|
|
48
131
|
|
|
49
|
-
list(params:
|
|
50
|
-
channel?: string;
|
|
51
|
-
agentId?: string;
|
|
52
|
-
tenantId: string;
|
|
53
|
-
channelInstallationId?: string;
|
|
54
|
-
limit?: number;
|
|
55
|
-
offset?: number;
|
|
56
|
-
}): Promise<Binding[]>;
|
|
132
|
+
list(params: BindingListParams): Promise<Binding[]>;
|
|
57
133
|
|
|
58
|
-
import(bindings: CreateBindingInput[]): Promise<Binding[]>;
|
|
134
|
+
import(tenantId: string, bindings: CreateBindingInput[]): Promise<Binding[]>;
|
|
59
135
|
export(params: { tenantId: string }): Promise<Binding[]>;
|
|
60
136
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/** Channel types persisted by the installation store, including internal-only channels. */
|
|
2
|
+
export type ChannelInstallationType = "lark" | "email" | "slack" | "wechat" | "room";
|
|
2
3
|
|
|
4
|
+
/** Channel types exposed by the public installation management API. */
|
|
5
|
+
export type PublicChannelInstallationType = Exclude<ChannelInstallationType, "room">;
|
|
6
|
+
|
|
7
|
+
/** Credentials and routing configuration for a Lark installation. */
|
|
3
8
|
export interface LarkChannelInstallationConfig {
|
|
4
9
|
appId: string;
|
|
5
10
|
appSecret: string;
|
|
@@ -8,11 +13,13 @@ export interface LarkChannelInstallationConfig {
|
|
|
8
13
|
assistantId?: string;
|
|
9
14
|
}
|
|
10
15
|
|
|
16
|
+
/** Credentials and identity configuration for a WeChat installation. */
|
|
11
17
|
export interface WechatChannelInstallationConfig {
|
|
12
18
|
botToken: string;
|
|
13
19
|
uin?: string;
|
|
14
20
|
}
|
|
15
21
|
|
|
22
|
+
/** A persisted tenant-scoped channel installation. */
|
|
16
23
|
export interface ChannelInstallation<TConfig = unknown> {
|
|
17
24
|
id: string;
|
|
18
25
|
tenantId: string;
|
|
@@ -26,15 +33,26 @@ export interface ChannelInstallation<TConfig = unknown> {
|
|
|
26
33
|
updatedAt: Date;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
|
|
36
|
+
/** Internal input accepted by installation stores. */
|
|
37
|
+
export interface CreateChannelInstallationInput {
|
|
30
38
|
channel: ChannelInstallationType;
|
|
31
39
|
name?: string;
|
|
32
|
-
config:
|
|
40
|
+
config: Record<string, unknown>;
|
|
33
41
|
enabled?: boolean;
|
|
34
42
|
fallbackAgentId?: string;
|
|
35
43
|
rejectWhenNoBinding?: boolean;
|
|
36
44
|
}
|
|
37
45
|
|
|
46
|
+
type PublicInstallationBase = Omit<CreateChannelInstallationInput, "channel" | "config">;
|
|
47
|
+
|
|
48
|
+
/** Public create request with channel-specific configuration and no internal room variant. */
|
|
49
|
+
export type CreateChannelInstallationRequest = PublicInstallationBase & (
|
|
50
|
+
| { channel: "lark"; config: LarkChannelInstallationConfig }
|
|
51
|
+
| { channel: "wechat"; config: WechatChannelInstallationConfig }
|
|
52
|
+
| { channel: "email" | "slack"; config: Record<string, unknown> }
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
/** Fields accepted when updating an existing public installation. */
|
|
38
56
|
export interface UpdateChannelInstallationRequest {
|
|
39
57
|
name?: string;
|
|
40
58
|
config?: Record<string, unknown>;
|
|
@@ -43,6 +61,7 @@ export interface UpdateChannelInstallationRequest {
|
|
|
43
61
|
rejectWhenNoBinding?: boolean;
|
|
44
62
|
}
|
|
45
63
|
|
|
64
|
+
/** Persistence boundary for tenant-scoped channel installations. */
|
|
46
65
|
export interface ChannelInstallationStore {
|
|
47
66
|
getInstallationById(
|
|
48
67
|
installationId: string,
|
|
@@ -64,7 +83,7 @@ export interface ChannelInstallationStore {
|
|
|
64
83
|
createInstallation(
|
|
65
84
|
tenantId: string,
|
|
66
85
|
installationId: string,
|
|
67
|
-
data:
|
|
86
|
+
data: CreateChannelInstallationInput,
|
|
68
87
|
): Promise<ChannelInstallation>;
|
|
69
88
|
|
|
70
89
|
updateInstallation(
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** A safely extracted value from an own enumerable data-property descriptor. */
|
|
2
|
+
export interface DescriptorDataValue {
|
|
3
|
+
ok: true;
|
|
4
|
+
value: unknown;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function ownDescriptorField(descriptor: object, key: string): unknown {
|
|
8
|
+
const field = Object.getOwnPropertyDescriptor(descriptor, key);
|
|
9
|
+
return field && Object.prototype.hasOwnProperty.call(field, "value")
|
|
10
|
+
? field.value
|
|
11
|
+
: undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Reads an own enumerable data-property descriptor without consulting its prototype.
|
|
16
|
+
*
|
|
17
|
+
* @param descriptor Property descriptor to inspect.
|
|
18
|
+
* @returns The descriptor value when it is an own enumerable data descriptor, otherwise `undefined`.
|
|
19
|
+
*/
|
|
20
|
+
export function descriptorDataValue(descriptor: unknown): DescriptorDataValue | undefined {
|
|
21
|
+
if (typeof descriptor !== "object" || descriptor === null) return undefined;
|
|
22
|
+
try {
|
|
23
|
+
const keys = Reflect.ownKeys(descriptor);
|
|
24
|
+
if (!keys.includes("value") || !keys.includes("enumerable")
|
|
25
|
+
|| keys.includes("get") || keys.includes("set")) return undefined;
|
|
26
|
+
const valueField = Object.getOwnPropertyDescriptor(descriptor, "value");
|
|
27
|
+
if (!valueField || !Object.prototype.hasOwnProperty.call(valueField, "value")
|
|
28
|
+
|| ownDescriptorField(descriptor, "enumerable") !== true) return undefined;
|
|
29
|
+
return { ok: true, value: valueField.value };
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Copies an object-like value into a local plain record using only exact own enumerable data descriptors.
|
|
37
|
+
*
|
|
38
|
+
* Prototypes are deliberately ignored so records from other JavaScript realms remain valid and inherited
|
|
39
|
+
* behavior can never participate in validation.
|
|
40
|
+
*
|
|
41
|
+
* @param value Untrusted value to snapshot.
|
|
42
|
+
* @param required Own string keys that must be present.
|
|
43
|
+
* @param optional Own string keys that may be present.
|
|
44
|
+
* @returns A canonical local record, or `undefined` for malformed descriptors, keys, arrays, or proxies.
|
|
45
|
+
*/
|
|
46
|
+
export function snapshotExactRecord(
|
|
47
|
+
value: unknown,
|
|
48
|
+
required: readonly string[],
|
|
49
|
+
optional: readonly string[] = [],
|
|
50
|
+
): Record<string, unknown> | undefined {
|
|
51
|
+
try {
|
|
52
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
53
|
+
const keys = Reflect.ownKeys(value);
|
|
54
|
+
const allowed = new Set([...required, ...optional]);
|
|
55
|
+
if (required.some((key) => !keys.includes(key))
|
|
56
|
+
|| keys.some((key) => typeof key !== "string" || !allowed.has(key))) return undefined;
|
|
57
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
58
|
+
const descriptorKeys = Reflect.ownKeys(descriptors);
|
|
59
|
+
if (descriptorKeys.length !== keys.length || keys.some((key) => !descriptorKeys.includes(key))) return undefined;
|
|
60
|
+
const result: Record<string, unknown> = {};
|
|
61
|
+
for (const key of keys) {
|
|
62
|
+
if (typeof key !== "string") return undefined;
|
|
63
|
+
const descriptor = descriptors[key];
|
|
64
|
+
const data = descriptorDataValue(descriptor);
|
|
65
|
+
if (!data) return undefined;
|
|
66
|
+
Object.defineProperty(result, key, {
|
|
67
|
+
value: data.value,
|
|
68
|
+
enumerable: true,
|
|
69
|
+
configurable: true,
|
|
70
|
+
writable: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Copies a dense cross-realm array using only own element data descriptors and the intrinsic length descriptor.
|
|
81
|
+
*
|
|
82
|
+
* @param value Untrusted value to snapshot.
|
|
83
|
+
* @returns A canonical local dense array, or `undefined` for holes, extras, accessors, or malformed proxies.
|
|
84
|
+
*/
|
|
85
|
+
export function snapshotExactArray(value: unknown): unknown[] | undefined {
|
|
86
|
+
try {
|
|
87
|
+
if (!Array.isArray(value)) return undefined;
|
|
88
|
+
const keys = Reflect.ownKeys(value);
|
|
89
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
90
|
+
const descriptorKeys = Reflect.ownKeys(descriptors);
|
|
91
|
+
if (descriptorKeys.length !== keys.length || keys.some((key) => !descriptorKeys.includes(key))) return undefined;
|
|
92
|
+
const lengthDescriptorField = Object.getOwnPropertyDescriptor(descriptors, "length");
|
|
93
|
+
const lengthDescriptor = lengthDescriptorField
|
|
94
|
+
&& Object.prototype.hasOwnProperty.call(lengthDescriptorField, "value")
|
|
95
|
+
? lengthDescriptorField.value
|
|
96
|
+
: undefined;
|
|
97
|
+
if (typeof lengthDescriptor !== "object" || lengthDescriptor === null
|
|
98
|
+
|| Reflect.ownKeys(lengthDescriptor).some((key) => key === "get" || key === "set")
|
|
99
|
+
|| !Object.prototype.hasOwnProperty.call(lengthDescriptor, "value")) return undefined;
|
|
100
|
+
const length = ownDescriptorField(lengthDescriptor, "value");
|
|
101
|
+
if (ownDescriptorField(lengthDescriptor, "enumerable") !== false
|
|
102
|
+
|| ownDescriptorField(lengthDescriptor, "configurable") !== false
|
|
103
|
+
|| ownDescriptorField(lengthDescriptor, "writable") !== true
|
|
104
|
+
|| !Number.isSafeInteger(length) || typeof length !== "number" || length < 0
|
|
105
|
+
|| keys.length !== length + 1) return undefined;
|
|
106
|
+
const result: unknown[] = [];
|
|
107
|
+
for (let index = 0; index < length; index += 1) {
|
|
108
|
+
const key = String(index);
|
|
109
|
+
const descriptor = descriptors[key];
|
|
110
|
+
const data = descriptorDataValue(descriptor);
|
|
111
|
+
if (!keys.includes(key) || !data) return undefined;
|
|
112
|
+
result.push(data.value);
|
|
113
|
+
}
|
|
114
|
+
if (keys.some((key) => typeof key !== "string" || (key !== "length" && !/^(0|[1-9]\d*)$/.test(key)))) return undefined;
|
|
115
|
+
return result;
|
|
116
|
+
} catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProjectBotMembership,
|
|
3
|
+
} from "./ProjectRoomProtocol";
|
|
4
|
+
|
|
5
|
+
/** Persistence operations for bot membership in a project room. */
|
|
6
|
+
export interface ProjectBotMembershipStore {
|
|
7
|
+
/** Lists bot memberships belonging to a project. */
|
|
8
|
+
list(tenantId: string, projectId: string): Promise<ProjectBotMembership[]>;
|
|
9
|
+
|
|
10
|
+
/** Finds a bot membership by identifier within a tenant. */
|
|
11
|
+
findById(tenantId: string, id: string): Promise<ProjectBotMembership | null>;
|
|
12
|
+
|
|
13
|
+
/** Finds a bot membership by assistant within a project. */
|
|
14
|
+
findByAssistant(tenantId: string, projectId: string, assistantId: string): Promise<ProjectBotMembership | null>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Saves a bot membership. A new row returns `created`, an existing active or
|
|
18
|
+
* paused row returns `updated`, and a removed row returns `reactivated`.
|
|
19
|
+
* `coordinator_conflict` means a non-removed
|
|
20
|
+
* coordinator already exists for the Tenant+Project; `mention_conflict`
|
|
21
|
+
* means the mention name is already used by an active or paused membership
|
|
22
|
+
* in the Tenant+room.
|
|
23
|
+
*/
|
|
24
|
+
save(
|
|
25
|
+
input: Omit<ProjectBotMembership, "joinedAt" | "updatedAt">,
|
|
26
|
+
): Promise<
|
|
27
|
+
| { kind: "created" | "updated" | "reactivated"; membership: ProjectBotMembership }
|
|
28
|
+
| { kind: "coordinator_conflict" | "mention_conflict" }
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Updates a bot membership using optimistic concurrency and uniqueness
|
|
33
|
+
* safeguards. `not_found` means the membership is not in the tenant;
|
|
34
|
+
* `conflict` means `updatedAt` differs from `expectedUpdatedAt`.
|
|
35
|
+
* `coordinator_conflict` applies to the one non-removed Coordinator per
|
|
36
|
+
* Tenant+Project rule; `mention_conflict` applies to mention uniqueness
|
|
37
|
+
* among active and paused memberships in the Tenant+room.
|
|
38
|
+
*/
|
|
39
|
+
update(input: {
|
|
40
|
+
tenantId: string;
|
|
41
|
+
id: string;
|
|
42
|
+
patch: Partial<Pick<ProjectBotMembership, "role" | "title" | "responsibility" | "mentionName" | "status">>;
|
|
43
|
+
expectedUpdatedAt: Date;
|
|
44
|
+
}): Promise<
|
|
45
|
+
| { kind: "updated"; membership: ProjectBotMembership }
|
|
46
|
+
| { kind: "not_found" | "conflict" | "coordinator_conflict" | "mention_conflict" }
|
|
47
|
+
>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProjectHumanRole,
|
|
3
|
+
ProjectMembership,
|
|
4
|
+
} from "./ProjectRoomProtocol";
|
|
5
|
+
|
|
6
|
+
/** Result of an atomic human membership mutation, including its committed row. */
|
|
7
|
+
export type ProjectMembershipMutationResult =
|
|
8
|
+
| { kind: "updated" | "removed"; membership: ProjectMembership }
|
|
9
|
+
| { kind: "not_found" | "conflict" | "last_owner" };
|
|
10
|
+
|
|
11
|
+
/** Persistence operations for human membership in a project. */
|
|
12
|
+
export interface ProjectMembershipStore {
|
|
13
|
+
/** Lists memberships belonging to a project. */
|
|
14
|
+
list(tenantId: string, projectId: string): Promise<ProjectMembership[]>;
|
|
15
|
+
|
|
16
|
+
/** Finds a user's membership in a project, if one exists. */
|
|
17
|
+
findByUser(tenantId: string, projectId: string, userId: string): Promise<ProjectMembership | null>;
|
|
18
|
+
|
|
19
|
+
/** Creates a project membership. */
|
|
20
|
+
create(input: Omit<ProjectMembership, "joinedAt" | "updatedAt">): Promise<ProjectMembership>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Atomically initializes an empty Project with its first owner.
|
|
24
|
+
* `created` means this call initialized the Project; `existing` means the
|
|
25
|
+
* requested user already owns the initialized Project and this was an
|
|
26
|
+
* idempotent retry; `already_initialized` means membership rows exist but
|
|
27
|
+
* this call did not produce either initial-owner outcome.
|
|
28
|
+
*/
|
|
29
|
+
createInitialOwner(
|
|
30
|
+
input: Omit<ProjectMembership, "role" | "status" | "joinedAt" | "updatedAt">,
|
|
31
|
+
): Promise<
|
|
32
|
+
| { kind: "created" | "existing"; membership: ProjectMembership }
|
|
33
|
+
| { kind: "already_initialized" }
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Updates a role within the tenant using optimistic concurrency and owner
|
|
38
|
+
* safeguards. `not_found` means the membership is not in the tenant;
|
|
39
|
+
* `conflict` means `updatedAt` differs from `expectedUpdatedAt`.
|
|
40
|
+
*/
|
|
41
|
+
updateRole(input: {
|
|
42
|
+
tenantId: string;
|
|
43
|
+
id: string;
|
|
44
|
+
role: ProjectHumanRole;
|
|
45
|
+
expectedUpdatedAt: Date;
|
|
46
|
+
}): Promise<ProjectMembershipMutationResult>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Removes a membership within the tenant using optimistic concurrency and
|
|
50
|
+
* owner safeguards. `not_found` means the membership is not in the tenant;
|
|
51
|
+
* `conflict` means `updatedAt` differs from `expectedUpdatedAt`.
|
|
52
|
+
*/
|
|
53
|
+
remove(input: {
|
|
54
|
+
tenantId: string;
|
|
55
|
+
id: string;
|
|
56
|
+
expectedUpdatedAt: Date;
|
|
57
|
+
}): Promise<ProjectMembershipMutationResult>;
|
|
58
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProjectRoomMessage,
|
|
3
|
+
ProjectRoomMessageCursor,
|
|
4
|
+
} from "./ProjectRoomProtocol";
|
|
5
|
+
|
|
6
|
+
/** Persistence operations for messages in a project room. */
|
|
7
|
+
export interface ProjectRoomMessageStore {
|
|
8
|
+
/** Creates a room message. */
|
|
9
|
+
create(input: Omit<ProjectRoomMessage, "createdAt">): Promise<ProjectRoomMessage>;
|
|
10
|
+
|
|
11
|
+
/** Creates or returns the existing room message for an idempotency key. */
|
|
12
|
+
createIdempotent(
|
|
13
|
+
input: Omit<ProjectRoomMessage, "createdAt"> & { idempotencyKey: string },
|
|
14
|
+
): Promise<ProjectRoomMessage>;
|
|
15
|
+
|
|
16
|
+
/** Lists messages strictly before an optional cursor, up to the requested limit. */
|
|
17
|
+
list(input: {
|
|
18
|
+
tenantId: string;
|
|
19
|
+
roomId: string;
|
|
20
|
+
before?: ProjectRoomMessageCursor;
|
|
21
|
+
limit: number;
|
|
22
|
+
}): Promise<ProjectRoomMessage[]>;
|
|
23
|
+
|
|
24
|
+
/** Finds a room message by identifier within a tenant. */
|
|
25
|
+
findById(tenantId: string, id: string): Promise<ProjectRoomMessage | null>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** Human access roles available within a project. */
|
|
2
|
+
export type ProjectHumanRole = "owner" | "admin" | "member" | "viewer";
|
|
3
|
+
|
|
4
|
+
/** Lifecycle states for a human project membership. */
|
|
5
|
+
export type ProjectMembershipStatus = "active" | "removed";
|
|
6
|
+
|
|
7
|
+
/** Roles a bot can hold within a project room. */
|
|
8
|
+
export type ProjectBotRole = "coordinator" | "specialist";
|
|
9
|
+
|
|
10
|
+
/** Lifecycle states for a bot room membership. */
|
|
11
|
+
export type ProjectBotMembershipStatus = "active" | "paused" | "removed";
|
|
12
|
+
|
|
13
|
+
/** Origins supported by project room messages. */
|
|
14
|
+
export type ProjectRoomMessageSource =
|
|
15
|
+
| "user"
|
|
16
|
+
| "agent"
|
|
17
|
+
| "task"
|
|
18
|
+
| "routine"
|
|
19
|
+
| "system";
|
|
20
|
+
|
|
21
|
+
/** The main room associated with a project. */
|
|
22
|
+
export interface ProjectRoom {
|
|
23
|
+
id: string;
|
|
24
|
+
tenantId: string;
|
|
25
|
+
workspaceId: string;
|
|
26
|
+
projectId: string;
|
|
27
|
+
type: "main";
|
|
28
|
+
name: string;
|
|
29
|
+
createdAt: Date;
|
|
30
|
+
updatedAt: Date;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A user's membership and access role within a project. */
|
|
34
|
+
export interface ProjectMembership {
|
|
35
|
+
id: string;
|
|
36
|
+
tenantId: string;
|
|
37
|
+
projectId: string;
|
|
38
|
+
userId: string;
|
|
39
|
+
role: ProjectHumanRole;
|
|
40
|
+
status: ProjectMembershipStatus;
|
|
41
|
+
joinedAt: Date;
|
|
42
|
+
updatedAt: Date;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A bot's role, presentation, and execution thread within a project room. */
|
|
46
|
+
export interface ProjectBotMembership {
|
|
47
|
+
id: string;
|
|
48
|
+
tenantId: string;
|
|
49
|
+
workspaceId: string;
|
|
50
|
+
projectId: string;
|
|
51
|
+
roomId: string;
|
|
52
|
+
assistantId: string;
|
|
53
|
+
role: ProjectBotRole;
|
|
54
|
+
title: string;
|
|
55
|
+
responsibility?: string;
|
|
56
|
+
mentionName: string;
|
|
57
|
+
status: ProjectBotMembershipStatus;
|
|
58
|
+
roomThreadId: string;
|
|
59
|
+
joinedAt: Date;
|
|
60
|
+
updatedAt: Date;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Identifies a human author of a project room message. */
|
|
64
|
+
interface ProjectRoomMessageHumanAuthor {
|
|
65
|
+
type: "human";
|
|
66
|
+
userId: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Identifies a bot membership as the author of a project room message. */
|
|
70
|
+
interface ProjectRoomMessageBotAuthor {
|
|
71
|
+
type: "bot";
|
|
72
|
+
membershipId: string;
|
|
73
|
+
assistantId: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Identifies the system as the author of a project room message. */
|
|
77
|
+
interface ProjectRoomMessageSystemAuthor {
|
|
78
|
+
type: "system";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The discriminated author variants supported by project room messages. */
|
|
82
|
+
export type ProjectRoomMessageAuthor =
|
|
83
|
+
| ProjectRoomMessageHumanAuthor
|
|
84
|
+
| ProjectRoomMessageBotAuthor
|
|
85
|
+
| ProjectRoomMessageSystemAuthor;
|
|
86
|
+
|
|
87
|
+
/** A bot or the whole team targeted by a room message. */
|
|
88
|
+
export type ProjectRoomMention =
|
|
89
|
+
| { type: "bot"; membershipId: string }
|
|
90
|
+
| { type: "team" };
|
|
91
|
+
|
|
92
|
+
/** Text payload carried by a project room message. */
|
|
93
|
+
interface ProjectRoomTextContent {
|
|
94
|
+
type: "text";
|
|
95
|
+
text: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A message posted to a project room. */
|
|
99
|
+
export interface ProjectRoomMessage {
|
|
100
|
+
id: string;
|
|
101
|
+
tenantId: string;
|
|
102
|
+
workspaceId: string;
|
|
103
|
+
projectId: string;
|
|
104
|
+
roomId: string;
|
|
105
|
+
author: ProjectRoomMessageAuthor;
|
|
106
|
+
content: ProjectRoomTextContent;
|
|
107
|
+
mentions: ProjectRoomMention[];
|
|
108
|
+
replyToMessageId?: string;
|
|
109
|
+
source: ProjectRoomMessageSource;
|
|
110
|
+
sourceId?: string;
|
|
111
|
+
idempotencyKey?: string;
|
|
112
|
+
createdAt: Date;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Stable position used to page through project room messages. */
|
|
116
|
+
export interface ProjectRoomMessageCursor {
|
|
117
|
+
createdAt: Date;
|
|
118
|
+
id: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Metadata linking a room-presence thread to its project room bot membership. */
|
|
122
|
+
export interface ProjectRoomThreadMetadata {
|
|
123
|
+
source: "project_room";
|
|
124
|
+
kind: "room_presence";
|
|
125
|
+
workspaceId: string;
|
|
126
|
+
projectId: string;
|
|
127
|
+
roomId: string;
|
|
128
|
+
membershipId: string;
|
|
129
|
+
assistantId: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Metadata linking a task-execution thread to its owning project task and bot membership. */
|
|
133
|
+
export interface ProjectTaskThreadMetadata {
|
|
134
|
+
source: "project_task";
|
|
135
|
+
kind: "task_execution";
|
|
136
|
+
workspaceId: string;
|
|
137
|
+
projectId: string;
|
|
138
|
+
roomId: string;
|
|
139
|
+
membershipId: string;
|
|
140
|
+
assistantId: string;
|
|
141
|
+
taskId: string;
|
|
142
|
+
parentTaskId?: string;
|
|
143
|
+
}
|