added NPM packages

This commit is contained in:
Tyler Brown Cifu Shuster 2021-01-26 21:45:18 -08:00
parent 8f3afbd0ef
commit 41ebbcc82f
45 changed files with 8626 additions and 0 deletions

1
pkg/npm/README.md Normal file
View File

@ -0,0 +1 @@
Each one of the folders in this directory is published at `@urbit/{folder name}`

3
pkg/npm/api/.eslintrc.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
extends: "@urbit"
};

85
pkg/npm/api/contacts/index.d.ts vendored Normal file
View File

@ -0,0 +1,85 @@
import { Path, Patp } from "..";
export type ContactUpdate =
| ContactUpdateCreate
| ContactUpdateDelete
| ContactUpdateAdd
| ContactUpdateRemove
| ContactUpdateEdit
| ContactUpdateInitial
| ContactUpdateContacts;
interface ContactUpdateCreate {
create: Path;
}
interface ContactUpdateDelete {
delete: Path;
}
interface ContactUpdateAdd {
add: {
path: Path;
ship: Patp;
contact: Contact;
};
}
interface ContactUpdateRemove {
remove: {
path: Path;
ship: Patp;
};
}
interface ContactUpdateEdit {
edit: {
path: Path;
ship: Patp;
"edit-field": ContactEdit;
};
}
interface ContactUpdateInitial {
initial: Rolodex;
}
interface ContactUpdateContacts {
contacts: {
path: Path;
contacts: Contacts;
};
}
//
type ContactAvatar = ContactAvatarUrl | ContactAvatarOcts;
export type Rolodex = {
[p in Path]: Contacts;
};
export type Contacts = {
[p in Patp]: Contact;
};
interface ContactAvatarUrl {
url: string;
}
interface ContactAvatarOcts {
octs: string;
}
export interface Contact {
nickname: string;
email: string;
phone: string;
website: string;
notes: string;
color: string;
avatar: string | null;
}
export type ContactEdit = {
[k in keyof Contact]: Contact[k];
};

View File

@ -0,0 +1,83 @@
import { Enc, Path, Patp, Poke } from "..";
import { Contact, ContactEdit, ContactUpdateCreate, ContactUpdateEdit, ContactUpdateRemove } from "./index.d";
import { GroupPolicy, Resource } from "../groups/index.d"
export const viewAction = <T>(data: T): Poke<T> => ({
app: 'contact-view',
mark: 'json',
json: data
});
export const hookAction = <T>(data: T): Poke<T> => ({
app: 'contact-hook',
mark: 'contact-action',
json: data
});
export const create = (
name: string,
policy: Enc<GroupPolicy>,
title: string,
description: string
): Poke<ContactUpdateCreate> => viewAction({ // TODO which type is correct?
create: {
name,
policy,
title,
description
}
});
export const share = (
recipient: Patp,
path: Patp,
ship: Patp,
contact: Contact
): Poke<any> => viewAction({ // TODO type
share: {
recipient,
path,
ship,
contact
}
});
export const remove = (
path: Path,
ship: Patp
): Poke<ContactUpdateRemove> => viewAction({
remove: {
path,
ship
}
});
export const edit = (
path: Path,
ship: Patp,
editField: ContactEdit
): Poke<ContactUpdateEdit> => hookAction({
edit: {
path,
ship,
'edit-field': editField
}
});
export const invite = (
resource: Resource,
ship: Patp,
text: string = ''
): Poke<any> => viewAction({ // TODO type
invite: {
resource,
ship,
text
}
});
export const join = (
resource: Resource
): Poke<any> => viewAction({ // TODO type
join: resource
});

47
pkg/npm/api/graph/index.d.ts vendored Normal file
View File

@ -0,0 +1,47 @@
import { Patp } from "..";
import BigIntOrderedMap from "../lib/BigIntOrderedMap";
export interface TextContent {
text: string;
}
export interface UrlContent {
url: string;
}
export interface CodeContent {
code: {
expresssion: string;
output: string | undefined;
}
}
export interface ReferenceContent {
uid: string;
}
export interface MentionContent {
mention: string;
}
export type Content =
| TextContent
| UrlContent
| CodeContent
| ReferenceContent
| MentionContent;
export interface Post {
author: Patp;
contents: Content[];
hash: string | null;
index: string;
pending?: boolean;
signatures: string[];
"time-sent": number;
}
export interface GraphNode {
children: Graph;
post: Post;
}
export type Graph = BigIntOrderedMap<GraphNode>;
export type Graphs = { [rid: string]: Graph };

364
pkg/npm/api/graph/index.ts Normal file
View File

@ -0,0 +1,364 @@
import _ from 'lodash';
import { PatpNoSig, Patp, Poke, Thread, Path, Enc } from '..';
import { Content, GraphNode, Post } from './index.d';
import { deSig, unixToDa } from '../lib/util';
import { makeResource, resourceFromPath } from '../groups/index';
import { GroupPolicy } from '../groups';
export const createBlankNodeWithChildPost = (
ship: PatpNoSig,
parentIndex: string = '',
childIndex: string = '',
contents: Content[]
): GraphNode => {
const date = unixToDa(Date.now()).toString();
const nodeIndex = parentIndex + '/' + date;
const childGraph = {};
childGraph[childIndex] = {
post: {
author: `~${ship}`,
index: nodeIndex + '/' + childIndex,
'time-sent': Date.now(),
contents,
hash: null,
signatures: []
},
children: null
};
return {
post: {
author: `~${ship}`,
index: nodeIndex,
'time-sent': Date.now(),
contents: [],
hash: null,
signatures: []
},
children: childGraph
};
};
function markPending(nodes: any) {
_.forEach(nodes, node => {
node.post.author = deSig(node.post.author);
node.post.pending = true;
markPending(node.children || {});
});
}
export const createPost = (
ship: PatpNoSig,
contents: Content[],
parentIndex: string = '',
childIndex:string = 'DATE_PLACEHOLDER'
): Post => {
if (childIndex === 'DATE_PLACEHOLDER') {
childIndex = unixToDa(Date.now()).toString();
}
return {
author: `~${ship}`,
index: parentIndex + '/' + childIndex,
'time-sent': Date.now(),
contents,
hash: null,
signatures: []
};
};
function moduleToMark(mod: string): string | undefined {
if(mod === 'link') {
return 'graph-validator-link';
}
if(mod === 'publish') {
return 'graph-validator-publish';
}
if(mod === 'chat') {
return 'graph-validator-chat';
}
return undefined;
}
export const storeAction = <T>(data: T): Poke<T> => ({
app: 'graph-store',
mark: 'graph-update',
json: data
});
export const viewAction = <T>(threadName: string, action: T): Thread<T> => ({
inputMark: 'graph-view-action',
outputMark: 'json',
threadName,
body: action
});
export const hookAction = <T>(data: T): Poke<T> => ({
app: 'graph-push-hook',
mark: 'graph-update',
json: data
});
export const createManagedGraph = (
ship: PatpNoSig,
name: string,
title: string,
description: string,
group: Path,
mod: string
): Thread<any> => {
const associated = { group: resourceFromPath(group) };
const resource = makeResource(`~${ship}`, name);
return viewAction('graph-create', {
create: {
resource,
title,
description,
associated,
module: mod,
mark: moduleToMark(mod)
}
});
}
export const createUnmanagedGraph = (
ship: PatpNoSig,
name: string,
title: string,
description: string,
policy: Enc<GroupPolicy>,
mod: string
): Thread<any> => {
const resource = makeResource(`~${ship}`, name);
return viewAction('graph-create', {
create: {
resource,
title,
description,
associated: { policy },
module: mod,
mark: moduleToMark(mod)
}
});
}
export const joinGraph = (
ship: Patp,
name: string
): Thread<any> => {
const resource = makeResource(ship, name);
return viewAction('graph-join', {
join: {
resource,
ship,
}
});
}
export const deleteGraph = (
ship: PatpNoSig,
name: string
): Thread<any> => {
const resource = makeResource(`~${ship}`, name);
return viewAction('graph-delete', {
"delete": {
resource
}
});
}
export const leaveGraph = (
ship: Patp,
name: string
): Thread<any> => {
const resource = makeResource(ship, name);
return viewAction('graph-leave', {
"leave": {
resource
}
});
}
export const groupifyGraph = (
ship: Patp,
name: string,
toPath?: string
): Thread<any> => {
const resource = makeResource(ship, name);
const to = toPath && resourceFromPath(toPath);
return viewAction('graph-groupify', {
groupify: {
resource,
to
}
});
}
export const evalCord = (
cord: string
): Thread<any> => {
return ({
inputMark: 'graph-view-action',
outputMark: 'tang',
threadName: 'graph-eval',
body: {
eval: cord
}
});
}
export const addGraph = (
ship: Patp,
name: string,
graph: any,
mark: any
): Poke<any> => {
return storeAction({
'add-graph': {
resource: { ship, name },
graph,
mark
}
});
}
export const addPost = (
ship: Patp,
name: string,
post: Post
) => {
let nodes = {};
nodes[post.index] = {
post,
children: null
};
return addNodes(ship, name, nodes);
}
export const addNode = (
ship: Patp,
name: string,
node: GraphNode
) => {
let nodes = {};
nodes[node.post.index] = node;
return this.addNodes(ship, name, nodes);
}
export const addNodes = (
ship: Patp,
name: string,
nodes: Object
): Poke<any> => {
const action = {
'add-nodes': {
resource: { ship, name },
nodes
}
};
markPending(action['add-nodes'].nodes);
action['add-nodes'].resource.ship = action['add-nodes'].resource.ship.slice(1);
// this.store.handleEvent({ data: { 'graph-update': action } });// TODO address this.store
return hookAction(action);
}
export const removeNodes = (
ship: Patp,
name: string,
indices: string[]
): Poke<any> => {
return hookAction({
'remove-nodes': {
resource: { ship, name },
indices
}
});
}
// TODO these abominations
// getKeys() {
// return this.scry<any>('graph-store', '/keys')
// .then((keys) => {
// this.store.handleEvent({
// data: keys
// });
// });
// }
// getTags() {
// return this.scry<any>('graph-store', '/tags')
// .then((tags) => {
// this.store.handleEvent({
// data: tags
// });
// });
// }
// getTagQueries() {
// return this.scry<any>('graph-store', '/tag-queries')
// .then((tagQueries) => {
// this.store.handleEvent({
// data: tagQueries
// });
// });
// }
// getGraph(ship: string, resource: string) {
// return this.scry<any>('graph-store', `/graph/${ship}/${resource}`)
// .then((graph) => {
// this.store.handleEvent({
// data: graph
// });
// });
// }
// async getNewest(ship: string, resource: string, count: number, index = '') {
// const data = await this.scry<any>('graph-store', `/newest/${ship}/${resource}/${count}${index}`);
// this.store.handleEvent({ data });
// }
// async getOlderSiblings(ship: string, resource: string, count: number, index = '') {
// const idx = index.split('/').map(decToUd).join('/');
// const data = await this.scry<any>('graph-store',
// `/node-siblings/older/${ship}/${resource}/${count}${idx}`
// );
// this.store.handleEvent({ data });
// }
// async getYoungerSiblings(ship: string, resource: string, count: number, index = '') {
// const idx = index.split('/').map(decToUd).join('/');
// const data = await this.scry<any>('graph-store',
// `/node-siblings/younger/${ship}/${resource}/${count}${idx}`
// );
// this.store.handleEvent({ data });
// }
// getGraphSubset(ship: string, resource: string, start: string, end: string) {
// return this.scry<any>(
// 'graph-store',
// `/graph-subset/${ship}/${resource}/${end}/${start}`
// ).then((subset) => {
// this.store.handleEvent({
// data: subset
// });
// });
// }
// getNode(ship: string, resource: string, index: string) {
// const idx = index.split('/').map(numToUd).join('/');
// return this.scry<any>(
// 'graph-store',
// `/node/${ship}/${resource}${idx}`
// ).then((node) => {
// this.store.handleEvent({
// data: node
// });
// });
// }

177
pkg/npm/api/groups/index.d.ts vendored Normal file
View File

@ -0,0 +1,177 @@
import { PatpNoSig, Path, Jug, ShipRank, Enc } from '..';
export interface RoleTag {
tag: 'admin' | 'moderator' | 'janitor';
}
export interface AppTag {
app: string;
tag: string;
}
export type Tag = AppTag | RoleTag;
export interface InvitePolicy {
invite: {
pending: Set<PatpNoSig>;
};
}
export interface OpenPolicy {
open: {
banned: Set<PatpNoSig>;
banRanks: Set<ShipRank>;
};
}
export interface Resource {
name: string;
ship: PatpNoSig; // TODO different declaration than in metadata?
}
export type OpenPolicyDiff =
| AllowRanksDiff
| BanRanksDiff
| AllowShipsDiff
| BanShipsDiff;
export interface AllowRanksDiff {
allowRanks: ShipRank[];
}
export interface BanRanksDiff {
banRanks: ShipRank[];
}
export interface AllowShipsDiff {
allowShips: PatpNoSig[];
}
export interface BanShipsDiff {
banShips: PatpNoSig[];
}
export type InvitePolicyDiff = AddInvitesDiff | RemoveInvitesDiff;
export interface AddInvitesDiff {
addInvites: PatpNoSig[];
}
export interface RemoveInvitesDiff {
removeInvites: PatpNoSig[];
}
export interface ReplacePolicyDiff {
replace: GroupPolicy;
}
export type GroupPolicyDiff =
| { open: OpenPolicyDiff }
| { invite: InvitePolicyDiff }
| ReplacePolicyDiff;
export type GroupPolicy = OpenPolicy | InvitePolicy;
export interface TaggedShips {
[tag: string]: Set<PatpNoSig>;
}
export interface Tags {
role: TaggedShips;
[app: string]: TaggedShips;
}
export interface Group {
members: Set<PatpNoSig>;
tags: Tags;
policy: GroupPolicy;
hidden: boolean;
}
export type Groups = {
[p in Path]: Group;
};
export interface GroupUpdateInitial {
initial: Enc<Groups>;
}
export interface GroupUpdateAddGroup {
addGroup: {
resource: Resource;
policy: Enc<GroupPolicy>;
hidden: boolean;
};
}
export interface GroupUpdateAddMembers {
addMembers: {
ships: PatpNoSig[];
resource: Resource;
};
}
export interface GroupUpdateRemoveMembers {
removeMembers: {
ships: PatpNoSig[];
resource: Resource;
};
}
export interface GroupUpdateAddTag {
addTag: {
tag: Tag;
resource: Resource;
ships: PatpNoSig[];
};
}
export interface GroupUpdateRemoveTag {
removeTag: {
tag: Tag;
resource: Resource;
ships: PatpNoSig[];
};
}
export interface GroupUpdateChangePolicy {
changePolicy: { resource: Resource; diff: GroupPolicyDiff };
}
export interface GroupUpdateRemoveGroup {
removeGroup: {
resource: Resource;
};
}
export interface GroupUpdateExpose {
expose: {
resource: Resource;
};
}
export interface GroupUpdateInitialGroup {
initialGroup: {
resource: Resource;
group: Enc<Group>;
};
}
export type GroupUpdate =
| GroupUpdateInitial
| GroupUpdateAddGroup
| GroupUpdateAddMembers
| GroupUpdateRemoveMembers
| GroupUpdateAddTag
| GroupUpdateRemoveTag
| GroupUpdateChangePolicy
| GroupUpdateRemoveGroup
| GroupUpdateExpose
| GroupUpdateInitialGroup;
export type GroupAction = Omit<GroupUpdate, 'initialGroup' | 'initial'>;
export const groupBunts = {
group: (): Group => ({ members: new Set(), tags: { role: {} }, hidden: false, policy: groupBunts.policy() }),
policy: (): GroupPolicy => ({ open: { banned: new Set(), banRanks: new Set() } })
};

108
pkg/npm/api/groups/index.ts Normal file
View File

@ -0,0 +1,108 @@
import { Enc, Path, Patp, PatpNoSig, Poke } from "..";
import {
Group,
GroupAction,
GroupPolicyDiff,
GroupUpdateAddMembers,
GroupUpdateAddTag,
GroupUpdateChangePolicy,
GroupUpdateRemoveGroup,
GroupUpdateRemoveMembers,
GroupUpdateRemoveTag,
Resource,
Tag
} from "./index.d";
export const proxyAction = <T>(data: T): Poke<T> => ({
app: 'group-push-hook',
mark: 'group-update',
json: data
});
export const storeAction = <T>(data: T): Poke<T> => ({
app: 'group-store',
mark: 'group-update',
json: data
});
export const remove = (
resource: Resource,
ships: PatpNoSig[]
): Poke<GroupUpdateRemoveMembers> => proxyAction({
removeMembers: {
resource,
ships
}
});
export const addTag = (
resource: Resource,
tag: Tag,
ships: Patp[]
): Poke<GroupUpdateAddTag> => proxyAction({
addTag: {
resource,
tag,
ships
}
});
export const removeTag = (
tag: Tag,
resource: Resource,
ships: PatpNoSig[]
): Poke<GroupUpdateRemoveTag> => proxyAction({
removeTag: {
tag,
resource,
ships
}
});
export const add = (
resource: Resource,
ships: PatpNoSig[]
): Poke<GroupUpdateAddMembers> => proxyAction({
addMembers: {
resource,
ships
}
});
export const removeGroup = (
resource: Resource
): Poke<GroupUpdateRemoveGroup> => storeAction({
removeGroup: {
resource
}
});
export const changePolicy = (
resource: Resource,
diff: GroupPolicyDiff
): Poke<GroupUpdateChangePolicy> => proxyAction({
changePolicy: {
resource,
diff
}
});
const roleTags = ['janitor', 'moderator', 'admin'];
// TODO make this type better?
export function roleForShip(group: Group, ship: PatpNoSig): string | undefined {
return roleTags.reduce((currRole, role) => {
const roleShips = group?.tags?.role?.[role];
return roleShips && roleShips.has(ship) ? role : currRole;
}, undefined as string | undefined);
}
export function resourceFromPath(path: Path): Resource {
const [, , ship, name] = path.split('/');
return { ship, name }
}
export function makeResource(ship: string, name:string) {
return { ship, name };
}

78
pkg/npm/api/hark/index.d.ts vendored Normal file
View File

@ -0,0 +1,78 @@
import { Content, Post } from "../graph/index.d";
import { GroupUpdate } from "../groups/index.d";
import BigIntOrderedMap from "../lib/BigIntOrderedMap";
export type GraphNotifDescription = "link" | "comment" | "note" | "mention";
export interface UnreadStats {
unreads: Set<string> | number;
notifications: number;
last: number;
}
export interface GraphNotifIndex {
graph: string;
group: string;
description: GraphNotifDescription;
module: string;
index: string;
}
export interface GroupNotifIndex {
group: string;
description: string;
}
export interface ChatNotifIndex {
chat: string;
mention: boolean;
}
export type NotifIndex =
| { graph: GraphNotifIndex }
| { group: GroupNotifIndex }
| { chat: ChatNotifIndex };
export type GraphNotificationContents = Post[];
export type GroupNotificationContents = GroupUpdate[];
export type ChatNotificationContents = Content[];
export type NotificationContents =
| { graph: GraphNotificationContents }
| { group: GroupNotificationContents }
| { chat: ChatNotificationContents };
export interface Notification {
read: boolean;
time: number;
contents: NotificationContents;
}
export interface IndexedNotification {
index: NotifIndex;
notification: Notification;
}
export type Timebox = IndexedNotification[];
export type Notifications = BigIntOrderedMap<Timebox>;
export interface NotificationGraphConfig {
watchOnSelf: boolean;
mentions: boolean;
watching: WatchedIndex[]
}
export interface Unreads {
chat: Record<string, UnreadStats>;
graph: Record<string, Record<string, UnreadStats>>;
group: Record<string, UnreadStats>;
}
interface WatchedIndex {
graph: string;
index: string;
}
export type GroupNotificationsConfig = string[];

View File

67
pkg/npm/api/index.d.ts vendored Normal file
View File

@ -0,0 +1,67 @@
/**
* Martian embassy
*/
// an urbit style path rendered as string
export type Path = string;
// patp including leading sig
export type Patp = string;
// patp excluding leading sig
export type PatpNoSig = string;
// @uvH encoded string
export type Serial = string;
// jug from hoon
export type Jug<K,V> = Map<K,Set<V>>;
// name of app
export type AppName = 'chat' | 'link' | 'contacts' | 'publish' | 'graph';
export function getTagFromFrond<O>(frond: O): keyof O {
const tags = Object.keys(frond) as Array<keyof O>;
const tag = tags[0];
if(!tag) {
throw new Error("bad frond");
}
return tag;
}
export type ShipRank = 'czar' | 'king' | 'duke' | 'earl' | 'pawn';
export type Action = 'poke' | 'subscribe' | 'ack' | 'unsubscribe' | 'delete';
export type SetElement<S> = S extends Set<(infer T)> ? T : never;
export type MapKey<M> = M extends Map<(infer K), any> ? K : never;
export type MapValue<M> = M extends Map<any, (infer V)> ? V : never;
/**
* Turns sets into arrays and maps into objects so we can send them over the wire
*/
export type Enc<S> =
S extends Set<any> ?
Enc<SetElement<S>>[] :
S extends Map<string, any> ?
{ [s: string]: Enc<MapValue<S>> } :
S extends object ?
{ [K in keyof S]: Enc<S[K]> } :
S;
export type Mark = string;
export interface Poke<Action> {
ship?: string; // This should be handled by the http library, but is part of the spec
app: string;
mark: Mark;
json: Action;
}
export interface Thread<Action> {
inputMark: string;
outputMark: string;
threadName: string;
body: Action;
}

5
pkg/npm/api/index.js Normal file
View File

@ -0,0 +1,5 @@
import BigIntOrderedMap from './lib/BigIntOrderedMap';
export {
BigIntOrderedMap
};

85
pkg/npm/api/invite/index.d.ts vendored Normal file
View File

@ -0,0 +1,85 @@
import { Serial, PatpNoSig, Path } from '..';
export type InviteUpdate =
InviteUpdateInitial
| InviteUpdateCreate
| InviteUpdateDelete
| InviteUpdateInvite
| InviteUpdateAccepted
| InviteUpdateDecline;
export interface InviteUpdateInitial {
initial: Invites;
}
export interface InviteUpdateCreate {
create: {
path: Path;
};
}
export interface InviteUpdateDelete {
delete: {
path: Path;
};
}
export interface InviteUpdateInvite {
invite: {
path: Path;
uid: Serial;
invite: Invite;
};
}
export interface InviteUpdateAccepted {
accepted: {
path: Path;
uid: Serial;
};
}
export interface InviteUpdateDecline {
decline: {
path: Path;
uid: Serial;
};
}
export type InviteAction =
InviteActionAccept
| InviteActionDecline;
export interface InviteActionAccept {
accept: {
term: string,
uid: Serial
}
}
export interface InviteActionDecline {
decline: {
term: string,
uid: Serial
}
}
// actual datastructures
export type Invites = {
[p in Path]: AppInvites;
};
export type AppInvites = {
[s in Serial]: Invite;
};
export interface Invite {
app: string;
path: Path;
recipient: PatpNoSig;
ship: PatpNoSig;
text: string;
}

View File

@ -0,0 +1,28 @@
import { InviteAction, InviteActionAccept, InviteActionDecline } from "./index.d";
import { Poke, Serial } from "..";
export const action = <T>(data: T): Poke<T> => ({
app: 'invite-store',
mark: 'invite-action',
json: data
});
export const accept = (
app: string,
uid: Serial
): Poke<InviteActionAccept> => action({
accept: {
term: app,
uid
}
});
export const decline = (
app: string,
uid: Serial
): Poke<InviteActionDecline> => action({
decline: {
term: app,
uid
}
});

View File

@ -0,0 +1,233 @@
import { BigInteger } from "big-integer";
interface NonemptyNode<V> {
n: [BigInteger, V];
l: MapNode<V>;
r: MapNode<V>;
}
type MapNode<V> = NonemptyNode<V> | null;
/**
* An implementation of ordered maps for JS
* Plagiarised wholesale from sys/zuse
*/
export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
private root: MapNode<V> = null;
size: number = 0;
constructor(initial: [BigInteger, V][] = []) {
initial.forEach(([key, val]) => {
this.set(key, val);
});
}
/**
* Retrieve an value for a key
*/
get(key: BigInteger): V | null {
const inner = (node: MapNode<V>) => {
if (!node) {
return node;
}
const [k, v] = node.n;
if (key.eq(k)) {
return v;
}
if (key.gt(k)) {
return inner(node.l);
} else {
return inner(node.r);
}
};
return inner(this.root);
}
/**
* Put an item by a key
*/
set(key: BigInteger, value: V): void {
const inner = (node: MapNode<V>) => {
if (!node) {
return {
n: [key, value],
l: null,
r: null,
};
}
const [k] = node.n;
if (key.eq(k)) {
this.size--;
return {
...node,
n: [k, value],
};
}
if (key.gt(k)) {
const l = inner(node.l);
if (!l) {
throw new Error("invariant violation");
}
return {
...node,
l,
};
}
const r = inner(node.r);
if (!r) {
throw new Error("invariant violation");
}
return { ...node, r };
};
this.size++;
this.root = inner(this.root);
}
/**
* Remove all entries
*/
clear() {
this.root = null;
}
/**
* Predicate testing if map contains key
*/
has(key: BigInteger): boolean {
const inner = (node: MapNode<V>) => {
if (!node) {
return false;
}
const [k] = node.n;
if (k.eq(key)) {
return true;
}
if (key.gt(k)) {
return inner(node.l);
}
return inner(node.r);
};
return inner(this.root);
}
/**
* Remove value associated with key, returning whether that key
* existed in the first place
*/
delete(key: BigInteger) {
const inner = (node: MapNode<V>): [boolean, MapNode<V>] => {
if (!node) {
return [false, null];
}
const [k] = node.n;
if (k.eq(key)) {
return [true, this.nip(node)];
}
if (key.gt(k)) {
const [bool, l] = inner(node.l);
return [
bool,
{
...node,
l,
},
];
}
const [bool, r] = inner(node.r);
return [
bool,
{
...node,
r,
},
];
};
const [ret, newRoot] = inner(this.root);
if(ret) {
this.size--;
}
this.root = newRoot;
return ret;
}
private nip(nod: NonemptyNode<V>): MapNode<V> {
const inner = (node: NonemptyNode<V>) => {
if (!node.l) {
return node.r;
}
if (!node.r) {
return node.l;
}
return {
...node.l,
r: inner(node.r),
};
};
return inner(nod);
}
peekLargest(): [BigInteger, V] | undefined {
const inner = (node: MapNode<V>) => {
if(!node) {
return undefined;
}
if(node.l) {
return inner(node.l);
}
return node.n;
}
return inner(this.root);
}
peekSmallest(): [BigInteger, V] | undefined {
const inner = (node: MapNode<V>) => {
if(!node) {
return undefined;
}
if(node.r) {
return inner(node.r);
}
return node.n;
}
return inner(this.root);
}
keys(): BigInteger[] {
const list = Array.from(this);
return list.map(([key]) => key);
}
forEach(f: (value: V, key: BigInteger) => void) {
const list = Array.from(this);
return list.forEach(([k,v]) => f(v,k));
}
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
let result: [BigInteger, V][] = [];
const inner = (node: MapNode<V>) => {
if (!node) {
return;
}
inner(node.l);
result.push(node.n);
inner(node.r);
};
inner(this.root);
let idx = 0;
return {
[Symbol.iterator]: this[Symbol.iterator],
next: (): IteratorResult<[BigInteger, V]> => {
if (idx < result.length) {
return { value: result[idx++], done: false };
}
return { done: true, value: null };
},
};
}
}

211
pkg/npm/api/lib/util.ts Normal file
View File

@ -0,0 +1,211 @@
import _ from "lodash";
import f from "lodash/fp";
import bigInt, { BigInteger } from "big-integer";
import { Resource } from "../groups/index.d";
const DA_UNIX_EPOCH = bigInt("170141184475152167957503069145530368000"); // `@ud` ~1970.1.1
const DA_SECOND = bigInt("18446744073709551616"); // `@ud` ~s1
/**
* Returns true if an app uses a graph backend
*
* @param {string} app The name of the app
*
* @return {boolean} Whether or not it uses a graph backend
*/
export function appIsGraph(app: string): boolean {
return app === 'publish' || app == 'link';
}
/**
* Given a bigint representing an urbit date, returns a unix timestamp.
*
* @param {BigInteger} da The urbit date
*
* @return {number} The unix timestamp
*/
export function daToUnix(da: BigInteger): number {
// ported from +time:enjs:format in hoon.hoon
const offset = DA_SECOND.divide(bigInt(2000));
const epochAdjusted = offset.add(da.subtract(DA_UNIX_EPOCH));
return Math.round(
epochAdjusted.multiply(bigInt(1000)).divide(DA_SECOND).toJSNumber()
);
}
/**
* Given a unix timestamp, returns a bigint representing an urbit date
*
* @param {number} unix The unix timestamp
*
* @return {BigInteger} The urbit date
*/
export function unixToDa(unix: number): BigInteger {
const timeSinceEpoch = bigInt(unix).multiply(DA_SECOND).divide(bigInt(1000));
return DA_UNIX_EPOCH.add(timeSinceEpoch);
}
export function makePatDa(patda: string): BigInteger {
return bigInt(udToDec(patda));
}
export function udToDec(ud: string): string {
return ud.replace(/\./g, "");
}
export function decToUd(str: string): string {
return _.trimStart(
f.flow(
f.split(""),
f.reverse,
f.chunk(3),
f.map(f.flow(f.reverse, f.join(""))),
f.reverse,
f.join(".")
)(str),
"0."
);
}
export function resourceAsPath(resource: Resource): string {
const { name, ship } = resource;
return `/ship/~${ship}/${name}`;
}
export function uuid(): string {
let str = "0v";
str += Math.ceil(Math.random() * 8) + ".";
for (let i = 0; i < 5; i++) {
let _str = Math.ceil(Math.random() * 10000000).toString(32);
_str = ("00000" + _str).substr(-5, 5);
str += _str + ".";
}
return str.slice(0, -1);
}
/*
Goes from:
~2018.7.17..23.15.09..5be5 // urbit @da
To:
(javascript Date object)
*/
export function daToDate(st: string): Date {
const dub = function (n: string) {
return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString();
};
const da = st.split("..");
const bigEnd = da[0].split(".");
const lilEnd = da[1].split(".");
const ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(
lilEnd[0]
)}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
return new Date(ds);
}
/*
Goes from:
(javascript Date object)
To:
~2018.7.17..23.15.09..5be5 // urbit @da
*/
export function dateToDa(d: Date, mil: boolean = false): string {
const fil = function (n: number) {
return n >= 10 ? n : "0" + n;
};
return (
`~${d.getUTCFullYear()}.` +
`${d.getUTCMonth() + 1}.` +
`${fil(d.getUTCDate())}..` +
`${fil(d.getUTCHours())}.` +
`${fil(d.getUTCMinutes())}.` +
`${fil(d.getUTCSeconds())}` +
`${mil ? "..0000" : ""}`
);
}
export function deSig(ship: string): string | null {
if (!ship) {
return null;
}
return ship.replace("~", "");
}
// trim patps to match dojo, chat-cli
export function cite(ship: string): string {
let patp = ship,
shortened = "";
if (patp === null || patp === "") {
return null;
}
if (patp.startsWith("~")) {
patp = patp.substr(1);
}
// comet
if (patp.length === 56) {
shortened = "~" + patp.slice(0, 6) + "_" + patp.slice(50, 56);
return shortened;
}
// moon
if (patp.length === 27) {
shortened = "~" + patp.slice(14, 20) + "^" + patp.slice(21, 27);
return shortened;
}
return `~${patp}`;
}
// encode the string into @ta-safe format, using logic from +wood.
// for example, 'some Chars!' becomes '~.some.~43.hars~21.'
//
export function stringToTa(str: string): string {
let out = "";
for (let i = 0; i < str.length; i++) {
const char = str[i];
let add = "";
switch (char) {
case " ":
add = ".";
break;
case ".":
add = "~.";
break;
case "~":
add = "~~";
break;
default:
const charCode = str.charCodeAt(i);
if (
(charCode >= 97 && charCode <= 122) || // a-z
(charCode >= 48 && charCode <= 57) || // 0-9
char === "-"
) {
add = char;
} else {
// TODO behavior for unicode doesn't match +wood's,
// but we can probably get away with that for now.
add = "~" + charCode.toString(16) + ".";
}
}
out = out + add;
}
return "~." + out;
}
/**
* Formats a numbers as a `@ud` inserting dot where needed
*/
export function numToUd(num: number): string {
return f.flow(
f.split(''),
f.reverse,
f.chunk(3),
f.reverse,
f.map(s => s.join('')),
f.join('.')
)(num.toString())
}

55
pkg/npm/api/metadata/index.d.ts vendored Normal file
View File

@ -0,0 +1,55 @@
import { AppName, Path, Patp } from '..';
export type MetadataUpdate =
MetadataUpdateInitial
| MetadataUpdateAdd
| MetadataUpdateUpdate
| MetadataUpdateRemove;
export interface MetadataUpdateInitial {
associations: ResourceAssociations;
}
export type ResourceAssociations = {
[p in Path]: Association;
}
export type MetadataUpdateAdd = {
add: Association;
}
export type MetadataUpdateUpdate = {
update: Association;
}
export type MetadataUpdateRemove = {
remove: Resource & {
'group-path': Path;
}
}
export type Associations = Record<AppName, AppAssociations>;
export type AppAssociations = {
[p in Path]: Association;
}
export interface Resource {
'app-path': Path;
'app-name': AppName;
}
export type Association = Resource & {
'group-path': Path;
metadata: Metadata;
};
export interface Metadata {
color: string;
creator: Patp;
'date-created': string;
description: string;
title: string;
module: string;
}

View File

@ -0,0 +1,55 @@
import { AppName, Path, PatpNoSig, Poke } from "..";
import { Association, Metadata, MetadataUpdateAdd, MetadataUpdateUpdate } from './index.d';
export const action = <T>(data: T): Poke<T> => ({
app: 'metadata-hook',
mark: 'metadata-action',
json: data
});
export const add = (
ship: PatpNoSig,
appName: AppName,
appPath: Path,
groupPath: Path,
title: string,
description: string,
dateCreated: string,
color: string,
moduleName: string
): Poke<MetadataUpdateAdd> => {
const creator = `~${ship}`;
return action({
add: {
'group-path': groupPath,
resource: {
'app-path': appPath,
'app-name': appName
},
metadata: {
title,
description,
color,
'date-created': dateCreated,
creator,
'module': moduleName
}
}
});
}
export const update = (
association: Association,
newMetadata: Partial<Metadata>
): Poke<MetadataUpdateUpdate> => {
return action({
add: {
'group-path': association['group-path'],
resource: {
'app-path': association['app-path'],
'app-name': association['app-name'],
},
metadata: {...association.metadata, ...newMetadata }
}
});
}

31
pkg/npm/api/package-lock.json generated Normal file
View File

@ -0,0 +1,31 @@
{
"name": "@urbit/api",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@babel/runtime": {
"version": "7.12.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"big-integer": {
"version": "1.6.48",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
"integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w=="
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
}
}
}

18
pkg/npm/api/package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "@urbit/api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Tyler Brown Cifu Shuster",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@urbit/eslint-config": "^1.0.0",
"big-integer": "^1.6.48",
"lodash": "^4.17.20"
}
}

27
pkg/npm/api/tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": false,
"noImplicitReturns": true,
"moduleResolution": "node",
"esModuleInterop": true,
"noUnusedLocals": false,
"noImplicitAny": false,
"noEmit": true,
"target": "es2015",
"module": "es2015",
"strict": true,
"jsx": "react",
"baseUrl": ".",
"paths": {
"~/*": ["src/*"]
}
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
]
}

View File

@ -0,0 +1,186 @@
const env = {
"browser": true,
"es6": true,
"node": true
};
const rules = {
"array-bracket-spacing": ["error", "never"],
"arrow-parens": [
"error",
"as-needed",
{
"requireForBlockBody": true
}
],
"arrow-spacing": "error",
"block-spacing": ["error", "always"],
"brace-style": ["error", "1tbs"],
"camelcase": [
"error",
{
"properties": "never"
}
],
"comma-dangle": ["error", "never"],
"eol-last": ["error", "always"],
"func-name-matching": "error",
"indent": [
"off",
2,
{
"ArrayExpression": "off",
"SwitchCase": 1,
"CallExpression": {
"arguments": "off"
},
"FunctionDeclaration": {
"parameters": "off"
},
"FunctionExpression": {
"parameters": "off"
},
"MemberExpression": "off",
"ObjectExpression": "off",
"ImportDeclaration": "off"
}
],
"handle-callback-err": "off",
"linebreak-style": ["error", "unix"],
"max-lines": [
"error",
{
"max": 300,
"skipBlankLines": true,
"skipComments": true
}
],
"max-lines-per-function": [
"warn",
{
"skipBlankLines": true,
"skipComments": true
}
],
"max-statements-per-line": [
"error",
{
"max": 1
}
],
"new-cap": [
"error",
{
"newIsCap": true,
"capIsNew": false
}
],
"new-parens": "error",
"no-buffer-constructor": "error",
"no-console": "off",
"no-extra-semi": "off",
"no-fallthrough": "off",
"no-func-assign": "off",
"no-implicit-coercion": "error",
"no-multi-assign": "error",
"no-multiple-empty-lines": [
"error",
{
"max": 1
}
],
"no-nested-ternary": "error",
"no-param-reassign": "off",
"no-return-assign": "error",
"no-return-await": "off",
"no-shadow-restricted-names": "error",
"no-tabs": "error",
"no-trailing-spaces": "error",
"no-unused-vars": [
"error",
{
"vars": "all",
"args": "none",
"ignoreRestSiblings": false
}
],
"no-use-before-define": [
"error",
{
"functions": false,
"classes": false
}
],
"no-useless-escape": "off",
"no-var": "error",
"nonblock-statement-body-position": ["error", "below"],
"object-curly-spacing": ["error", "always"],
"padded-blocks": ["error", "never"],
"prefer-arrow-callback": "error",
"prefer-const": [
"error",
{
"destructuring": "all",
"ignoreReadBeforeAssign": true
}
],
"prefer-template": "off",
"quotes": ["error", "single"],
"semi": ["error", "always"],
"spaced-comment": [
"error",
"always",
{
"exceptions": ["!"]
}
],
"space-before-blocks": "error",
"unicode-bom": ["error", "never"],
"valid-jsdoc": "error",
"wrap-iife": ["error", "inside"],
"react/jsx-closing-bracket-location": 1,
"react/jsx-tag-spacing": 1,
"react/jsx-max-props-per-line": ["error", { "maximum": 2, "when": "multiline" }],
"react/prop-types": 0
};
module.exports = {
"env": env,
"extends": [
"plugin:react/recommended",
"eslint:recommended",
],
"settings": {
"react": {
"version": "^16.5.2"
}
},
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 10,
"requireConfigFile": false,
"sourceType": "module"
},
"root": true,
"rules": rules,
"overrides": [
{
"files": ["**/*.ts", "**/*.tsx"],
"env": env,
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": { "jsx": true },
"ecmaVersion": 10,
"requireConfigFile": false,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": rules
}
]
};

5
pkg/npm/eslint-config/package-lock.json generated Normal file
View File

@ -0,0 +1,5 @@
{
"name": "@urbit/eslint-config",
"version": "1.0.0",
"lockfileVersion": 1
}

View File

@ -0,0 +1,14 @@
{
"name": "@urbit/eslint-config",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Tyler Brown Cifu Shuster",
"license": "MIT",
"peerDependencies": {
"eslint": ">= 3"
}
}

View File

@ -0,0 +1,42 @@
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Begin CI...
uses: actions/checkout@v2
- name: Use Node 12
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Use cached node_modules
uses: actions/cache@v1
with:
path: node_modules
key: nodeModules-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
nodeModules-
- name: Install dependencies
run: yarn install --frozen-lockfile
env:
CI: true
- name: Lint
run: yarn lint
env:
CI: true
- name: Test
run: yarn test --ci --coverage --maxWorkers=2
env:
CI: true
- name: Build
run: yarn build
env:
CI: true

0
pkg/npm/http-api/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,5 @@
{
"files.watcherExclude": {
"**/node_modules/**": false
}
}

21
pkg/npm/http-api/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Tyler Brown Cifu Shuster
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,21 @@
# Urbit Connector
This project allows you to connect to an [Urbit](https://urbit.org) ship via a JavaScript application.
## Example
Check out the `example` directory for examples of how to use this code.
1. Open `example/index.html` in your browser and follow the instructions there, or
2. With a ship running in the same fashion as indicated in the file above, run `node example/index.js`
The code for either of these can be found in `src/example/browser.js` or `src/example/node.js`, depending on your context.
## Design
This library is designed to be useful for node applications that communicate with an urbit running either on the local computer or on a remote one.
The majority of its methods are asynchronous and return Promises. This is due to the non-blocking nature of JavaScript. If used in a React app, response handlers should be bound with `this` to `setState` after a message is received.
## NOTE
You must enable CORS requests on your urbit for this library to work in browser context. Use `+cors-registry` to see domains which have made requests to your urbit, and then approve the needed one, e.g. `|cors-approve http://zod.arvo.network`.

View File

@ -0,0 +1,17 @@
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is not neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/*!********************************!*\
!*** ./src/example/browser.js ***!
\********************************/
/*! unknown exports (runtime-defined) */
/*! runtime requirements: */
eval("// import Urbit from '../../dist/browser';\n// window.Urbit = Urbit;\n\n//# sourceURL=webpack://@urbit/http-api/./src/example/browser.js?");
/******/ })()
;

View File

@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
<script src="browser.js"></script>
<style>
@import url("https://rsms.me/inter/inter.css");
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
font-weight: 400;
}
body {
margin: 0 auto;
max-width: 70ch;
padding: 2ch;
font-family: 'Inter', sans-serif;
}
#mylog {
white-space: pre-wrap;
padding: 2ch;
background: black;
color: white;
font-family: 'Source Code Pro', monospace;
}
#mylog div {
margin-bottom: 1rem;
}
.chunk {
border-bottom: 1px dashed currentColor;
}
</style>
</head>
<body>
<details>
<summary>Show instructions</summary>
<p>Assuming you are running a fakezod on port 8080, run</p>
<code id="instructions">|cors-approve '{window.location.origin}'</code>
<p>in its dojo.</p>
<p>Press the button to run the code below. Output will be logged. You should see <code>&lt; ~zod: opening airlock</code> in your dojo.</code> Create a chat and send a message to see the events logged.</p>
<pre>window.airlock = await Urbit.authenticate({
ship: 'zod',
url: 'localhost:8080',
code: 'lidlut-tabwed-pillex-ridrup',
verbose: true
});
window.airlock.subscribe('chat-view', '/primary', { event: console.log });</pre>
</details>
<button id="blastoff" onclick="blastOff()">Blast Off</button>
<pre id="mylog">
</pre>
</body>
<script>
var baseLogFunction = console.log;
console.log = function(){
baseLogFunction.apply(console, arguments);
var chunk = document.createElement('div');
chunk.className = 'chunk';
var args = Array.prototype.slice.call(arguments);
for(var i=0;i<args.length;i++){
const val = typeof args[i] === 'string' ? args[i] : JSON.stringify(args[i]);
var node = createLogNode(val);
chunk.appendChild(node);
}
document.querySelector("#mylog").insertBefore(chunk, document.querySelector("#mylog").firstChild);
}
function createLogNode(message){
var node = document.createElement("div");
node.className = 'message';
var textNode = document.createTextNode(message);
node.appendChild(textNode);
return node;
}
window.onerror = function(message, url, linenumber) {
console.log("JavaScript error: " + message + " on line " +
linenumber + " for " + url);
}
const instructions = document.getElementById('instructions');
instructions.innerText = instructions.innerText.replace('{window.location.origin}', window.location.origin);
async function blastOff() {
window.airlock = await Urbit.authenticate({
ship: 'zod',
url: 'localhost:8080',
code: 'lidlut-tabwed-pillex-ridrup',
verbose: true
});
window.airlock.subscribe('chat-view', '/primary', { event: console.log });
document.body.removeChild(document.getElementById('blastoff'))
}
</script>
</html>

View File

@ -0,0 +1,17 @@
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is not neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/*!*****************************!*\
!*** ./src/example/node.js ***!
\*****************************/
/*! unknown exports (runtime-defined) */
/*! runtime requirements: */
eval("// import Urbit from '../../dist/index';\n// async function blastOff() {\n// const airlock = await Urbit.authenticate({\n// ship: 'zod',\n// url: 'localhost:8080',\n// code: 'lidlut-tabwed-pillex-ridrup',\n// verbose: true\n// });\n// airlock.subscribe('chat-view', '/primary');\n// }\n// blastOff();\n\n//# sourceURL=webpack://@urbit/http-api/./src/example/node.js?");
/******/ })()
;

View File

@ -0,0 +1,2 @@
import Urbit from './dist';
export { Urbit as default, Urbit };

5591
pkg/npm/http-api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,64 @@
{
"name": "@urbit/http-api",
"version": "1.1.0",
"license": "MIT",
"description": "Library to interact with an Urbit ship over HTTP",
"repository": "github:tylershuster/urbit",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"browser": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=13"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "npm run clean && webpack --config webpack.prod.js && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json",
"clean": "rm -rf dist/*"
},
"peerDependencies": {},
"prettier": {
"printWidth": 80,
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
},
"author": "Tyler Brown Cifu Shuster",
"devDependencies": {
"@babel/core": "^7.12.3",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
"@babel/preset-typescript": "^7.12.1",
"@types/eventsource": "^1.1.5",
"@types/react": "^16.9.56",
"@typescript-eslint/eslint-plugin": "^4.7.0",
"@typescript-eslint/parser": "^4.7.0",
"@types/browser-or-node": "^1.2.0",
"babel-loader": "^8.2.1",
"clean-webpack-plugin": "^3.0.0",
"tslib": "^2.0.3",
"typescript": "^3.9.7",
"webpack": "^5.4.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"browser-or-node": "^1.3.0",
"browserify-zlib": "^0.2.0",
"buffer": "^5.7.1",
"encoding": "^0.1.13",
"eventsource": "^1.0.7",
"node-fetch": "^2.6.1",
"stream-browserify": "^3.0.0",
"stream-http": "^3.1.1",
"util": "^0.12.3",
"xmlhttprequest": "^1.8.0",
"xmlhttprequest-ssl": "^1.6.0"
}
}

View File

@ -0,0 +1,40 @@
import Urbit from '..';
export interface UrbitAppInterface {
airlock: Urbit;
app: string;
}
export default class UrbitApp implements UrbitAppInterface {
airlock: Urbit;
get app(): string {
throw new Error('Access app property on base UrbitApp');
}
constructor(airlock: Urbit) {
this.airlock = airlock;
}
/**
* Getter that barfs if no ship has been passed
*/
get ship(): string {
if (!this.airlock.ship) {
throw new Error('No ship specified');
}
return this.airlock.ship;
}
/**
* Helper to allow any app to handle subscriptions.
*
* @param path Path on app to subscribe to
*/
subscribe(path: string) {
const ship = this.ship;
const app = this.app;
return this.airlock.subscribe(app, path);
}
// TODO handle methods that don't exist
}

View File

@ -0,0 +1,3 @@
// import Urbit from '../../dist/browser';
// window.Urbit = Urbit;

View File

@ -0,0 +1,14 @@
// import Urbit from '../../dist/index';
// async function blastOff() {
// const airlock = await Urbit.authenticate({
// ship: 'zod',
// url: 'localhost:8080',
// code: 'lidlut-tabwed-pillex-ridrup',
// verbose: true
// });
// airlock.subscribe('chat-view', '/primary');
// }
// blastOff();

View File

@ -0,0 +1,456 @@
import { isBrowser, isNode } from 'browser-or-node';
import { Action, Thread } from '../../api';
import { AuthenticationInterface, SubscriptionInterface, CustomEventHandler, PokeInterface, SubscriptionRequestInterface, headers, UrbitInterface, SSEOptions, ThreadInterface } from './types';
import UrbitApp from './app/base';
import { uncamelize, hexString } from './utils';
/**
* A class for interacting with an urbit ship, given its URL and code
*/
export class Urbit implements UrbitInterface {
/**
* UID will be used for the channel: The current unix time plus a random hex string
*/
uid: string = `${Math.floor(Date.now() / 1000)}-${hexString(6)}`;
/**
* Last Event ID is an auto-updated index of which events have been sent over this channel
*/
lastEventId: number = 0;
lastAcknowledgedEventId: number = 0;
/**
* SSE Client is null for now; we don't want to start polling until it the channel exists
*/
sseClient: EventSource | null = null;
/**
* Cookie gets set when we log in.
*/
cookie?: string | undefined;
/**
* A registry of requestId to successFunc/failureFunc
*
* These functions are registered during a +poke and are executed
* in the onServerEvent()/onServerError() callbacks. Only one of
* the functions will be called, and the outstanding poke will be
* removed after calling the success or failure function.
*/
outstandingPokes: Map<number, object> = new Map();
/**
* A registry of requestId to subscription functions.
*
* These functions are registered during a +subscribe and are
* executed in the onServerEvent()/onServerError() callbacks. The
* event function will be called whenever a new piece of data on this
* subscription is available, which may be 0, 1, or many times. The
* disconnect function may be called exactly once.
*/
outstandingSubscriptions: Map<number, SubscriptionInterface> = new Map();
/**
* Ship can be set, in which case we can do some magic stuff like send chats
*/
ship?: string | null;
/**
* If verbose, logs output eagerly.
*/
verbose?: boolean;
/**
* All registered apps, keyed by name
*/
static apps: Map<string, typeof UrbitApp> = new Map();
/** This is basic interpolation to get the channel URL of an instantiated Urbit connection. */
get channelUrl(): string {
return `${this.url}/~/channel/${this.uid}`;
}
get fetchOptions(): any {
const headers: headers = {
'Content-Type': 'application/json',
};
if (!isBrowser) {
headers.Cookie = this.cookie;
}
return {
credentials: 'include',
headers
};
}
/**
* Constructs a new Urbit connection.
*
* @param url The URL (with protocol and port) of the ship to be accessed
* @param code The access code for the ship at that address
*/
constructor(
public url: string,
public code: string
) {
return this;
// We return a proxy so we can set dynamic properties like `Urbit.onChatHook`
return new Proxy(this, {
get(target: Urbit, property: string) {
// First check if this is a regular property
if (property in target) {
return (target as any)[property];
}
// Then check if it's a registered app
const app = Urbit.apps.get(uncamelize(property));
if (app) {
return new app(target);
}
// Then check to see if we're trying to register an EventSource watcher
if (property.startsWith('on')) {
const on = uncamelize(property.replace('on', '')).toLowerCase();
return ((action: CustomEventHandler) => {
target.eventSource().addEventListener('message', (event: MessageEvent) => {
if (target.verbose) {
console.log(`Received SSE from ${on}: `, event);
}
if (event.data && JSON.parse(event.data)) {
const data: any = JSON.parse(event.data);
if (data.json.hasOwnProperty(on)) {
action(data.json[on], data.json.response);
}
}
});
});
}
return undefined;
}
})
}
/**
* All-in-one hook-me-up.
*
* Given a ship, url, and code, this returns an airlock connection
* that is ready to go. It `|hi`s itself to create the channel,
* then opens the channel via EventSource.
*
* @param AuthenticationInterface
*/
static async authenticate({ ship, url, code, verbose = false }: AuthenticationInterface) {
const airlock = new Urbit(`http://${url}`, code);
airlock.verbose = verbose;
airlock.ship = ship;
await airlock.connect();
await airlock.poke({ app: 'hood', mark: 'helm-hi', json: 'opening airlock' });
await airlock.eventSource();
return airlock;
}
/**
* Connects to the Urbit ship. Nothing can be done until this is called.
* That's why we roll it into this.authenticate
*/
async connect(): Promise<void> {
if (this.verbose) {
console.log(`password=${this.code} `, isBrowser ? "Connecting in browser context at " + `${this.url}/~/login` : "Connecting from node context");
}
return fetch(`${this.url}/~/login`, {
method: 'post',
body: `password=${this.code}`,
credentials: 'include',
}).then(response => {
if (this.verbose) {
console.log('Received authentication response', response);
}
const cookie = response.headers.get('set-cookie');
if (!this.ship) {
this.ship = new RegExp(/urbauth-~([\w-]+)/).exec(cookie)[1];
}
if (!isBrowser) {
this.cookie = cookie;
}
}).catch(error => {
console.log(XMLHttpRequest);
console.log('errored')
console.log(error);
});
}
/**
* Returns (and initializes, if necessary) the SSE pipe for the appropriate channel.
*/
eventSource(): EventSource {
if (!this.sseClient || this.sseClient.readyState === this.sseClient.CLOSED) {
const sseOptions: SSEOptions = {
headers: {}
};
if (isBrowser) {
sseOptions.withCredentials = true;
} else if (isNode) {
sseOptions.headers.Cookie = this.cookie;
}
this.sseClient = new EventSource(this.channelUrl, {
withCredentials: true
});
this.sseClient!.addEventListener('message', (event: MessageEvent) => {
if (this.verbose) {
console.log('Received SSE: ', event);
}
this.ack(Number(event.lastEventId));
if (event.data && JSON.parse(event.data)) {
const data: any = JSON.parse(event.data);
if (data.response === 'poke' && this.outstandingPokes.has(data.id)) {
const funcs = this.outstandingPokes.get(data.id);
if (data.hasOwnProperty('ok')) {
funcs.success();
} else if (data.hasOwnProperty('err')) {
funcs.fail(data.err);
} else {
console.error('Invalid poke response', data);
}
this.outstandingPokes.delete(data.id);
} else if (data.response === 'subscribe' ||
(data.response === 'poke' && this.outstandingSubscriptions.has(data.id))) {
const funcs = this.outstandingSubscriptions.get(data.id);
if (data.hasOwnProperty('err')) {
funcs.err(data.err);
this.outstandingSubscriptions.delete(data.id);
}
} else if (data.response === 'diff' && this.outstandingSubscriptions.has(data.id)) {
const funcs = this.outstandingSubscriptions.get(data.id);
funcs.event(data.json);
} else if (data.response === 'quit' && this.outstandingSubscriptions.has(data.id)) {
const funcs = this.outstandingSubscriptions.get(data.id);
funcs.quit(data);
this.outstandingSubscriptions.delete(data.id);
} else {
console.log('Unrecognized response', data);
}
// An incoming message, for example:
// {
// id: 10,
// json: {
// 'chat-update' : { // This is where we hook our "on" handlers like "onChatUpdate"
// message: {
// envelope: {
// author: 'zod',
// letter: {
// text: 'hi'
// },
// number: 10,
// uid: 'saludhafhsdf',
// when: 124459
// },
// path: '/~zod/mailbox'
// }
// }
// }
// }
}
});
this.sseClient!.addEventListener('error', function(event: Event) {
console.error('pipe error', event);
});
}
return this.sseClient;
}
addEventListener(callback: (data: any) => void) {
return this.eventSource().addEventListener('message', (event: MessageEvent) => {
if (event.data && JSON.parse(event.data)) {
callback(JSON.parse(event.data));
}
});
}
/**
* Autoincrements the next event ID for the appropriate channel.
*/
getEventId(): number {
this.lastEventId = Number(this.lastEventId) + 1;
return this.lastEventId;
}
/**
* Acknowledges an event.
*
* @param eventId The event to acknowledge.
*/
ack(eventId: number): Promise<void | number> {
return this.sendMessage('ack', { 'event-id': eventId });
}
/**
* This is a wrapper method that can be used to send any action with data.
*
* Every message sent has some common parameters, like method, headers, and data
* structure, so this method exists to prevent duplication.
*
* @param action The action to send
* @param data The data to send with the action
*
* @returns void | number If successful, returns the number of the message that was sent
*/
async sendMessage(action: Action, data?: object): Promise<void | number> {
const id = this.getEventId();
if (this.verbose) {
console.log(`Sending message ${id}:`, action, data,);
}
let response: Response | undefined;
try {
response = await fetch(this.channelUrl, {
...this.fetchOptions,
method: 'put',
body: JSON.stringify([{
id,
action,
...data,
}]),
});
} catch (error) {
console.error('message error', error);
response = undefined;
}
if (this.verbose) {
console.log(`Received from message ${id}: `, response);
}
return id;
}
/**
* Pokes a ship with data.
*
* @param app The app to poke
* @param mark The mark of the data being sent
* @param json The data to send
*/
poke(params: PokeInterface): Promise<void | number> {
const { app, mark, json, onSuccess, onError } = {onSuccess: () => {}, onError: () => {}, ...params};
return new Promise((resolve, reject) => {
this
.sendMessage('poke', { ship: this.ship, app, mark, json })
.then(pokeId => {
if (!pokeId) {
return reject('Poke failed');
}
if (!this.sseClient) resolve(pokeId); // A poke may occur before a listener has been opened
this.outstandingPokes.set(pokeId, {
success: () => {
onSuccess();
resolve(pokeId);
},
fail: (event) => {
onError();
reject(event.err);
}
});
}).catch(error => {
console.error(error);
});
});
}
/**
* Subscribes to a path on an app on a ship.
*
* @param app The app to subsribe to
* @param path The path to which to subscribe
* @param handlers Handlers to deal with various events of the subscription
*/
async subscribe(params: SubscriptionRequestInterface): Promise<void | number> {
const { app, path, err, event, quit } = { err: () => {}, event: () => {}, quit: () => {}, ...params };
const subscriptionId = await this.sendMessage('subscribe', { ship: this.ship, app, path });
console.log('subscribed', subscriptionId);
if (!subscriptionId) return;
this.outstandingSubscriptions.set(subscriptionId, {
err, event, quit
});
return subscriptionId;
}
/**
* Unsubscribes to a given subscription.
*
* @param subscription
*/
unsubscribe(subscription: string): Promise<void | number> {
return this.sendMessage('unsubscribe', { subscription });
}
/**
* Deletes the connection to a channel.
*/
delete(): Promise<void | number> {
return this.sendMessage('delete');
}
/**
*
* @param app The app into which to scry
* @param path The path at which to scry
*/
async scry(app: string, path: string): Promise<void | any> {
const response = await fetch(`/~/scry/${app}${path}.json`, this.fetchOptions);
return await response.json();
}
/**
*
* @param inputMark The mark of the data being sent
* @param outputMark The mark of the data being returned
* @param threadName The thread to run
* @param body The data to send to the thread
*/
async spider<T>(params: Thread<T>): Promise<T> {
const { inputMark, outputMark, threadName, body } = params;
const res = await fetch(`/spider/${inputMark}/${threadName}/${outputMark}.json`, {
...this.fetchOptions,
method: 'POST',
body: JSON.stringify(body)
});
return res.json();
}
app(appName: string): UrbitApp {
const appClass = Urbit.apps.get(appName);
if (!appClass) {
throw new Error(`App ${appName} not found`);
}
return new appClass(this);
}
/**
* Utility function to connect to a ship that has its *.arvo.network domain configured.
*
* @param name Name of the ship e.g. zod
* @param code Code to log in
*/
static async onArvoNetwork(ship: string, code: string): Promise<Urbit> {
const url = `https://${ship}.arvo.network`;
return await Urbit.authenticate({ ship, url, code });
}
static extend(appClass: any): void {
Urbit.apps.set(appClass.app, appClass);
}
}
export default Urbit;

45
pkg/npm/http-api/src/types/index.d.ts vendored Normal file
View File

@ -0,0 +1,45 @@
import { Action, Mark, Poke } from '../../../api/index';
export interface PokeInterface extends Poke<Mark, any> {
onSuccess?: () => void;
onError?: () => void;
}
export interface AuthenticationInterface {
ship: string;
url: string;
code: string;
verbose?: boolean;
}
export interface SubscriptionInterface {
err?(error: any): void;
event?(data: any): void;
quit?(data: any): void;
}
export type SubscriptionRequestInterface = SubscriptionInterface & {
app: string;
path: string;
}
export interface headers {
'Content-Type': string;
Cookie?: string;
}
export interface UrbitInterface {
connect(): void;
}
export interface CustomEventHandler {
(data: any, response: string): void;
}
export interface SSEOptions {
headers?: {
Cookie?: string
};
withCredentials?: boolean;
}

View File

@ -0,0 +1,82 @@
import * as http from 'http';
interface HttpResponse {
req: http.ClientRequest;
res: http.IncomingMessage;
data: string;
}
export function request(
url: string,
options: http.ClientRequestArgs,
body?: string
): Promise<HttpResponse> {
return new Promise<HttpResponse>((resolve, reject) => {
const req = http.request(url, options, res => {
let data = "";
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
resolve({ req, res, data });
});
res.on("error", e => {
reject(e);
});
});
if (body) {
req.write(body);
}
req.end();
});
}
export function camelize(str: string) {
return str
.replace(/\s(.)/g, function($1: string) { return $1.toUpperCase(); })
.replace(/\s/g, '')
.replace(/^(.)/, function($1: string) { return $1.toLowerCase(); });
}
export function uncamelize(str: string, separator = '-') {
// Replace all capital letters by separator followed by lowercase one
var str = str.replace(/[A-Z]/g, function (letter: string) {
return separator + letter.toLowerCase();
});
return str.replace(new RegExp('^' + separator), '');
}
/**
* Returns a hex string of given length.
*
* Poached from StackOverflow.
*
* @param len Length of hex string to return.
*/
export function hexString(len: number): string {
const maxlen = 8;
const min = Math.pow(16, Math.min(len, maxlen) - 1);
const max = Math.pow(16, Math.min(len, maxlen)) - 1;
const n = Math.floor(Math.random() * (max - min + 1)) + min;
let r = n.toString(16);
while (r.length < len) {
r = r + hexString(len - maxlen);
}
return r;
}
/**
* Generates a random UID.
*
* Copied from https://github.com/urbit/urbit/blob/137e4428f617c13f28ed31e520eff98d251ed3e9/pkg/interface/src/lib/util.js#L3
*/
export function uid(): string {
let str = '0v';
str += Math.ceil(Math.random() * 8) + '.';
for (let i = 0; i < 5; i++) {
let _str = Math.ceil(Math.random() * 10000000).toString(32);
_str = ('00000' + _str).substr(-5, 5);
str += _str + '.';
}
return str.slice(0, -1);
}

View File

@ -0,0 +1,8 @@
import Urbit from '../src';
describe('blah', () => {
it('works', () => {
const connection = new Urbit('~sampel-palnet', '+code');
expect(connection).toEqual(2);
});
});

View File

@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "./dist/cjs"
},
}

View File

@ -0,0 +1,18 @@
{
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "@types"],
"compilerOptions": {
"outDir": "./dist/esm",
"module": "ES2020",
"noImplicitAny": true,
"target": "ES2020",
"pretty": true,
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"declaration": true,
"sourceMap": true,
"strict": false
// "lib": ["ES2020"],
}
}

View File

@ -0,0 +1,109 @@
const path = require('path');
const webpack = require('webpack');
const shared = {
mode: 'production',
entry: {
app: './src/index.ts'
},
module: {
rules: [
{
test: /\.(j|t)s$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/typescript'],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-optional-chaining',
],
}
},
exclude: /node_modules/
}
]
},
resolve: {
extensions: ['.js', '.ts', '.ts'],
fallback: {
fs: false,
child_process: false,
util: require.resolve("util/"),
buffer: require.resolve('buffer/'),
assert: false,
http: require.resolve('stream-http'),
https: require.resolve('stream-http'),
stream: require.resolve('stream-browserify'),
zlib: require.resolve("browserify-zlib"),
}
},
optimization: {
minimize: false,
usedExports: true
}
};
const serverConfig = {
...shared,
target: 'node',
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'dist'),
library: 'Urbit',
libraryExport: 'default'
},
plugins: [
new webpack.ProvidePlugin({
XMLHttpRequest: ['xmlhttprequest-ssl', 'XMLHttpRequest'],
EventSource: 'eventsource',
fetch: ['node-fetch', 'default'],
}),
],
};
const browserConfig = {
...shared,
target: 'web',
output: {
filename: 'browser.js',
path: path.resolve(__dirname, 'dist'),
library: 'Urbit',
libraryExport: 'default'
},
plugins: [
new webpack.ProvidePlugin({
Buffer: 'buffer',
}),
],
};
const exampleBrowserConfig = {
...shared,
mode: 'development',
entry: {
app: './src/example/browser.js'
},
output: {
filename: 'browser.js',
path: path.resolve(__dirname, 'example'),
}
};
const exampleNodeConfig = {
...shared,
mode: 'development',
target: 'node',
entry: {
app: './src/example/node.js'
},
output: {
filename: 'node.js',
path: path.resolve(__dirname, 'example'),
}
};
module.exports = [ serverConfig, browserConfig, exampleBrowserConfig, exampleNodeConfig ];