Steps toward secure-chat. Highlights the need for a tree of facets.
This commit is contained in:
parent
23fa218df2
commit
6224a4dbf3
|
@ -0,0 +1,14 @@
|
||||||
|
version 1 .
|
||||||
|
pointer Actor.Ref .
|
||||||
|
|
||||||
|
UserId = int .
|
||||||
|
|
||||||
|
Join = <joinedUser @uid UserId @handle ref>.
|
||||||
|
|
||||||
|
NickClaim = <claimNick @uid UserId @name string @k ref>.
|
||||||
|
|
||||||
|
UserInfo = <user @uid UserId @name string>.
|
||||||
|
|
||||||
|
Says = <says @who UserId @what string>.
|
||||||
|
|
||||||
|
NickConflict = <nickConflict>.
|
|
@ -143,8 +143,8 @@ export class Turn {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
replace(ref: Ref, h: Handle | undefined, assertion: Assertion): Handle {
|
replace(ref: Ref, h: Handle | undefined, assertion: Assertion | undefined): Handle | undefined {
|
||||||
const newHandle = this.assert(ref, assertion);
|
const newHandle = assertion === void 0 ? void 0 : this.assert(ref, assertion);
|
||||||
this.retract(h);
|
this.retract(h);
|
||||||
return newHandle;
|
return newHandle;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Assertion, Entity, Handle, Ref, Turn } from 'actor';
|
import { Assertion, Entity, Handle, LocalAction, Ref, Turn } from 'actor';
|
||||||
import { Dictionary, IdentityMap, is, Record, Tuple } from '@preserves/core';
|
import { Dictionary, IdentityMap, is, Record, Tuple } from '@preserves/core';
|
||||||
import { Bag, ChangeDescription } from './bag';
|
import { Bag, ChangeDescription } from './bag';
|
||||||
|
|
||||||
import { toObserve } from './gen/dataspace';
|
import { fromObserve, toObserve, Observe } from './gen/dataspace';
|
||||||
export * from './gen/dataspace';
|
export * from './gen/dataspace';
|
||||||
|
|
||||||
// Q. Why keep "Observe"? Why not do the clever trick of asserting the
|
// Q. Why keep "Observe"? Why not do the clever trick of asserting the
|
||||||
|
@ -86,3 +86,21 @@ export class Dataspace implements Partial<Entity> {
|
||||||
this.subscriptions.get(rec.label)?.forEach((_seen, peer) => turn.message(peer, rec));
|
this.subscriptions.get(rec.label)?.forEach((_seen, peer) => turn.message(peer, rec));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function during(f: (t: Turn, a: Assertion) => (LocalAction | null)): Partial<Entity> {
|
||||||
|
const assertionMap = new Map<Handle, LocalAction>();
|
||||||
|
return {
|
||||||
|
assert(t: Turn, a: Assertion, h: Handle): void {
|
||||||
|
const g = f(t, a);
|
||||||
|
if (g !== null) assertionMap.set(h, g);
|
||||||
|
},
|
||||||
|
retract(t: Turn, h: Handle): void {
|
||||||
|
assertionMap.get(h)?.(t);
|
||||||
|
assertionMap.delete(h);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function observe(t: Turn, ds: Ref, label: symbol, e: Partial<Entity>): Handle {
|
||||||
|
return t.assert(ds, fromObserve(Observe({ label, observer: t.ref(e) })));
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,182 @@
|
||||||
|
import * as _ from "@preserves/core";
|
||||||
|
import * as _i_Actor from "../actor";
|
||||||
|
|
||||||
|
export const $claimNick = Symbol.for("claimNick");
|
||||||
|
export const $joinedUser = Symbol.for("joinedUser");
|
||||||
|
export const $nickConflict = Symbol.for("nickConflict");
|
||||||
|
export const $says = Symbol.for("says");
|
||||||
|
export const $user = Symbol.for("user");
|
||||||
|
|
||||||
|
export type _ptr = _i_Actor.Ref;
|
||||||
|
|
||||||
|
export type _val = _.Value<_ptr>;
|
||||||
|
|
||||||
|
export type UserId = number;
|
||||||
|
|
||||||
|
export type Join = {"uid": UserId, "handle": _ptr};
|
||||||
|
|
||||||
|
export type NickClaim = {"uid": UserId, "name": string, "k": _ptr};
|
||||||
|
|
||||||
|
export type UserInfo = {"uid": UserId, "name": string};
|
||||||
|
|
||||||
|
export type Says = {"who": UserId, "what": string};
|
||||||
|
|
||||||
|
export type NickConflict = null;
|
||||||
|
|
||||||
|
|
||||||
|
export const _toPtr = (v: _val) => {let result: undefined | _ptr; result = _i_Actor.toRef(v); return result;};
|
||||||
|
|
||||||
|
export function UserId(value: number): UserId {return value;}
|
||||||
|
|
||||||
|
export function Join({uid, handle}: {uid: UserId, handle: _ptr}): Join {return {"uid": uid, "handle": handle};}
|
||||||
|
|
||||||
|
export function NickClaim({uid, name, k}: {uid: UserId, name: string, k: _ptr}): NickClaim {return {"uid": uid, "name": name, "k": k};}
|
||||||
|
|
||||||
|
export function UserInfo({uid, name}: {uid: UserId, name: string}): UserInfo {return {"uid": uid, "name": name};}
|
||||||
|
|
||||||
|
export function Says({who, what}: {who: UserId, what: string}): Says {return {"who": who, "what": what};}
|
||||||
|
|
||||||
|
export function NickConflict(): NickConflict {return null;}
|
||||||
|
|
||||||
|
export function asUserId(v: _val): UserId {
|
||||||
|
let result = toUserId(v);
|
||||||
|
if (result === void 0) throw new TypeError(`Invalid UserId: ${_.stringify(v)}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toUserId(v: _val): undefined | UserId {
|
||||||
|
let _tmp0: (number) | undefined;
|
||||||
|
let result: undefined | UserId;
|
||||||
|
_tmp0 = typeof v === 'number' ? v : void 0;
|
||||||
|
if (_tmp0 !== void 0) {result = _tmp0;};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromUserId(_v: UserId): _val {return _v;}
|
||||||
|
|
||||||
|
export function asJoin(v: _val): Join {
|
||||||
|
let result = toJoin(v);
|
||||||
|
if (result === void 0) throw new TypeError(`Invalid Join: ${_.stringify(v)}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toJoin(v: _val): undefined | Join {
|
||||||
|
let result: undefined | Join;
|
||||||
|
if (_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v)) {
|
||||||
|
let _tmp0: (null) | undefined;
|
||||||
|
_tmp0 = _.is(v.label, $joinedUser) ? null : void 0;
|
||||||
|
if (_tmp0 !== void 0) {
|
||||||
|
let _tmp1: (UserId) | undefined;
|
||||||
|
_tmp1 = toUserId(v[0]);
|
||||||
|
if (_tmp1 !== void 0) {
|
||||||
|
let _tmp2: (_ptr) | undefined;
|
||||||
|
_tmp2 = _toPtr(v[1]);
|
||||||
|
if (_tmp2 !== void 0) {result = {"uid": _tmp1, "handle": _tmp2};};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromJoin(_v: Join): _val {return _.Record($joinedUser, [fromUserId(_v["uid"]), _v["handle"]]);}
|
||||||
|
|
||||||
|
export function asNickClaim(v: _val): NickClaim {
|
||||||
|
let result = toNickClaim(v);
|
||||||
|
if (result === void 0) throw new TypeError(`Invalid NickClaim: ${_.stringify(v)}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toNickClaim(v: _val): undefined | NickClaim {
|
||||||
|
let result: undefined | NickClaim;
|
||||||
|
if (_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v)) {
|
||||||
|
let _tmp0: (null) | undefined;
|
||||||
|
_tmp0 = _.is(v.label, $claimNick) ? null : void 0;
|
||||||
|
if (_tmp0 !== void 0) {
|
||||||
|
let _tmp1: (UserId) | undefined;
|
||||||
|
_tmp1 = toUserId(v[0]);
|
||||||
|
if (_tmp1 !== void 0) {
|
||||||
|
let _tmp2: (string) | undefined;
|
||||||
|
_tmp2 = typeof v[1] === 'string' ? v[1] : void 0;
|
||||||
|
if (_tmp2 !== void 0) {
|
||||||
|
let _tmp3: (_ptr) | undefined;
|
||||||
|
_tmp3 = _toPtr(v[2]);
|
||||||
|
if (_tmp3 !== void 0) {result = {"uid": _tmp1, "name": _tmp2, "k": _tmp3};};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromNickClaim(_v: NickClaim): _val {return _.Record($claimNick, [fromUserId(_v["uid"]), _v["name"], _v["k"]]);}
|
||||||
|
|
||||||
|
export function asUserInfo(v: _val): UserInfo {
|
||||||
|
let result = toUserInfo(v);
|
||||||
|
if (result === void 0) throw new TypeError(`Invalid UserInfo: ${_.stringify(v)}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toUserInfo(v: _val): undefined | UserInfo {
|
||||||
|
let result: undefined | UserInfo;
|
||||||
|
if (_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v)) {
|
||||||
|
let _tmp0: (null) | undefined;
|
||||||
|
_tmp0 = _.is(v.label, $user) ? null : void 0;
|
||||||
|
if (_tmp0 !== void 0) {
|
||||||
|
let _tmp1: (UserId) | undefined;
|
||||||
|
_tmp1 = toUserId(v[0]);
|
||||||
|
if (_tmp1 !== void 0) {
|
||||||
|
let _tmp2: (string) | undefined;
|
||||||
|
_tmp2 = typeof v[1] === 'string' ? v[1] : void 0;
|
||||||
|
if (_tmp2 !== void 0) {result = {"uid": _tmp1, "name": _tmp2};};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromUserInfo(_v: UserInfo): _val {return _.Record($user, [fromUserId(_v["uid"]), _v["name"]]);}
|
||||||
|
|
||||||
|
export function asSays(v: _val): Says {
|
||||||
|
let result = toSays(v);
|
||||||
|
if (result === void 0) throw new TypeError(`Invalid Says: ${_.stringify(v)}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toSays(v: _val): undefined | Says {
|
||||||
|
let result: undefined | Says;
|
||||||
|
if (_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v)) {
|
||||||
|
let _tmp0: (null) | undefined;
|
||||||
|
_tmp0 = _.is(v.label, $says) ? null : void 0;
|
||||||
|
if (_tmp0 !== void 0) {
|
||||||
|
let _tmp1: (UserId) | undefined;
|
||||||
|
_tmp1 = toUserId(v[0]);
|
||||||
|
if (_tmp1 !== void 0) {
|
||||||
|
let _tmp2: (string) | undefined;
|
||||||
|
_tmp2 = typeof v[1] === 'string' ? v[1] : void 0;
|
||||||
|
if (_tmp2 !== void 0) {result = {"who": _tmp1, "what": _tmp2};};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromSays(_v: Says): _val {return _.Record($says, [fromUserId(_v["who"]), _v["what"]]);}
|
||||||
|
|
||||||
|
export function asNickConflict(v: _val): NickConflict {
|
||||||
|
let result = toNickConflict(v);
|
||||||
|
if (result === void 0) throw new TypeError(`Invalid NickConflict: ${_.stringify(v)}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toNickConflict(v: _val): undefined | NickConflict {
|
||||||
|
let result: undefined | NickConflict;
|
||||||
|
if (_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v)) {
|
||||||
|
let _tmp0: (null) | undefined;
|
||||||
|
_tmp0 = _.is(v.label, $nickConflict) ? null : void 0;
|
||||||
|
if (_tmp0 !== void 0) {result = null;};
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromNickConflict(_v: NickConflict): _val {return _.Record($nickConflict, []);}
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { $joinedUser, $says, $user, asJoin, asSays, asUserInfo, fromNickClaim, fromSays, NickClaim, Says, UserId } from "./gen/secure-chat-protocol.js";
|
||||||
|
import { during, observe } from "./dataspace.js";
|
||||||
|
import { Assertion, Ref, Turn } from "./actor.js";
|
||||||
|
import readline from 'readline';
|
||||||
|
|
||||||
|
export default function (t: Turn, ds: Ref) {
|
||||||
|
observe(t, ds, $joinedUser, {
|
||||||
|
assert(t, j0) {
|
||||||
|
const j = asJoin(j0);
|
||||||
|
runSession(t, j.uid, j.handle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runSession(t: Turn, uid: UserId, session: Ref) {
|
||||||
|
let username: string | undefined;
|
||||||
|
|
||||||
|
function updateUsername(t: Turn, name: string) {
|
||||||
|
const q = t.assert(session, fromNickClaim(NickClaim({ uid, name, k: t.ref({
|
||||||
|
message(t, reply0) {
|
||||||
|
if (reply0 === true) {
|
||||||
|
username = name;
|
||||||
|
console.log(`Nick changed to ${username}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Nick conflict: name is still ${username ?? '<not set>'}`);
|
||||||
|
}
|
||||||
|
t.retract(q);
|
||||||
|
}
|
||||||
|
})})));
|
||||||
|
}
|
||||||
|
updateUsername(t, 'user' + process.pid);
|
||||||
|
|
||||||
|
const users = new Map<UserId, string>();
|
||||||
|
observe(t, session, $user, during((_t, ui0) => {
|
||||||
|
const ui = asUserInfo(ui0);
|
||||||
|
const oldName = users.get(ui.uid);
|
||||||
|
console.log(oldName === void 0
|
||||||
|
? `${ui.name} arrived`
|
||||||
|
: `${oldName} changed name to ${ui.name}`);
|
||||||
|
users.set(ui.uid, ui.name);
|
||||||
|
return (_t) => {
|
||||||
|
if (users.get(ui.uid) === ui.name) {
|
||||||
|
console.log(`${ui.name} departed`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
observe(t, session, $says, {
|
||||||
|
message(_t: Turn, u0: Assertion): void {
|
||||||
|
const u = asSays(u0);
|
||||||
|
console.log(`${users.get(u.who) ?? `<unknown ${u.who}>`}: ${u.what}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||||
|
|
||||||
|
rl.on('line', (line: string) => t.freshen(t => {
|
||||||
|
if (line.toLowerCase().startsWith('/nick ')) {
|
||||||
|
updateUsername(t, line.slice(5).trimLeft());
|
||||||
|
} else {
|
||||||
|
t.message(session, fromSays(Says({ who: uid, what: line })));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
rl.on('close', () => t.freshen(t => t.quit()));
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { $claimNick, $joinedUser, asNickClaim, fromJoin, fromNickConflict, fromUserInfo, Join, NickConflict, UserId, UserInfo } from "./gen/secure-chat-protocol.js";
|
||||||
|
import { Assertion, Handle, Ref, Turn } from "./actor.js";
|
||||||
|
import { observe, during, $Observe, asObserve } from "./dataspace.js";
|
||||||
|
|
||||||
|
export default function (t: Turn, ds: Ref) {
|
||||||
|
let nextUserId: UserId = 0;
|
||||||
|
|
||||||
|
const nicks = new Map<string, UserId>();
|
||||||
|
const users = new Map<UserId, { infoHandle: Handle | undefined, nick: string | undefined }>();
|
||||||
|
|
||||||
|
observe(t, ds, $claimNick, {
|
||||||
|
assert(t: Turn, c0: Assertion): void {
|
||||||
|
const c = asNickClaim(c0);
|
||||||
|
if (nicks.has(c.name)) {
|
||||||
|
t.message(c.k, fromNickConflict(NickConflict()));
|
||||||
|
} else {
|
||||||
|
t.message(c.k, true);
|
||||||
|
let u = users.get(c.uid);
|
||||||
|
if (!u) {
|
||||||
|
u = { infoHandle: void 0, nick: void 0 };
|
||||||
|
users.set(c.uid, u);
|
||||||
|
}
|
||||||
|
if (u.nick !== void 0) nicks.delete(u.nick);
|
||||||
|
u.infoHandle = t.replace(ds, u.infoHandle, fromUserInfo(UserInfo(c)));
|
||||||
|
u.nick = c.name;
|
||||||
|
nicks.set(c.name, c.uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observe(t, ds, $Observe, during((t, o0) => {
|
||||||
|
const o = asObserve(o0);
|
||||||
|
if (o.label !== $joinedUser) return null;
|
||||||
|
|
||||||
|
const uid: UserId = nextUserId++;
|
||||||
|
const h = t.assert(o.observer, fromJoin(Join({
|
||||||
|
uid,
|
||||||
|
handle: ds,
|
||||||
|
})));
|
||||||
|
return t => t.retract(h);
|
||||||
|
}));
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { $Present, $Says, asPresent, asSays, fromPresent, fromSays, Present, Says } from "./gen/simple-chat-protocol.js";
|
import { $Present, $Says, asPresent, asSays, fromPresent, fromSays, Present, Says } from "./gen/simple-chat-protocol.js";
|
||||||
import { fromObserve, Observe } from "./gen/dataspace.js";
|
import { during, observe } from "./dataspace.js";
|
||||||
import { Assertion, Handle, LocalAction, Ref, Turn } from "./actor.js";
|
import { Assertion, Handle, Ref, Turn } from "./actor.js";
|
||||||
import readline from 'readline';
|
import readline from 'readline';
|
||||||
|
|
||||||
export default function (t: Turn, ds: Ref) {
|
export default function (t: Turn, ds: Ref) {
|
||||||
|
@ -13,31 +13,18 @@ export default function (t: Turn, ds: Ref) {
|
||||||
}
|
}
|
||||||
updateUsername(t, 'user' + process.pid);
|
updateUsername(t, 'user' + process.pid);
|
||||||
|
|
||||||
const usermap = new Map<Handle, LocalAction>();
|
observe(t, ds, $Present, during((_t, e0) => {
|
||||||
t.assert(ds, fromObserve(Observe({
|
const e = asPresent(e0);
|
||||||
label: $Present,
|
console.log(`${e.username} arrived`);
|
||||||
observer: t.ref({
|
return (_t) => console.log(`${e.username} departed`);
|
||||||
assert(_t: Turn, e0: Assertion, h: Handle): void {
|
}));
|
||||||
const e = asPresent(e0);
|
|
||||||
console.log(`${e.username} arrived`);
|
|
||||||
usermap.set(h, (_t: Turn) => console.log(`${e.username} departed`));
|
|
||||||
},
|
|
||||||
retract(t: Turn, h: Handle): void {
|
|
||||||
usermap.get(h)?.(t);
|
|
||||||
usermap.delete(h);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})));
|
|
||||||
|
|
||||||
t.assert(ds, fromObserve(Observe({
|
observe(t, ds, $Says, {
|
||||||
label: $Says,
|
message(_t: Turn, u0: Assertion): void {
|
||||||
observer: t.ref({
|
const u = asSays(u0);
|
||||||
message(_t: Turn, u0: Assertion): void {
|
console.log(`${u.who}: ${u.what}`);
|
||||||
const u = asSays(u0);
|
},
|
||||||
console.log(`${u.who}: ${u.what}`);
|
});
|
||||||
},
|
|
||||||
}),
|
|
||||||
})));
|
|
||||||
|
|
||||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue