@parlor/chat 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +23 -2
- package/dist/events.d.ts +4 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +12 -0
- package/dist/events.js.map +1 -0
- package/dist/id-schemas.d.ts +5 -0
- package/dist/id-schemas.d.ts.map +1 -0
- package/dist/id-schemas.js +11 -0
- package/dist/id-schemas.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/items.d.ts +295 -0
- package/dist/items.d.ts.map +1 -0
- package/dist/items.js +113 -0
- package/dist/items.js.map +1 -0
- package/dist/limits.d.ts +4 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +4 -0
- package/dist/limits.js.map +1 -0
- package/dist/model.d.ts +143 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +2 -0
- package/dist/model.js.map +1 -0
- package/dist/open-turns.d.ts +19 -0
- package/dist/open-turns.d.ts.map +1 -0
- package/dist/open-turns.js +31 -0
- package/dist/open-turns.js.map +1 -0
- package/dist/protocol.d.ts +69 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +80 -0
- package/dist/protocol.js.map +1 -0
- package/dist/server/actions.d.ts +561 -0
- package/dist/server/actions.d.ts.map +1 -0
- package/dist/server/actions.js +422 -0
- package/dist/server/actions.js.map +1 -0
- package/dist/server/context.d.ts +24 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +77 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/index.d.ts +50 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +145 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/live.d.ts +24 -0
- package/dist/server/live.d.ts.map +1 -0
- package/dist/server/live.js +130 -0
- package/dist/server/live.js.map +1 -0
- package/dist/server/migrations.d.ts +3 -0
- package/dist/server/migrations.d.ts.map +1 -0
- package/dist/server/migrations.js +96 -0
- package/dist/server/migrations.js.map +1 -0
- package/dist/server/party.d.ts +24 -0
- package/dist/server/party.d.ts.map +1 -0
- package/dist/server/party.js +38 -0
- package/dist/server/party.js.map +1 -0
- package/dist/server/routes.d.ts +17 -0
- package/dist/server/routes.d.ts.map +1 -0
- package/dist/server/routes.js +105 -0
- package/dist/server/routes.js.map +1 -0
- package/dist/server/rows.d.ts +49 -0
- package/dist/server/rows.d.ts.map +1 -0
- package/dist/server/rows.js +136 -0
- package/dist/server/rows.js.map +1 -0
- package/dist/server/store.d.ts +130 -0
- package/dist/server/store.d.ts.map +1 -0
- package/dist/server/store.js +539 -0
- package/dist/server/store.js.map +1 -0
- package/dist/tags.d.ts +7 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/tags.js +7 -0
- package/dist/tags.js.map +1 -0
- package/dist/testing/index.d.ts +46 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +96 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/view.d.ts +87 -0
- package/dist/view.d.ts.map +1 -0
- package/dist/view.js +236 -0
- package/dist/view.js.map +1 -0
- package/package.json +47 -3
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type DataProvider, type Principal, type UserId } from "@parlor/core";
|
|
2
|
+
import { type ServerPlugin } from "@parlor/server";
|
|
3
|
+
import { type LiveRelay } from "./live.js";
|
|
4
|
+
export { CHAT_ACTIVITY_TAG, CHAT_TAG, type ChatActionDeps, type ChatActionMap, type ChatActionRegistry, chatActions, type ChatHostActionMap, type ChatUserActionMap, HOST_GRANTS_TAG, hostChatActions, HOSTS_TAG, userChatActions, } from "./actions.js";
|
|
5
|
+
export { TRANSCRIPT_BUDGET_BYTES, turnContext, type TurnContextInput, } from "./context.js";
|
|
6
|
+
export { chatItemBodies, hostItemDraft } from "../items.js";
|
|
7
|
+
export { idSchema } from "../id-schemas.js";
|
|
8
|
+
export { inProcessRelay, type LiveRelay, MAX_OPEN_BYTES_PER_TURN, MAX_OPEN_TURNS, } from "./live.js";
|
|
9
|
+
export { CHAT_MIGRATIONS } from "./migrations.js";
|
|
10
|
+
export { CHAT_PLUGIN } from "../protocol.js";
|
|
11
|
+
export { type ChatActionConfig, chatAction, type ChatHost, type ChatUser, type Party, type PartyOf, } from "./party.js";
|
|
12
|
+
export { chatRoutes, type ChatRouteDeps, MAX_STREAM_BODY_BYTES } from "./routes.js";
|
|
13
|
+
export { type ChatDb, type ChatStore, type Lapsed, type NewTurn, sqlChatStore, type StreamTurn, type TurnHolder, type UserDraft, } from "./store.js";
|
|
14
|
+
/** Chat then serves nothing, so the client reads `useChat().status` as `unavailable`. */
|
|
15
|
+
export declare const UNAVAILABLE_WITHOUT_AUTH = "chat() is unavailable: no auth provider is configured, so every action runs without a signed-in user.";
|
|
16
|
+
export declare const AUTH_UNDEFINED = "auth is undefined. Check the environment variables your auth setup reads, or remove the auth option to run without sign-in.";
|
|
17
|
+
/** Outlives a 30 s network drop plus a 5 s reconnect. */
|
|
18
|
+
export declare const DEFAULT_LEASE_MS = 60000;
|
|
19
|
+
export declare const DEFAULT_HEARTBEAT_MS = 30000;
|
|
20
|
+
export declare const DEFAULT_MAX_QUEUED = 4;
|
|
21
|
+
/** Who asks to use the chat, with what the server knows to decide. */
|
|
22
|
+
export interface ChatAccessRequest {
|
|
23
|
+
/** Whose chat it is: the signed-in user, or the user a connected computer works for. */
|
|
24
|
+
readonly userId: UserId;
|
|
25
|
+
readonly principal: Principal;
|
|
26
|
+
readonly scope: string;
|
|
27
|
+
readonly database: DataProvider;
|
|
28
|
+
}
|
|
29
|
+
export type ChatAccess = (request: ChatAccessRequest) => boolean | Promise<boolean>;
|
|
30
|
+
export interface ChatOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Who may use the agent; everyone signed in when absent. A refused user can neither read
|
|
33
|
+
* nor send, their computers take no turns, and auth pairs no computer or device for them.
|
|
34
|
+
*/
|
|
35
|
+
readonly access?: ChatAccess | undefined;
|
|
36
|
+
/** App notes for the agent in every fresh session's context, such as an AGENTS.md excerpt. */
|
|
37
|
+
readonly instructions?: string | undefined;
|
|
38
|
+
readonly leaseMs?: number | undefined;
|
|
39
|
+
readonly maxQueued?: number | undefined;
|
|
40
|
+
/** Defaults to `inProcessRelay`; a multi-instance deployment passes one that crosses instances. */
|
|
41
|
+
readonly relay?: (() => LiveRelay) | undefined;
|
|
42
|
+
}
|
|
43
|
+
/** A lease past the revocation memory would outlive the revocation of its grant. */
|
|
44
|
+
export declare const MAX_LEASE_MS: number;
|
|
45
|
+
/**
|
|
46
|
+
* The inline agent's server half: the `parlor_chat_*` log, the user and host actions, the
|
|
47
|
+
* `chat` channel, and the host-only stream and heartbeat routes.
|
|
48
|
+
*/
|
|
49
|
+
export declare function chat(options?: ChatOptions): ServerPlugin;
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,YAAY,EAEjB,KAAK,SAAS,EAEd,KAAK,MAAM,EAEZ,MAAM,cAAc,CAAC;AACtB,OAAO,EAAyB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAK1E,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAK3D,OAAO,EACL,iBAAiB,EACjB,QAAQ,EACR,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,WAAW,EACX,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,eAAe,EACf,eAAe,EACf,SAAS,EACT,eAAe,GAChB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,uBAAuB,EACvB,WAAW,EACX,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EACL,cAAc,EACd,KAAK,SAAS,EACd,uBAAuB,EACvB,cAAc,GACf,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EACL,KAAK,gBAAgB,EACrB,UAAU,EACV,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,KAAK,EACV,KAAK,OAAO,GACb,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,UAAU,EAAE,KAAK,aAAa,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EACL,KAAK,MAAM,EACX,KAAK,SAAS,EACd,KAAK,MAAM,EACX,KAAK,OAAO,EACZ,YAAY,EACZ,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,SAAS,GACf,MAAM,YAAY,CAAC;AAEpB,yFAAyF;AACzF,eAAO,MAAM,wBAAwB,0GACoE,CAAC;AAE1G,eAAO,MAAM,cAAc,gIACoG,CAAC;AAEhI,yDAAyD;AACzD,eAAO,MAAM,gBAAgB,QAAS,CAAC;AACvC,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAC3C,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,sEAAsE;AACtE,MAAM,WAAW,iBAAiB;IAChC,wFAAwF;IACxF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;CACjC;AAED,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpF,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IACzC,8FAA8F;IAC9F,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,mGAAmG;IACnG,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,SAAS,CAAC,GAAG,SAAS,CAAC;CAChD;AAED,oFAAoF;AACpF,eAAO,MAAM,YAAY,QAAwB,CAAC;AASlD;;;GAGG;AACH,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,YAAY,CA+G5D"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { userAudience } from "@parlor/agent-ui/server";
|
|
2
|
+
import { DEV_USER_ID, userOf, } from "@parlor/core";
|
|
3
|
+
import { REVOCATION_HORIZON_MS } from "@parlor/server";
|
|
4
|
+
import { CHAT_PLUGIN } from "../protocol.js";
|
|
5
|
+
import { CHAT_TAG, chatActions, HOSTS_TAG } from "./actions.js";
|
|
6
|
+
import { inProcessRelay } from "./live.js";
|
|
7
|
+
import { CHAT_MIGRATIONS } from "./migrations.js";
|
|
8
|
+
import { chatRoutes } from "./routes.js";
|
|
9
|
+
import { sqlChatStore } from "./store.js";
|
|
10
|
+
export { CHAT_ACTIVITY_TAG, CHAT_TAG, chatActions, HOST_GRANTS_TAG, hostChatActions, HOSTS_TAG, userChatActions, } from "./actions.js";
|
|
11
|
+
export { TRANSCRIPT_BUDGET_BYTES, turnContext, } from "./context.js";
|
|
12
|
+
export { chatItemBodies, hostItemDraft } from "../items.js";
|
|
13
|
+
export { idSchema } from "../id-schemas.js";
|
|
14
|
+
export { inProcessRelay, MAX_OPEN_BYTES_PER_TURN, MAX_OPEN_TURNS, } from "./live.js";
|
|
15
|
+
export { CHAT_MIGRATIONS } from "./migrations.js";
|
|
16
|
+
export { CHAT_PLUGIN } from "../protocol.js";
|
|
17
|
+
export { chatAction, } from "./party.js";
|
|
18
|
+
export { chatRoutes, MAX_STREAM_BODY_BYTES } from "./routes.js";
|
|
19
|
+
export { sqlChatStore, } from "./store.js";
|
|
20
|
+
/** Chat then serves nothing, so the client reads `useChat().status` as `unavailable`. */
|
|
21
|
+
export const UNAVAILABLE_WITHOUT_AUTH = "chat() is unavailable: no auth provider is configured, so every action runs without a signed-in user.";
|
|
22
|
+
export const AUTH_UNDEFINED = "auth is undefined. Check the environment variables your auth setup reads, or remove the auth option to run without sign-in.";
|
|
23
|
+
/** Outlives a 30 s network drop plus a 5 s reconnect. */
|
|
24
|
+
export const DEFAULT_LEASE_MS = 60_000;
|
|
25
|
+
export const DEFAULT_HEARTBEAT_MS = 30_000;
|
|
26
|
+
export const DEFAULT_MAX_QUEUED = 4;
|
|
27
|
+
/** A lease past the revocation memory would outlive the revocation of its grant. */
|
|
28
|
+
export const MAX_LEASE_MS = REVOCATION_HORIZON_MS;
|
|
29
|
+
function positiveInteger(name, value) {
|
|
30
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
31
|
+
throw new Error(`chat(): ${name} must be a positive whole number, got ${value}.`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The inline agent's server half: the `parlor_chat_*` log, the user and host actions, the
|
|
37
|
+
* `chat` channel, and the host-only stream and heartbeat routes.
|
|
38
|
+
*/
|
|
39
|
+
export function chat(options = {}) {
|
|
40
|
+
return {
|
|
41
|
+
name: CHAT_PLUGIN,
|
|
42
|
+
migrations: CHAT_MIGRATIONS,
|
|
43
|
+
setup(host) {
|
|
44
|
+
const leaseMs = positiveInteger("leaseMs", options.leaseMs ?? DEFAULT_LEASE_MS);
|
|
45
|
+
const maxQueued = positiveInteger("maxQueued", options.maxQueued ?? DEFAULT_MAX_QUEUED);
|
|
46
|
+
if (leaseMs > MAX_LEASE_MS) {
|
|
47
|
+
throw new Error(`chat(): leaseMs ${leaseMs} is above ${MAX_LEASE_MS}, the longest a revoked computer's turn token can be kept out.`);
|
|
48
|
+
}
|
|
49
|
+
const { auth, database } = host;
|
|
50
|
+
if (!database)
|
|
51
|
+
throw new Error("chat() needs a database.");
|
|
52
|
+
if (!auth?.delegate) {
|
|
53
|
+
if (host.authMisconfigured)
|
|
54
|
+
throw new Error(AUTH_UNDEFINED);
|
|
55
|
+
console.warn(UNAVAILABLE_WITHOUT_AUTH);
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
const relay = (options.relay ?? inProcessRelay)();
|
|
59
|
+
const db = () => database.read();
|
|
60
|
+
const ownStream = (owner) => userAudience(owner.scope, owner.userId);
|
|
61
|
+
const transact = (owner, work) => database.transaction(async (tx) => {
|
|
62
|
+
const { result, threadId } = await work(tx);
|
|
63
|
+
if (threadId !== undefined)
|
|
64
|
+
await host.announce(ownStream(owner), [[CHAT_TAG, threadId]], tx);
|
|
65
|
+
return result;
|
|
66
|
+
});
|
|
67
|
+
const heartbeatMs = DEFAULT_HEARTBEAT_MS;
|
|
68
|
+
const reconcile = async (owner) => {
|
|
69
|
+
if (!(await sqlChatStore.hasLapsed(db(), owner)))
|
|
70
|
+
return;
|
|
71
|
+
await database.transaction(async (tx) => {
|
|
72
|
+
for (const change of await sqlChatStore.expireLapsed(tx, owner)) {
|
|
73
|
+
const tags = [
|
|
74
|
+
...change.threads.map((threadId) => [CHAT_TAG, threadId]),
|
|
75
|
+
...change.hosts.map((hostId) => [HOSTS_TAG, hostId]),
|
|
76
|
+
];
|
|
77
|
+
await host.announce(ownStream(change), tags, tx);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
const pruneHosts = async (owner, hostIds) => {
|
|
82
|
+
try {
|
|
83
|
+
await database.transaction((tx) => sqlChatStore.pruneHosts(tx, owner, hostIds));
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.warn("[parlor] chat: pruning revoked computers failed:", error);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
let sweep;
|
|
90
|
+
const actions = chatActions({
|
|
91
|
+
enforcing: auth.enforcing,
|
|
92
|
+
transact,
|
|
93
|
+
reconcile,
|
|
94
|
+
pruneHosts,
|
|
95
|
+
auth,
|
|
96
|
+
store: sqlChatStore,
|
|
97
|
+
relay,
|
|
98
|
+
leaseMs,
|
|
99
|
+
heartbeatMs,
|
|
100
|
+
maxQueued,
|
|
101
|
+
instructions: options.instructions,
|
|
102
|
+
appName: process.env.PARLOR_APP_NAME ?? "this app",
|
|
103
|
+
appOrigin: () => process.env.PARLOR_ORIGIN ?? process.env.PARLOR_WEB_ORIGIN ?? "",
|
|
104
|
+
});
|
|
105
|
+
const { access } = options;
|
|
106
|
+
return {
|
|
107
|
+
actions,
|
|
108
|
+
...(access
|
|
109
|
+
? {
|
|
110
|
+
allows: ({ principal, scope }) => access({
|
|
111
|
+
userId: userOf(principal) ?? DEV_USER_ID,
|
|
112
|
+
principal,
|
|
113
|
+
scope,
|
|
114
|
+
database,
|
|
115
|
+
}),
|
|
116
|
+
}
|
|
117
|
+
: {}),
|
|
118
|
+
routes: chatRoutes({
|
|
119
|
+
db,
|
|
120
|
+
store: sqlChatStore,
|
|
121
|
+
relay,
|
|
122
|
+
heartbeatMs,
|
|
123
|
+
reconcile,
|
|
124
|
+
announceHost: (owner, hostId) => host.announce(ownStream(owner), [[HOSTS_TAG, hostId]]),
|
|
125
|
+
}),
|
|
126
|
+
channel: relay.channel,
|
|
127
|
+
start: () => {
|
|
128
|
+
sweep = setInterval(() => {
|
|
129
|
+
(host.ready?.() ?? Promise.resolve())
|
|
130
|
+
.then(() => reconcile())
|
|
131
|
+
.catch((error) => {
|
|
132
|
+
console.warn("[parlor] chat: sweeping lapsed turns failed:", error);
|
|
133
|
+
});
|
|
134
|
+
}, heartbeatMs);
|
|
135
|
+
sweep.unref?.();
|
|
136
|
+
},
|
|
137
|
+
close: () => {
|
|
138
|
+
clearInterval(sweep);
|
|
139
|
+
relay.close();
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAEL,WAAW,EAIX,MAAM,GACP,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,qBAAqB,EAAqB,MAAM,gBAAgB,CAAC;AAG1E,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAuB,WAAW,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,cAAc,EAAkB,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAe,YAAY,EAAE,MAAM,YAAY,CAAC;AAEvD,OAAO,EACL,iBAAiB,EACjB,QAAQ,EAIR,WAAW,EAGX,eAAe,EACf,eAAe,EACf,SAAS,EACT,eAAe,GAChB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,uBAAuB,EACvB,WAAW,GAEZ,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EACL,cAAc,EAEd,uBAAuB,EACvB,cAAc,GACf,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAEL,UAAU,GAKX,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,UAAU,EAAsB,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EAKL,YAAY,GAIb,MAAM,YAAY,CAAC;AAEpB,yFAAyF;AACzF,MAAM,CAAC,MAAM,wBAAwB,GACnC,uGAAuG,CAAC;AAE1G,MAAM,CAAC,MAAM,cAAc,GACzB,6HAA6H,CAAC;AAEhI,yDAAyD;AACzD,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;AACvC,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAC3C,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AA2BpC,oFAAoF;AACpF,MAAM,CAAC,MAAM,YAAY,GAAG,qBAAqB,CAAC;AAElD,SAAS,eAAe,CAAC,IAAY,EAAE,KAAa;IAClD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,yCAAyC,KAAK,GAAG,CAAC,CAAC;IACpF,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,IAAI,CAAC,OAAO,GAAgB,EAAE;IAC5C,OAAO;QACL,IAAI,EAAE,WAAW;QACjB,UAAU,EAAE,eAAe;QAC3B,KAAK,CAAC,IAAI;YACR,MAAM,OAAO,GAAG,eAAe,CAAC,SAAS,EAAE,OAAO,CAAC,OAAO,IAAI,gBAAgB,CAAC,CAAC;YAChF,MAAM,SAAS,GAAG,eAAe,CAC/B,WAAW,EACX,OAAO,CAAC,SAAS,IAAI,kBAAkB,CACxC,CAAC;YACF,IAAI,OAAO,GAAG,YAAY,EAAE,CAAC;gBAC3B,MAAM,IAAI,KAAK,CACb,mBAAmB,OAAO,aAAa,YAAY,gEAAgE,CACpH,CAAC;YACJ,CAAC;YACD,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;YAChC,IAAI,CAAC,QAAQ;gBAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;YAC3D,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC;gBACpB,IAAI,IAAI,CAAC,iBAAiB;oBAAE,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC;gBAC5D,OAAO,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;gBACvC,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,cAAc,CAAC,EAAE,CAAC;YAClD,MAAM,EAAE,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAY,CAAC;YAC3C,MAAM,SAAS,GAAG,CAAC,KAAgB,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;YAChF,MAAM,QAAQ,GAA+B,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAC3D,QAAQ,CAAC,WAAW,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;gBAChC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,IAAI,CAAC,EAAY,CAAC,CAAC;gBACtD,IAAI,QAAQ,KAAK,SAAS;oBACxB,MAAM,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACpE,OAAO,MAAM,CAAC;YAChB,CAAC,CAAC,CAAC;YACL,MAAM,WAAW,GAAG,oBAAoB,CAAC;YACzC,MAAM,SAAS,GAAG,KAAK,EAAE,KAAiB,EAAE,EAAE;gBAC5C,IAAI,CAAC,CAAC,MAAM,YAAY,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;oBAAE,OAAO;gBACzD,MAAM,QAAQ,CAAC,WAAW,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;oBACtC,KAAK,MAAM,MAAM,IAAI,MAAM,YAAY,CAAC,YAAY,CAAC,EAAY,EAAE,KAAK,CAAC,EAAE,CAAC;wBAC1E,MAAM,IAAI,GAAU;4BAClB,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;4BACzD,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;yBACrD,CAAC;wBACF,MAAM,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;oBACnD,CAAC;gBACH,CAAC,CAAC,CAAC;YACL,CAAC,CAAC;YACF,MAAM,UAAU,GAAiC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;gBACxE,IAAI,CAAC;oBACH,MAAM,QAAQ,CAAC,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAChC,YAAY,CAAC,UAAU,CAAC,EAAY,EAAE,KAAK,EAAE,OAAO,CAAC,CACtD,CAAC;gBACJ,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,OAAO,CAAC,IAAI,CAAC,kDAAkD,EAAE,KAAK,CAAC,CAAC;gBAC1E,CAAC;YACH,CAAC,CAAC;YACF,IAAI,KAAiD,CAAC;YACtD,MAAM,OAAO,GAAG,WAAW,CAAC;gBAC1B,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,QAAQ;gBACR,SAAS;gBACT,UAAU;gBACV,IAAI;gBACJ,KAAK,EAAE,YAAY;gBACnB,KAAK;gBACL,OAAO;gBACP,WAAW;gBACX,SAAS;gBACT,YAAY,EAAE,OAAO,CAAC,YAAY;gBAClC,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,UAAU;gBAClD,SAAS,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,EAAE;aAClF,CAAC,CAAC;YACH,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;YAC3B,OAAO;gBACL,OAAO;gBACP,GAAG,CAAC,MAAM;oBACR,CAAC,CAAC;wBACE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE,CAC/B,MAAM,CAAC;4BACL,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC,IAAI,WAAW;4BACxC,SAAS;4BACT,KAAK;4BACL,QAAQ;yBACT,CAAC;qBACL;oBACH,CAAC,CAAC,EAAE,CAAC;gBACP,MAAM,EAAE,UAAU,CAAC;oBACjB,EAAE;oBACF,KAAK,EAAE,YAAY;oBACnB,KAAK;oBACL,WAAW;oBACX,SAAS;oBACT,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAC9B,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;iBACzD,CAAC;gBACF,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,KAAK,EAAE,GAAG,EAAE;oBACV,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;wBACvB,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;6BAClC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;6BACvB,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;4BACxB,OAAO,CAAC,IAAI,CAAC,8CAA8C,EAAE,KAAK,CAAC,CAAC;wBACtE,CAAC,CAAC,CAAC;oBACP,CAAC,EAAE,WAAW,CAAC,CAAC;oBAChB,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;gBAClB,CAAC;gBACD,KAAK,EAAE,GAAG,EAAE;oBACV,aAAa,CAAC,KAAK,CAAC,CAAC;oBACrB,KAAK,CAAC,KAAK,EAAE,CAAC;gBAChB,CAAC;aACF,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { userAudience } from \"@parlor/agent-ui/server\";\nimport {\n type DataProvider,\n DEV_USER_ID,\n type Principal,\n type Tag,\n type UserId,\n userOf,\n} from \"@parlor/core\";\nimport { REVOCATION_HORIZON_MS, type ServerPlugin } from \"@parlor/server\";\n\nimport type { ChatOwner } from \"../model.js\";\nimport { CHAT_PLUGIN } from \"../protocol.js\";\nimport { CHAT_TAG, type ChatActionDeps, chatActions, HOSTS_TAG } from \"./actions.js\";\nimport { inProcessRelay, type LiveRelay } from \"./live.js\";\nimport { CHAT_MIGRATIONS } from \"./migrations.js\";\nimport { chatRoutes } from \"./routes.js\";\nimport { type ChatDb, sqlChatStore } from \"./store.js\";\n\nexport {\n CHAT_ACTIVITY_TAG,\n CHAT_TAG,\n type ChatActionDeps,\n type ChatActionMap,\n type ChatActionRegistry,\n chatActions,\n type ChatHostActionMap,\n type ChatUserActionMap,\n HOST_GRANTS_TAG,\n hostChatActions,\n HOSTS_TAG,\n userChatActions,\n} from \"./actions.js\";\nexport {\n TRANSCRIPT_BUDGET_BYTES,\n turnContext,\n type TurnContextInput,\n} from \"./context.js\";\nexport { chatItemBodies, hostItemDraft } from \"../items.js\";\nexport { idSchema } from \"../id-schemas.js\";\nexport {\n inProcessRelay,\n type LiveRelay,\n MAX_OPEN_BYTES_PER_TURN,\n MAX_OPEN_TURNS,\n} from \"./live.js\";\nexport { CHAT_MIGRATIONS } from \"./migrations.js\";\nexport { CHAT_PLUGIN } from \"../protocol.js\";\nexport {\n type ChatActionConfig,\n chatAction,\n type ChatHost,\n type ChatUser,\n type Party,\n type PartyOf,\n} from \"./party.js\";\nexport { chatRoutes, type ChatRouteDeps, MAX_STREAM_BODY_BYTES } from \"./routes.js\";\nexport {\n type ChatDb,\n type ChatStore,\n type Lapsed,\n type NewTurn,\n sqlChatStore,\n type StreamTurn,\n type TurnHolder,\n type UserDraft,\n} from \"./store.js\";\n\n/** Chat then serves nothing, so the client reads `useChat().status` as `unavailable`. */\nexport const UNAVAILABLE_WITHOUT_AUTH =\n \"chat() is unavailable: no auth provider is configured, so every action runs without a signed-in user.\";\n\nexport const AUTH_UNDEFINED =\n \"auth is undefined. Check the environment variables your auth setup reads, or remove the auth option to run without sign-in.\";\n\n/** Outlives a 30 s network drop plus a 5 s reconnect. */\nexport const DEFAULT_LEASE_MS = 60_000;\nexport const DEFAULT_HEARTBEAT_MS = 30_000;\nexport const DEFAULT_MAX_QUEUED = 4;\n\n/** Who asks to use the chat, with what the server knows to decide. */\nexport interface ChatAccessRequest {\n /** Whose chat it is: the signed-in user, or the user a connected computer works for. */\n readonly userId: UserId;\n readonly principal: Principal;\n readonly scope: string;\n readonly database: DataProvider;\n}\n\nexport type ChatAccess = (request: ChatAccessRequest) => boolean | Promise<boolean>;\n\nexport interface ChatOptions {\n /**\n * Who may use the agent; everyone signed in when absent. A refused user can neither read\n * nor send, their computers take no turns, and auth pairs no computer or device for them.\n */\n readonly access?: ChatAccess | undefined;\n /** App notes for the agent in every fresh session's context, such as an AGENTS.md excerpt. */\n readonly instructions?: string | undefined;\n readonly leaseMs?: number | undefined;\n readonly maxQueued?: number | undefined;\n /** Defaults to `inProcessRelay`; a multi-instance deployment passes one that crosses instances. */\n readonly relay?: (() => LiveRelay) | undefined;\n}\n\n/** A lease past the revocation memory would outlive the revocation of its grant. */\nexport const MAX_LEASE_MS = REVOCATION_HORIZON_MS;\n\nfunction positiveInteger(name: string, value: number): number {\n if (!Number.isInteger(value) || value < 1) {\n throw new Error(`chat(): ${name} must be a positive whole number, got ${value}.`);\n }\n return value;\n}\n\n/**\n * The inline agent's server half: the `parlor_chat_*` log, the user and host actions, the\n * `chat` channel, and the host-only stream and heartbeat routes.\n */\nexport function chat(options: ChatOptions = {}): ServerPlugin {\n return {\n name: CHAT_PLUGIN,\n migrations: CHAT_MIGRATIONS,\n setup(host) {\n const leaseMs = positiveInteger(\"leaseMs\", options.leaseMs ?? DEFAULT_LEASE_MS);\n const maxQueued = positiveInteger(\n \"maxQueued\",\n options.maxQueued ?? DEFAULT_MAX_QUEUED,\n );\n if (leaseMs > MAX_LEASE_MS) {\n throw new Error(\n `chat(): leaseMs ${leaseMs} is above ${MAX_LEASE_MS}, the longest a revoked computer's turn token can be kept out.`,\n );\n }\n const { auth, database } = host;\n if (!database) throw new Error(\"chat() needs a database.\");\n if (!auth?.delegate) {\n if (host.authMisconfigured) throw new Error(AUTH_UNDEFINED);\n console.warn(UNAVAILABLE_WITHOUT_AUTH);\n return {};\n }\n const relay = (options.relay ?? inProcessRelay)();\n const db = () => database.read() as ChatDb;\n const ownStream = (owner: ChatOwner) => userAudience(owner.scope, owner.userId);\n const transact: ChatActionDeps[\"transact\"] = (owner, work) =>\n database.transaction(async (tx) => {\n const { result, threadId } = await work(tx as ChatDb);\n if (threadId !== undefined)\n await host.announce(ownStream(owner), [[CHAT_TAG, threadId]], tx);\n return result;\n });\n const heartbeatMs = DEFAULT_HEARTBEAT_MS;\n const reconcile = async (owner?: ChatOwner) => {\n if (!(await sqlChatStore.hasLapsed(db(), owner))) return;\n await database.transaction(async (tx) => {\n for (const change of await sqlChatStore.expireLapsed(tx as ChatDb, owner)) {\n const tags: Tag[] = [\n ...change.threads.map((threadId) => [CHAT_TAG, threadId]),\n ...change.hosts.map((hostId) => [HOSTS_TAG, hostId]),\n ];\n await host.announce(ownStream(change), tags, tx);\n }\n });\n };\n const pruneHosts: ChatActionDeps[\"pruneHosts\"] = async (owner, hostIds) => {\n try {\n await database.transaction((tx) =>\n sqlChatStore.pruneHosts(tx as ChatDb, owner, hostIds),\n );\n } catch (error) {\n console.warn(\"[parlor] chat: pruning revoked computers failed:\", error);\n }\n };\n let sweep: ReturnType<typeof setInterval> | undefined;\n const actions = chatActions({\n enforcing: auth.enforcing,\n transact,\n reconcile,\n pruneHosts,\n auth,\n store: sqlChatStore,\n relay,\n leaseMs,\n heartbeatMs,\n maxQueued,\n instructions: options.instructions,\n appName: process.env.PARLOR_APP_NAME ?? \"this app\",\n appOrigin: () => process.env.PARLOR_ORIGIN ?? process.env.PARLOR_WEB_ORIGIN ?? \"\",\n });\n const { access } = options;\n return {\n actions,\n ...(access\n ? {\n allows: ({ principal, scope }) =>\n access({\n userId: userOf(principal) ?? DEV_USER_ID,\n principal,\n scope,\n database,\n }),\n }\n : {}),\n routes: chatRoutes({\n db,\n store: sqlChatStore,\n relay,\n heartbeatMs,\n reconcile,\n announceHost: (owner, hostId) =>\n host.announce(ownStream(owner), [[HOSTS_TAG, hostId]]),\n }),\n channel: relay.channel,\n start: () => {\n sweep = setInterval(() => {\n (host.ready?.() ?? Promise.resolve())\n .then(() => reconcile())\n .catch((error: unknown) => {\n console.warn(\"[parlor] chat: sweeping lapsed turns failed:\", error);\n });\n }, heartbeatMs);\n sweep.unref?.();\n },\n close: () => {\n clearInterval(sweep);\n relay.close();\n },\n };\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ThreadId, type TurnId } from "@parlor/core";
|
|
2
|
+
import type { ServerChannel } from "@parlor/sync";
|
|
3
|
+
import type { ChatOwner, OpenTurn } from "../model.js";
|
|
4
|
+
import { type HostStreamBatch } from "../protocol.js";
|
|
5
|
+
import type { TurnHolder } from "./store.js";
|
|
6
|
+
export declare const MAX_OPEN_BYTES_PER_TURN: number;
|
|
7
|
+
export declare const MAX_OPEN_TURNS = 1024;
|
|
8
|
+
/**
|
|
9
|
+
* The live half of the chat: what is streaming now, carried to the `chat` channel of the
|
|
10
|
+
* owner's own streams. Nothing here is the truth; drop it and every client still
|
|
11
|
+
* converges on the log, only without token streaming. So it writes nothing and can be
|
|
12
|
+
* replaced by a relay that crosses instances.
|
|
13
|
+
*/
|
|
14
|
+
export interface LiveRelay {
|
|
15
|
+
readonly channel: ServerChannel;
|
|
16
|
+
/** Applies a batch to the open turn and pushes it; a turn over its byte budget stops streaming. */
|
|
17
|
+
accept(turn: TurnHolder, batch: HostStreamBatch): void;
|
|
18
|
+
settle(owner: ChatOwner, threadId: ThreadId, turnId: TurnId): void;
|
|
19
|
+
open(owner: ChatOwner, threadId: ThreadId): readonly OpenTurn[];
|
|
20
|
+
close(): void;
|
|
21
|
+
}
|
|
22
|
+
/** The default: one instance, in memory. */
|
|
23
|
+
export declare function inProcessRelay(): LiveRelay;
|
|
24
|
+
//# sourceMappingURL=live.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../../src/server/live.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,MAAM,EAGZ,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAqB,aAAa,EAAE,MAAM,cAAc,CAAC;AAErE,OAAO,KAAK,EAAE,SAAS,EAAY,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEjE,OAAO,EAAgC,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACpF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,uBAAuB,QAAa,CAAC;AAClD,eAAO,MAAM,cAAc,OAAO,CAAC;AAEnC;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,mGAAmG;IACnG,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IACvD,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACnE,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,SAAS,QAAQ,EAAE,CAAC;IAChE,KAAK,IAAI,IAAI,CAAC;CACf;AAuBD,4CAA4C;AAC5C,wBAAgB,cAAc,IAAI,SAAS,CA8G1C"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { DEV_USER_ID, userOf, } from "@parlor/core";
|
|
2
|
+
import { applyFrame } from "../open-turns.js";
|
|
3
|
+
import { CHAT_CHANNEL } from "../protocol.js";
|
|
4
|
+
export const MAX_OPEN_BYTES_PER_TURN = 256 * 1024;
|
|
5
|
+
export const MAX_OPEN_TURNS = 1024;
|
|
6
|
+
const SETTLED_MEMORY = 4 * MAX_OPEN_TURNS;
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
function ownerKey(scope, userId) {
|
|
9
|
+
return `${scope}\u001f${userId ?? DEV_USER_ID}`;
|
|
10
|
+
}
|
|
11
|
+
function sizeOf(frame) {
|
|
12
|
+
return frame.type === "delta"
|
|
13
|
+
? encoder.encode(frame.text).length
|
|
14
|
+
: encoder.encode(JSON.stringify(frame.item)).length;
|
|
15
|
+
}
|
|
16
|
+
/** The default: one instance, in memory. */
|
|
17
|
+
export function inProcessRelay() {
|
|
18
|
+
const connections = new Map();
|
|
19
|
+
const turns = new Map();
|
|
20
|
+
const settled = new Set();
|
|
21
|
+
let closed = false;
|
|
22
|
+
const push = (owner, frame) => {
|
|
23
|
+
const audience = connections.get(ownerKey(owner.scope, owner.userId));
|
|
24
|
+
if (!audience || audience.size === 0)
|
|
25
|
+
return;
|
|
26
|
+
const data = JSON.stringify(frame);
|
|
27
|
+
for (const connection of audience) {
|
|
28
|
+
try {
|
|
29
|
+
connection.send(data);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
audience.delete(connection);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const channel = {
|
|
37
|
+
event: CHAT_CHANNEL,
|
|
38
|
+
connect(connection) {
|
|
39
|
+
if (closed || connection.closed?.aborted)
|
|
40
|
+
return () => { };
|
|
41
|
+
const key = ownerKey(connection.scope, userOf(connection.principal));
|
|
42
|
+
let audience = connections.get(key);
|
|
43
|
+
if (!audience) {
|
|
44
|
+
audience = new Set();
|
|
45
|
+
connections.set(key, audience);
|
|
46
|
+
}
|
|
47
|
+
audience.add(connection);
|
|
48
|
+
const detach = () => {
|
|
49
|
+
const current = connections.get(key);
|
|
50
|
+
current?.delete(connection);
|
|
51
|
+
if (current?.size === 0)
|
|
52
|
+
connections.delete(key);
|
|
53
|
+
connection.closed?.removeEventListener("abort", detach);
|
|
54
|
+
};
|
|
55
|
+
connection.closed?.addEventListener("abort", detach, { once: true });
|
|
56
|
+
return detach;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
const forget = (turnId) => {
|
|
60
|
+
turns.delete(turnId);
|
|
61
|
+
settled.add(turnId);
|
|
62
|
+
if (settled.size > SETTLED_MEMORY) {
|
|
63
|
+
const oldest = settled.values().next();
|
|
64
|
+
if (!oldest.done)
|
|
65
|
+
settled.delete(oldest.value);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
channel,
|
|
70
|
+
accept(holder, batch) {
|
|
71
|
+
if (closed || settled.has(batch.turnId))
|
|
72
|
+
return;
|
|
73
|
+
let state = turns.get(batch.turnId);
|
|
74
|
+
if (!state) {
|
|
75
|
+
if (turns.size >= MAX_OPEN_TURNS) {
|
|
76
|
+
const oldest = turns.keys().next();
|
|
77
|
+
if (!oldest.done)
|
|
78
|
+
turns.delete(oldest.value);
|
|
79
|
+
}
|
|
80
|
+
state = {
|
|
81
|
+
owner: holder.owner,
|
|
82
|
+
threadId: holder.threadId,
|
|
83
|
+
items: [],
|
|
84
|
+
bytes: 0,
|
|
85
|
+
overBudget: false,
|
|
86
|
+
};
|
|
87
|
+
turns.set(batch.turnId, state);
|
|
88
|
+
}
|
|
89
|
+
for (const hostFrame of batch.frames) {
|
|
90
|
+
const frame = {
|
|
91
|
+
...hostFrame,
|
|
92
|
+
threadId: state.threadId,
|
|
93
|
+
turnId: batch.turnId,
|
|
94
|
+
};
|
|
95
|
+
const cost = sizeOf(frame);
|
|
96
|
+
if (state.overBudget || state.bytes + cost > MAX_OPEN_BYTES_PER_TURN) {
|
|
97
|
+
state.overBudget = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
const applied = applyFrame(state.items, frame);
|
|
101
|
+
if (!applied)
|
|
102
|
+
continue;
|
|
103
|
+
state.items = applied.items;
|
|
104
|
+
state.bytes += cost;
|
|
105
|
+
push(state.owner, applied.frame);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
settle(owner, threadId, turnId) {
|
|
109
|
+
forget(turnId);
|
|
110
|
+
push(owner, { type: "settled", threadId, turnId });
|
|
111
|
+
},
|
|
112
|
+
open(owner, threadId) {
|
|
113
|
+
const open = [];
|
|
114
|
+
for (const [turnId, state] of turns) {
|
|
115
|
+
if (state.threadId === threadId &&
|
|
116
|
+
state.owner.scope === owner.scope &&
|
|
117
|
+
state.owner.userId === owner.userId) {
|
|
118
|
+
open.push({ turnId, threadId, items: state.items });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return open;
|
|
122
|
+
},
|
|
123
|
+
close() {
|
|
124
|
+
closed = true;
|
|
125
|
+
connections.clear();
|
|
126
|
+
turns.clear();
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=live.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live.js","sourceRoot":"","sources":["../../src/server/live.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DEV_USER_ID,\n type ThreadId,\n type TurnId,\n type UserId,\n userOf,\n} from \"@parlor/core\";\nimport type { ChannelConnection, ServerChannel } from \"@parlor/sync\";\n\nimport type { ChatOwner, OpenItem, OpenTurn } from \"../model.js\";\nimport { applyFrame } from \"../open-turns.js\";\nimport { CHAT_CHANNEL, type ChatFrame, type HostStreamBatch } from \"../protocol.js\";\nimport type { TurnHolder } from \"./store.js\";\n\nexport const MAX_OPEN_BYTES_PER_TURN = 256 * 1024;\nexport const MAX_OPEN_TURNS = 1024;\n\n/**\n * The live half of the chat: what is streaming now, carried to the `chat` channel of the\n * owner's own streams. Nothing here is the truth; drop it and every client still\n * converges on the log, only without token streaming. So it writes nothing and can be\n * replaced by a relay that crosses instances.\n */\nexport interface LiveRelay {\n readonly channel: ServerChannel;\n /** Applies a batch to the open turn and pushes it; a turn over its byte budget stops streaming. */\n accept(turn: TurnHolder, batch: HostStreamBatch): void;\n settle(owner: ChatOwner, threadId: ThreadId, turnId: TurnId): void;\n open(owner: ChatOwner, threadId: ThreadId): readonly OpenTurn[];\n close(): void;\n}\n\ninterface OpenState {\n readonly owner: ChatOwner;\n readonly threadId: ThreadId;\n items: readonly OpenItem[];\n bytes: number;\n overBudget: boolean;\n}\n\nconst SETTLED_MEMORY = 4 * MAX_OPEN_TURNS;\nconst encoder = new TextEncoder();\n\nfunction ownerKey(scope: string, userId: UserId | undefined): string {\n return `${scope}\\u001f${userId ?? DEV_USER_ID}`;\n}\n\nfunction sizeOf(frame: Extract<ChatFrame, { type: \"delta\" | \"item\" }>): number {\n return frame.type === \"delta\"\n ? encoder.encode(frame.text).length\n : encoder.encode(JSON.stringify(frame.item)).length;\n}\n\n/** The default: one instance, in memory. */\nexport function inProcessRelay(): LiveRelay {\n const connections = new Map<string, Set<ChannelConnection>>();\n const turns = new Map<TurnId, OpenState>();\n const settled = new Set<TurnId>();\n let closed = false;\n\n const push = (owner: ChatOwner, frame: ChatFrame) => {\n const audience = connections.get(ownerKey(owner.scope, owner.userId));\n if (!audience || audience.size === 0) return;\n const data = JSON.stringify(frame);\n for (const connection of audience) {\n try {\n connection.send(data);\n } catch {\n audience.delete(connection);\n }\n }\n };\n\n const channel: ServerChannel = {\n event: CHAT_CHANNEL,\n connect(connection) {\n if (closed || connection.closed?.aborted) return () => {};\n const key = ownerKey(connection.scope, userOf(connection.principal));\n let audience = connections.get(key);\n if (!audience) {\n audience = new Set();\n connections.set(key, audience);\n }\n audience.add(connection);\n const detach = () => {\n const current = connections.get(key);\n current?.delete(connection);\n if (current?.size === 0) connections.delete(key);\n connection.closed?.removeEventListener(\"abort\", detach);\n };\n connection.closed?.addEventListener(\"abort\", detach, { once: true });\n return detach;\n },\n };\n\n const forget = (turnId: TurnId) => {\n turns.delete(turnId);\n settled.add(turnId);\n if (settled.size > SETTLED_MEMORY) {\n const oldest = settled.values().next();\n if (!oldest.done) settled.delete(oldest.value);\n }\n };\n\n return {\n channel,\n accept(holder, batch) {\n if (closed || settled.has(batch.turnId)) return;\n let state = turns.get(batch.turnId);\n if (!state) {\n if (turns.size >= MAX_OPEN_TURNS) {\n const oldest = turns.keys().next();\n if (!oldest.done) turns.delete(oldest.value);\n }\n state = {\n owner: holder.owner,\n threadId: holder.threadId,\n items: [],\n bytes: 0,\n overBudget: false,\n };\n turns.set(batch.turnId, state);\n }\n for (const hostFrame of batch.frames) {\n const frame: Extract<ChatFrame, { type: \"delta\" | \"item\" }> = {\n ...hostFrame,\n threadId: state.threadId,\n turnId: batch.turnId,\n };\n const cost = sizeOf(frame);\n if (state.overBudget || state.bytes + cost > MAX_OPEN_BYTES_PER_TURN) {\n state.overBudget = true;\n break;\n }\n const applied = applyFrame(state.items, frame);\n if (!applied) continue;\n state.items = applied.items;\n state.bytes += cost;\n push(state.owner, applied.frame);\n }\n },\n settle(owner, threadId, turnId) {\n forget(turnId);\n push(owner, { type: \"settled\", threadId, turnId });\n },\n open(owner, threadId) {\n const open: OpenTurn[] = [];\n for (const [turnId, state] of turns) {\n if (\n state.threadId === threadId &&\n state.owner.scope === owner.scope &&\n state.owner.userId === owner.userId\n ) {\n open.push({ turnId, threadId, items: state.items });\n }\n }\n return open;\n },\n close() {\n closed = true;\n connections.clear();\n turns.clear();\n },\n };\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/server/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,eAAO,MAAM,eAAe,EAAE,SAAS,SAAS,EA8F/C,CAAC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export const CHAT_MIGRATIONS = [
|
|
2
|
+
{
|
|
3
|
+
id: "chat/0001_threads",
|
|
4
|
+
sql: `
|
|
5
|
+
create table parlor_chat_threads (
|
|
6
|
+
id text primary key,
|
|
7
|
+
scope text not null,
|
|
8
|
+
owner_id text not null,
|
|
9
|
+
title text not null default '',
|
|
10
|
+
host_id text,
|
|
11
|
+
agent_id text,
|
|
12
|
+
last_seq bigint not null default 0,
|
|
13
|
+
created_at timestamptz not null default now(),
|
|
14
|
+
updated_at timestamptz not null default now()
|
|
15
|
+
);
|
|
16
|
+
create index parlor_chat_threads_owner
|
|
17
|
+
on parlor_chat_threads (owner_id, scope, updated_at desc);
|
|
18
|
+
|
|
19
|
+
create table parlor_chat_items (
|
|
20
|
+
id text primary key,
|
|
21
|
+
thread_id text not null references parlor_chat_threads (id) on delete cascade,
|
|
22
|
+
turn_id text not null,
|
|
23
|
+
seq bigint not null,
|
|
24
|
+
author text not null check (author in ('user', 'agent', 'system')),
|
|
25
|
+
kind text not null,
|
|
26
|
+
body jsonb not null,
|
|
27
|
+
created_at timestamptz not null default now(),
|
|
28
|
+
unique (thread_id, seq)
|
|
29
|
+
);
|
|
30
|
+
create unique index parlor_chat_items_one_answer
|
|
31
|
+
on parlor_chat_items (thread_id, (body->>'askId')) where kind = 'answer';
|
|
32
|
+
|
|
33
|
+
create table parlor_chat_turns (
|
|
34
|
+
id text primary key,
|
|
35
|
+
thread_id text not null references parlor_chat_threads (id) on delete cascade,
|
|
36
|
+
scope text not null,
|
|
37
|
+
owner_id text not null,
|
|
38
|
+
host_id text not null,
|
|
39
|
+
agent_id text not null,
|
|
40
|
+
client_id text not null,
|
|
41
|
+
device_id text,
|
|
42
|
+
prompt_item_id text not null,
|
|
43
|
+
state text not null
|
|
44
|
+
check (state in ('queued', 'running', 'done', 'failed', 'cancelled')),
|
|
45
|
+
lease_until timestamptz,
|
|
46
|
+
outcome jsonb,
|
|
47
|
+
created_at timestamptz not null default now(),
|
|
48
|
+
finished_at timestamptz
|
|
49
|
+
);
|
|
50
|
+
create index parlor_chat_turns_queue
|
|
51
|
+
on parlor_chat_turns (host_id, state, created_at) where state = 'queued';
|
|
52
|
+
create unique index parlor_chat_turns_one_running
|
|
53
|
+
on parlor_chat_turns (thread_id) where state = 'running';
|
|
54
|
+
create index parlor_chat_turns_thread_state on parlor_chat_turns (thread_id, state);
|
|
55
|
+
create index parlor_chat_turns_lease
|
|
56
|
+
on parlor_chat_turns (lease_until) where state = 'running';
|
|
57
|
+
|
|
58
|
+
create index parlor_chat_items_signals
|
|
59
|
+
on parlor_chat_items (turn_id, seq) where kind in ('answer', 'cancel');
|
|
60
|
+
|
|
61
|
+
create table parlor_chat_hosts (
|
|
62
|
+
id text primary key,
|
|
63
|
+
scope text not null,
|
|
64
|
+
owner_id text not null,
|
|
65
|
+
grant_id text not null,
|
|
66
|
+
protocol integer not null,
|
|
67
|
+
name text not null,
|
|
68
|
+
platform text not null,
|
|
69
|
+
agents jsonb not null,
|
|
70
|
+
online_until timestamptz not null,
|
|
71
|
+
announced_online boolean not null
|
|
72
|
+
);
|
|
73
|
+
create index parlor_chat_hosts_owner on parlor_chat_hosts (owner_id, scope);
|
|
74
|
+
create index parlor_chat_hosts_lapse
|
|
75
|
+
on parlor_chat_hosts (online_until) where announced_online;`,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "chat/0002_answer_retry",
|
|
79
|
+
sql: `
|
|
80
|
+
drop index parlor_chat_items_one_answer;
|
|
81
|
+
create index parlor_chat_items_answers
|
|
82
|
+
on parlor_chat_items (thread_id, (body->>'askId')) where kind = 'answer';
|
|
83
|
+
create index parlor_chat_items_rejections
|
|
84
|
+
on parlor_chat_items (thread_id, (body->>'itemId'))
|
|
85
|
+
where kind = 'event' and body->>'event' = 'answer_rejected';`,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "chat/0003_host_devices",
|
|
89
|
+
sql: `alter table parlor_chat_hosts add column devices jsonb;`,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "chat/0004_host_presence",
|
|
93
|
+
sql: `alter table parlor_chat_hosts add column presence_due jsonb;`,
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
//# sourceMappingURL=migrations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/server/migrations.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAyB;IACnD;QACE,EAAE,EAAE,mBAAmB;QACvB,GAAG,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oEAuE2D;KACjE;IACD;QACE,EAAE,EAAE,wBAAwB;QAC5B,GAAG,EAAE;;;;;;qEAM4D;KAClE;IACD;QACE,EAAE,EAAE,wBAAwB;QAC5B,GAAG,EAAE,yDAAyD;KAC/D;IACD;QACE,EAAE,EAAE,yBAAyB;QAC7B,GAAG,EAAE,8DAA8D;KACpE;CACF,CAAC","sourcesContent":["import type { Migration } from \"@parlor/server\";\n\nexport const CHAT_MIGRATIONS: readonly Migration[] = [\n {\n id: \"chat/0001_threads\",\n sql: `\n create table parlor_chat_threads (\n id text primary key,\n scope text not null,\n owner_id text not null,\n title text not null default '',\n host_id text,\n agent_id text,\n last_seq bigint not null default 0,\n created_at timestamptz not null default now(),\n updated_at timestamptz not null default now()\n );\n create index parlor_chat_threads_owner\n on parlor_chat_threads (owner_id, scope, updated_at desc);\n\n create table parlor_chat_items (\n id text primary key,\n thread_id text not null references parlor_chat_threads (id) on delete cascade,\n turn_id text not null,\n seq bigint not null,\n author text not null check (author in ('user', 'agent', 'system')),\n kind text not null,\n body jsonb not null,\n created_at timestamptz not null default now(),\n unique (thread_id, seq)\n );\n create unique index parlor_chat_items_one_answer\n on parlor_chat_items (thread_id, (body->>'askId')) where kind = 'answer';\n\n create table parlor_chat_turns (\n id text primary key,\n thread_id text not null references parlor_chat_threads (id) on delete cascade,\n scope text not null,\n owner_id text not null,\n host_id text not null,\n agent_id text not null,\n client_id text not null,\n device_id text,\n prompt_item_id text not null,\n state text not null\n check (state in ('queued', 'running', 'done', 'failed', 'cancelled')),\n lease_until timestamptz,\n outcome jsonb,\n created_at timestamptz not null default now(),\n finished_at timestamptz\n );\n create index parlor_chat_turns_queue\n on parlor_chat_turns (host_id, state, created_at) where state = 'queued';\n create unique index parlor_chat_turns_one_running\n on parlor_chat_turns (thread_id) where state = 'running';\n create index parlor_chat_turns_thread_state on parlor_chat_turns (thread_id, state);\n create index parlor_chat_turns_lease\n on parlor_chat_turns (lease_until) where state = 'running';\n\n create index parlor_chat_items_signals\n on parlor_chat_items (turn_id, seq) where kind in ('answer', 'cancel');\n\n create table parlor_chat_hosts (\n id text primary key,\n scope text not null,\n owner_id text not null,\n grant_id text not null,\n protocol integer not null,\n name text not null,\n platform text not null,\n agents jsonb not null,\n online_until timestamptz not null,\n announced_online boolean not null\n );\n create index parlor_chat_hosts_owner on parlor_chat_hosts (owner_id, scope);\n create index parlor_chat_hosts_lapse\n on parlor_chat_hosts (online_until) where announced_online;`,\n },\n {\n id: \"chat/0002_answer_retry\",\n sql: `\n drop index parlor_chat_items_one_answer;\n create index parlor_chat_items_answers\n on parlor_chat_items (thread_id, (body->>'askId')) where kind = 'answer';\n create index parlor_chat_items_rejections\n on parlor_chat_items (thread_id, (body->>'itemId'))\n where kind = 'event' and body->>'event' = 'answer_rejected';`,\n },\n {\n id: \"chat/0003_host_devices\",\n sql: `alter table parlor_chat_hosts add column devices jsonb;`,\n },\n {\n id: \"chat/0004_host_presence\",\n sql: `alter table parlor_chat_hosts add column presence_due jsonb;`,\n },\n];\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ActionConfig, type ActionContext, type ActionDefinition, type DeviceId, type HostPrincipal, type Principal } from "@parlor/core";
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
import type { ChatOwner } from "../model.js";
|
|
4
|
+
export type Party = "user" | "host";
|
|
5
|
+
export interface ChatUser extends ChatOwner {
|
|
6
|
+
readonly deviceId: DeviceId | undefined;
|
|
7
|
+
}
|
|
8
|
+
/** A host and the owner whose rows it may touch: its own user. */
|
|
9
|
+
export interface ChatHost extends ChatOwner {
|
|
10
|
+
readonly host: HostPrincipal;
|
|
11
|
+
}
|
|
12
|
+
export type PartyOf<P extends Party> = P extends "user" ? ChatUser : ChatHost;
|
|
13
|
+
export interface ChatActionConfig<P extends Party, I extends z.ZodType, O> extends Omit<ActionConfig<I, O>, "run" | "confirm" | "public"> {
|
|
14
|
+
run(input: z.output<I>, party: PartyOf<P>, ctx: ActionContext): Promise<O> | O;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* An action only `party` may call, checked once before `run`: a user action takes a
|
|
18
|
+
* signed-in user, or without an enforcing provider an anonymous caller as `DEV_USER_ID`;
|
|
19
|
+
* a host action takes only a host. Everything else is `forbidden`, so no body checks the
|
|
20
|
+
* principal itself.
|
|
21
|
+
*/
|
|
22
|
+
export declare function chatAction<P extends Party, I extends z.ZodType, O>(party: P, enforcing: boolean, config: ChatActionConfig<P, I, O>): ActionDefinition<I, O>;
|
|
23
|
+
export declare function partyOf<P extends Party>(party: P, principal: Principal, scope: string, enforcing: boolean): PartyOf<P>;
|
|
24
|
+
//# sourceMappingURL=party.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"party.d.ts","sourceRoot":"","sources":["../../src/server/party.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EAErB,KAAK,QAAQ,EAEb,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpC,MAAM,WAAW,QAAS,SAAQ,SAAS;IACzC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC;CACzC;AAED,kEAAkE;AAClE,MAAM,WAAW,QAAS,SAAQ,SAAS;IACzC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;CAC9B;AAED,MAAM,MAAM,OAAO,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,SAAS,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE9E,MAAM,WAAW,gBAAgB,CAAC,CAAC,SAAS,KAAK,EAAE,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,CAAE,SAAQ,IAAI,CACrF,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,EAClB,KAAK,GAAG,SAAS,GAAG,QAAQ,CAC7B;IACC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;CAChF;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,KAAK,EAAE,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,EAChE,KAAK,EAAE,CAAC,EACR,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE,gBAAgB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAChC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAQxB;AAgCD,wBAAgB,OAAO,CAAC,CAAC,SAAS,KAAK,EACrC,KAAK,EAAE,CAAC,EACR,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,OAAO,GACjB,OAAO,CAAC,CAAC,CAAC,CAEZ"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ActionError, defineAction, DEV_USER_ID, } from "@parlor/core";
|
|
2
|
+
/**
|
|
3
|
+
* An action only `party` may call, checked once before `run`: a user action takes a
|
|
4
|
+
* signed-in user, or without an enforcing provider an anonymous caller as `DEV_USER_ID`;
|
|
5
|
+
* a host action takes only a host. Everything else is `forbidden`, so no body checks the
|
|
6
|
+
* principal itself.
|
|
7
|
+
*/
|
|
8
|
+
export function chatAction(party, enforcing, config) {
|
|
9
|
+
const { run, ...rest } = config;
|
|
10
|
+
return defineAction({
|
|
11
|
+
notifies: "user",
|
|
12
|
+
...rest,
|
|
13
|
+
run: (input, ctx) => run(input, partyOf(party, ctx.principal, ctx.scope, enforcing), ctx),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
const resolvers = {
|
|
17
|
+
host: (principal, scope) => {
|
|
18
|
+
if (principal.kind !== "host") {
|
|
19
|
+
throw new ActionError("forbidden", "Only a connected computer can call this.");
|
|
20
|
+
}
|
|
21
|
+
return { scope, userId: principal.userId, host: principal };
|
|
22
|
+
},
|
|
23
|
+
user: (principal, scope, enforcing) => {
|
|
24
|
+
if (principal.kind === "user") {
|
|
25
|
+
return { scope, userId: principal.userId, deviceId: principal.deviceId };
|
|
26
|
+
}
|
|
27
|
+
if (principal.kind === "anonymous") {
|
|
28
|
+
if (enforcing)
|
|
29
|
+
throw new ActionError("unauthenticated", "Sign in first.");
|
|
30
|
+
return { scope, userId: DEV_USER_ID, deviceId: undefined };
|
|
31
|
+
}
|
|
32
|
+
throw new ActionError("forbidden", `A ${principal.kind} principal cannot use the chat.`);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
export function partyOf(party, principal, scope, enforcing) {
|
|
36
|
+
return resolvers[party](principal, scope, enforcing);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=party.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"party.js","sourceRoot":"","sources":["../../src/server/party.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EAIX,YAAY,EAEZ,WAAW,GAGZ,MAAM,cAAc,CAAC;AAyBtB;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CACxB,KAAQ,EACR,SAAkB,EAClB,MAAiC;IAEjC,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,CAAC;IAChC,OAAO,YAAY,CAAO;QACxB,QAAQ,EAAE,MAAM;QAChB,GAAG,IAAI;QACP,GAAG,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CAClB,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC;KACvE,CAAC,CAAC;AACL,CAAC;AAUD,MAAM,SAAS,GAAmB;IAChC,IAAI,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE;QACzB,IAAI,SAAS,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC9B,MAAM,IAAI,WAAW,CAAC,WAAW,EAAE,0CAA0C,CAAC,CAAC;QACjF,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAC9D,CAAC;IACD,IAAI,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;QACpC,IAAI,SAAS,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC9B,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE,SAAS,CAAC,QAAQ,EAAE,CAAC;QAC3E,CAAC;QACD,IAAI,SAAS,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YACnC,IAAI,SAAS;gBAAE,MAAM,IAAI,WAAW,CAAC,iBAAiB,EAAE,gBAAgB,CAAC,CAAC;YAC1E,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;QAC7D,CAAC;QACD,MAAM,IAAI,WAAW,CACnB,WAAW,EACX,KAAK,SAAS,CAAC,IAAI,iCAAiC,CACrD,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,MAAM,UAAU,OAAO,CACrB,KAAQ,EACR,SAAoB,EACpB,KAAa,EACb,SAAkB;IAElB,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC,SAAS,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;AACvD,CAAC","sourcesContent":["import {\n ActionError,\n type ActionConfig,\n type ActionContext,\n type ActionDefinition,\n defineAction,\n type DeviceId,\n DEV_USER_ID,\n type HostPrincipal,\n type Principal,\n} from \"@parlor/core\";\nimport type { z } from \"zod\";\n\nimport type { ChatOwner } from \"../model.js\";\n\nexport type Party = \"user\" | \"host\";\n\nexport interface ChatUser extends ChatOwner {\n readonly deviceId: DeviceId | undefined;\n}\n\n/** A host and the owner whose rows it may touch: its own user. */\nexport interface ChatHost extends ChatOwner {\n readonly host: HostPrincipal;\n}\n\nexport type PartyOf<P extends Party> = P extends \"user\" ? ChatUser : ChatHost;\n\nexport interface ChatActionConfig<P extends Party, I extends z.ZodType, O> extends Omit<\n ActionConfig<I, O>,\n \"run\" | \"confirm\" | \"public\"\n> {\n run(input: z.output<I>, party: PartyOf<P>, ctx: ActionContext): Promise<O> | O;\n}\n\n/**\n * An action only `party` may call, checked once before `run`: a user action takes a\n * signed-in user, or without an enforcing provider an anonymous caller as `DEV_USER_ID`;\n * a host action takes only a host. Everything else is `forbidden`, so no body checks the\n * principal itself.\n */\nexport function chatAction<P extends Party, I extends z.ZodType, O>(\n party: P,\n enforcing: boolean,\n config: ChatActionConfig<P, I, O>,\n): ActionDefinition<I, O> {\n const { run, ...rest } = config;\n return defineAction<I, O>({\n notifies: \"user\",\n ...rest,\n run: (input, ctx) =>\n run(input, partyOf(party, ctx.principal, ctx.scope, enforcing), ctx),\n });\n}\n\ntype PartyResolvers = {\n readonly [P in Party]: (\n principal: Principal,\n scope: string,\n enforcing: boolean,\n ) => PartyOf<P>;\n};\n\nconst resolvers: PartyResolvers = {\n host: (principal, scope) => {\n if (principal.kind !== \"host\") {\n throw new ActionError(\"forbidden\", \"Only a connected computer can call this.\");\n }\n return { scope, userId: principal.userId, host: principal };\n },\n user: (principal, scope, enforcing) => {\n if (principal.kind === \"user\") {\n return { scope, userId: principal.userId, deviceId: principal.deviceId };\n }\n if (principal.kind === \"anonymous\") {\n if (enforcing) throw new ActionError(\"unauthenticated\", \"Sign in first.\");\n return { scope, userId: DEV_USER_ID, deviceId: undefined };\n }\n throw new ActionError(\n \"forbidden\",\n `A ${principal.kind} principal cannot use the chat.`,\n );\n },\n};\n\nexport function partyOf<P extends Party>(\n party: P,\n principal: Principal,\n scope: string,\n enforcing: boolean,\n): PartyOf<P> {\n return resolvers[party](principal, scope, enforcing);\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type HostId } from "@parlor/core";
|
|
2
|
+
import { type AnyPluginRoute } from "@parlor/server";
|
|
3
|
+
import type { ChatOwner } from "../model.js";
|
|
4
|
+
import type { LiveRelay } from "./live.js";
|
|
5
|
+
import type { ChatDb, ChatStore } from "./store.js";
|
|
6
|
+
export declare const MAX_STREAM_BODY_BYTES: number;
|
|
7
|
+
export interface ChatRouteDeps {
|
|
8
|
+
readonly db: () => ChatDb;
|
|
9
|
+
readonly store: ChatStore;
|
|
10
|
+
readonly relay: LiveRelay;
|
|
11
|
+
readonly heartbeatMs: number;
|
|
12
|
+
readonly reconcile: (owner: ChatOwner) => Promise<void>;
|
|
13
|
+
readonly announceHost: (owner: ChatOwner, hostId: HostId) => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
/** Plain routes only a host principal reaches, by type: no browser session, no agent token. */
|
|
16
|
+
export declare function chatRoutes(deps: ChatRouteDeps): readonly AnyPluginRoute[];
|
|
17
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/server/routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,KAAK,cAAc,EAAgC,MAAM,gBAAgB,CAAC;AAInF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEpD,eAAO,MAAM,qBAAqB,QAAY,CAAC;AAG/C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,QAAQ,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5E;AAmDD,+FAA+F;AAC/F,wBAAgB,UAAU,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,cAAc,EAAE,CAmDzE"}
|