Source: recipes.ts

/**
 * @author Michael Hasenstein <hasenstein@yahoo.com>
 * @copyright REFINIO GmbH 2018
 * @license CC-BY-NC-SA-2.5; portions MIT License
 * @version 0.0.1
 */

/**
 * This module defines the core ONE object types. It has the type definitions as well as the
 * recipes.
 *
 * **The module deliberately does not import any other module.** This ensures that there are no
 * cyclic imports (type imports don't count, they don't exist at runtime).
 * @module
 */

/**
 * A meta-type defined in `one.core/[src|lib]/@OneObjectInterfaces.d.ts` in module namespace
 * `"@OneObjectInterfaces"` that can be extended through TypeScript's
 * {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html|*declaration merging*} feature to include ONE object definitions defined through recipes of the application.
 *
 * In order to add your own project's unversioned ONE object types create similar .d.ts
 * file, declare a module namespace `@OneObjectInterfaces` and export an interface declaration for
 * `OneUnversionedObjectInterfaces` just as in the one.core file. If you tell `tsc`
 * (TypeScript server) via your `tsconfig.json` to include both one.core's and your own
 * definition files it will merge the declarations into one.
 *
 * To extend this structure have a look at file `one.core/@OneObjectInterfaces.d.ts` and do what
 * was one there to add ONE object types defined for the one.core tests for your own application
 * types.
 *
 * This interface contains the names (as keys) and TypeScript interface types (as values) of
 * all unversioned objects.
 *
 * **Important:** The string keys for each entry must be exactly the ONE object type name as
 * used in the "`name`" property of the recipe for that type.
 * @global
 * @typedef {object} OneUnversionedObjectInterfaces
 * @property {ONE-object-interface-type} type-name - Use the same string literal as used for
 * the "type" property in your ONE object(s) as key and the TypeScript interface definition
 * name as the value
 */
declare module '@OneObjectInterfaces' {
    export interface OneUnversionedObjectInterfaces extends OneCrdtMetaObjectInterfaces {
        Keys: Keys;
    }
}

/**
 * ONE microdata object properties can store one of these types:
 *
 * - NumberValue
 * - BooleanValue
 * - StringifiableValue
 * - ReferenceToObjValue
 * - ReferenceToIdValue
 * - ReferenceToClobValue
 * - ReferenceToBlobValue
 * - MapValue
 * - BagValue
 * - ArrayValue
 * - SetValue
 * - ObjectValue;
 *
 * Conversion from and to the types is performed upon writing and reading ONE microdata objects:
 *
 * When reading microdata and creating a Javascript object representation of the ONE object
 * the strings will be converted to these types if the respective rule for the item has type
 * information. By default, everything remains a string.
 *
 * @global
 * @typedef {(
 *   StringValue | IntegerValue | NumberValue | BooleanValue | StringifiableValue |
 *   ReferenceToObjValue | ReferenceToIdValue | ReferenceToClobValue | ReferenceToBlobValue |
 *   MapValue | BagValue | ArrayValue | SetValue | ObjectValue
 * )} ValueType
 */
export type ValueType =
    | StringValue
    | IntegerValue
    | NumberValue
    | BooleanValue
    | StringifiableValue
    | ReferenceToObjValue
    | ReferenceToIdValue
    | ReferenceToClobValue
    | ReferenceToBlobValue
    | MapValue
    | BagValue
    | ArrayValue
    | SetValue
    | ObjectValue;

/**
 * This object is part of {@link ValueType} and describes the value-type of a String.
 * @global
 * @typedef {object} StringValue
 * @property {'string'} type
 * @property {RegExp} [regexp]
 */
export interface StringValue {
    type: 'string';
    regexp?: RegExp;
}

/**
 * This object is part of {@link ValueType} and describes the value-type of an Integer.
 * @global
 * @typedef {object} IntegerValue
 * @property {'integer'} type
 * @property {number} [max]
 * @property {number} [min]
 */
export interface IntegerValue {
    type: 'integer';
    max?: number;
    min?: number;
}

/**
 * This object is part of {@link ValueType} and describes the value-type of a Number.
 * @global
 * @typedef {object} NumberValue
 * @property {'number'} type
 * @property {number} [max]
 * @property {number} [min]
 */
export interface NumberValue {
    type: 'number';
    max?: number;
    min?: number;
}

/**
 * This object is part of {@link ValueType} and describes the value-type of a Boolean.
 * @global
 * @typedef {object} BooleanValue
 * @property {'boolean'} type
 */
export interface BooleanValue {
    type: 'boolean';
}

/**
 * This object is part of {@link ValueType} and describes the value-type of a {@link SHA256Hash} of
 * an object.
 * @global
 * @typedef {object} ReferenceToObjValue
 * @property {'referenceToObj'} type
 * @property {Set<OneObjectTypeNames | '*'>} allowedTypes - Includes another recipe as a
 * sub-item. Create microdata span tag for complex data with attributes `itemscope` and
 * `itemtype="//refin.io/{THIS TYPE}"`. The actual values are placed in inner span tags
 * with an itemprop attributeSHA256IdHash. This is a list of acceptable types to include. The
 * itemprop` either is a hash link, or in imploded microdata those objects themselves are
 * included. If the list includes the type string '*' the meaning is "any type".
 */
export interface ReferenceToObjValue {
    type: 'referenceToObj';
    allowedTypes: Set<OneObjectTypeNames | '*'>;
}

/**
 * This object is part of {@link ValueType} and describes the value-type of a {@link
 * SHA256IdHash} of an object.
 * @global
 * @typedef {object} ReferenceToIdValue
 * @property {'referenceToId'} type
 * @property {Set<OneVersionedObjectTypeNames | '*'>} allowedTypes - Same as `referenceToObj`
 * but the hash link points to an ID hash.
 */
export interface ReferenceToIdValue {
    type: 'referenceToId';
    allowedTypes: Set<OneVersionedObjectTypeNames | '*'>;
}

/**
 * This object is part of {@link ValueType} and describes the value-type of a {@link SHA256Hash} of
 * a {@link CLOB}.
 * @global
 * @typedef {object} ReferenceToClobValue
 * @property {'referenceToClob'} type - Same as `referenceToObj` but the hash link points to a
 * CLOB (UTF-8) hash. It also is just a boolean value.
 */
export interface ReferenceToClobValue {
    type: 'referenceToClob';
}

/**
 * This object is part of {@link ValueType} and describes the value-type of a {@link SHA256Hash} of
 * a {@link BLOB}.
 * @global
 * @typedef {object} ReferenceToBlobValue
 * @property {'referenceToBlob'} type - Same as `referenceToObj` but the hash link points to a
 * BLOB hash. It also is just a boolean value.
 */
export interface ReferenceToBlobValue {
    type: 'referenceToBlob';
}

/**
 * This object is part of {@link ValueType} and describes the value-type of a Map Object.
 * @global
 * @typedef {object} MapValue
 * @property {'map'} type
 * @property {PrimitiveValueTypes|ReferenceValueTypes} key
 * @property {ValueType} value
 */
export interface MapValue {
    type: 'map';
    key: PrimitiveValueTypes | ReferenceValueTypes;
    value: ValueType;
}

/**
 * This object is part of {@link ValueType} and describes the value-type of a Bag Object.
 * A "bag" is a data-structure that implements a multiset, which is a set
 * that allows multiple (duplicates). In the microdata the sequence of items is ordered by ONE.
 * @global
 * @typedef {object} BagValue
 * @property {'bag'} type
 * @property {ValueType} item
 */
export interface BagValue {
    type: 'bag';
    item: ValueType;
}

/**
 * This object is part of {@link ValueType} and describes the value-type of an Array Object.
 * @global
 * @typedef {object} ArrayValue
 * @property {'array'} type
 * @property {ValueType} item
 */
export interface ArrayValue {
    type: 'array';
    item: ValueType;
}

/**
 * This object is part of {@link ValueType} and describes the value-type of a Set Object.
 * @global
 * @typedef {object} SetValue
 * @property {'set'} type
 * @property {ValueType} item
 */
export interface SetValue {
    type: 'set';
    item: ValueType;
}

/**
 * This object is part of {@link ValueType} and describes the value-type of an ONE Object.
 * @global
 * @typedef {object} ObjectValue
 * @property {'object'} type
 * @property {RecipeRule[]} rules
 */
export interface ObjectValue {
    type: 'object';
    rules: RecipeRule[];
}

/**
 * @global
 * @typedef {object} StringifiableValue
 * @property {'stringifiable'} type
 */
export interface StringifiableValue {
    type: 'stringifiable';
}

/**
 * @global
 * @typedef {(
 *   StringValue | IntegerValue | NumberValue | BooleanValue | StringifiableValue
 * )} PrimitiveValueTypes
 */
export type PrimitiveValueTypes =
    | StringValue
    | IntegerValue
    | NumberValue
    | BooleanValue
    | StringifiableValue;

/**
 * @global
 * @typedef {(
 *   ReferenceToObjValue | ReferenceToIdValue | ReferenceToClobValue | ReferenceToBlobValue
 * )} PrimitiveValueTypes
 */
export type ReferenceValueTypes =
    | ReferenceToObjValue
    | ReferenceToIdValue
    | ReferenceToClobValue
    | ReferenceToBlobValue;

/**
 * A meta-type defined in `one.core/@OneObjectInterfaces.d.ts` in module namespace
 * `"@OneObjectInterfaces"` that can be extended through TypeScript's
 * {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html|*declaration merging*} feature to include ONE object definitions defined through recipes of the application.
 *
 * In order to add your own project's ID ONE object types create similar .d.ts file,
 * declare a module namespace `@OneObjectInterfaces` and export an interface declaration for
 * `OneIdObjectInterfaces` just as in the one.core file. If you tell `tsc` (TypeScript
 * server) via your `tsconfig.json` to include both one.core's and your own definition files
 * it will merge the declarations into one.
 *
 * This interface contains the names (as keys) and TypeScript interface types (as values) of
 * all ID objects. Unless you need to refer to the ID object types by name it probably is
 * best to use TypeScript's built-in `Pick<T, K>` helper to pick the ID object properties
 * from the full versioned object types. Usually only the latter will have named interfaces.
 * If necessary, in .ts code files you can always refer to ID object interfaces using
 * `OneIdObjectInterfaces[TypeId]`, where `TypeId` is the appropriate string key. For key
 * names one.core follows the rule to take the name of the versioned object type and append, for
 * example, 'Id'.
 *
 * **Note** that ID object interfaces are special and must be used explicitly, they are not
 * part in any of the unions or merged definitions of ONE object types.
 * Except for in rare places ID object types are not to be permitted even when the
 * respective versioned object types are, because the ID object types may miss properties
 * that are not ID properties but still non-optional. ID objects are only used to calculate
 * ID hashes and never written, nor should they be used for processing.
 *
 * That is why the name (key) you choose for the ID object type may be arbitrary, it is not
 * used as type name. Only the values are used, i.e. the ID object declarations.
 *
 * This key/value interface still is necessary, the declarations cannot just be written
 * directly to `OneIdObjectTypes`: Declaration merging of ID object types declared for the
 * application requires this key/value structure.
 * @global
 * @typedef {object} OneIdObjectInterfaces
 * @property {ONE-object-interface-type} type-name - Use the same string literal as used for
 * the "$type$" property in your ONE object(s) as key and the TypeScript interface definition
 * name as the value
 */
declare module '@OneObjectInterfaces' {
    // eslint-disable-next-line no-undef -- OneCrdtIdObjectInterfaces *is* defined
    export interface OneIdObjectInterfaces extends OneCrdtIdObjectInterfaces {
        Access: Pick<Access, '$type$' | 'object'>;
        Chum: Pick<Chum, '$type$' | 'name' | 'instance' | 'person'>;
        Group: Pick<Group, '$type$' | 'name'>;
        IdAccess: Pick<IdAccess, '$type$' | 'id'>;
        Instance: Pick<Instance, '$type$' | 'name' | 'owner'>;
        Module: Pick<Module, '$type$' | 'name'>;
        Person: PersonId;
        Recipe: Pick<Recipe, '$type$' | 'name'>;
        MetaObjectList: Pick<MetaObjectList, 'object' | '$type$'>;
    }
}

/**
 * A meta-type defined in `one.core/@OneObjectInterfaces.d.ts` in module namespace
 * `"@OneObjectInterfaces"` that can be extended through TypeScript's
 * {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html|*declaration merging*} feature to include ONE object definitions defined through recipes of the application.
 *
 * In order to add your own project's versioned ONE object types create similar .d.ts
 * file, declare a module namespace `@OneObjectInterfaces` and export an interface declaration for
 * `OneVersionedObjectInterfaces` just as in the one.core file. If you tell `tsc`
 * (TypeScript server) via your `tsconfig.json` to include both one.core's and your own
 * definition files it will merge the declarations into one.
 *
 * This interface contains the names (as keys) and TypeScript interface types (as values) of
 * all versioned objects.
 *
 * **Important:** The string keys for each entry must be exactly the ONE object type name as
 * used in the "`name`" property of the recipe for that type.
 * @global
 * @typedef {object} OneVersionedObjectInterfaces
 * @property {ONE-object-interface-type} type-name - Use the same string literal as used for
 * the "$type$" property in your ONE object(s) as key and the TypeScript interface definition
 * name as the value
 */
declare module '@OneObjectInterfaces' {
    export interface OneVersionedObjectInterfaces extends OneCrdtObjectInterfaces {
        Access: Access;
        Chum: Chum;
        Group: Group;
        IdAccess: IdAccess;
        Instance: Instance;
        Module: Module;
        Person: Person;
        Recipe: Recipe;
        MetaObjectList: MetaObjectList;
    }
}

/**
 * Defined in one.core's `@OneObjectInterfaces.d.ts` file, this is the result of merging the
 * declarations of {@link OneUnversionedObjectInterfaces}, {@link OneVersionedObjectInterfaces}.
 *
 * This is a key/value map object where the keys are *all* valid ONE object type names, and the
 * values are the interface type (i.e. ONE object) definitions.
 *
 * ID object interface definitions are not included in this map because except for in rare
 * places ID object types are not to be permitted even when the respective versioned object
 * types are, because the ID object types may miss properties that are not ID properties but
 * still non-optional. ID objects are only used to calculate ID hashes and never written,
 * nor should they be used for processing.
 * @global
 * @typedef {object} OneObjectInterfaces
 */
export type OneObjectInterfaces = OneUnversionedObjectInterfaces & OneVersionedObjectInterfaces;

/**
 * This union is obtained by extracting all keys from {@link OneUnversionedObjectInterfaces}.
 * @global
 * @typedef {"Keys" | AllYourUnversionedTypeNames} OneUnversionedObjectTypeNames
 */
export type OneUnversionedObjectTypeNames = keyof OneUnversionedObjectInterfaces;

/**
 * This union is obtained by extracting all values from {@link OneUnversionedObjectInterfaces}.
 * @global
 * @typedef {Keys | AllYourUnversionedTypes} OneUnversionedObjectTypes
 */
export type OneUnversionedObjectTypes =
    OneUnversionedObjectInterfaces[keyof OneUnversionedObjectInterfaces];

/**
 * This union is obtained by extracting all keys from {@link OneVersionedObjectInterfaces}.
 * @global
 * @typedef {
 *     "Access" | "Chum" | "Group" | "Instance" | "Module" | "Person" | "Recipe" |
 *     AllYourVersionedTypeNames
 * } OneVersionedObjectTypeNames
 */
export type OneVersionedObjectTypeNames = keyof OneVersionedObjectInterfaces;

/**
 * This union is obtained by extracting all values from {@link OneVersionedObjectInterfaces}.
 * @global
 * @typedef {
 *     Access | Chum | Group | IdAccess | Instance | Module | Person | Recipe |
 *     AllYourVersionedTypes
 * } OneVersionedObjectTypes
 */
export type OneVersionedObjectTypes =
    OneVersionedObjectInterfaces[keyof OneVersionedObjectInterfaces];

/**
 * This union is obtained by extracting all values from {@link OneIdObjectInterfaces}.
 * @global
 * @typedef {
 *   AccessId | ChumId | GroupId | InstanceId | ModuleId | PersonId | RecipeId |
 *   AllYourIdObjectTypes
 * } OneIdObjectTypes
 */
export type OneIdObjectTypes = OneIdObjectInterfaces[keyof OneIdObjectInterfaces];

/**
 * The union of all "`name`" property values of all {@link Recipe}s used by one.core and the
 * application. The type is created as union of {@link OneUnversionedObjectTypeNames} and
 * {@link OneVersionedObjectTypeNames}.
 * @global
 * @typedef {OneUnversionedObjectTypeNames|OneVersionedObjectTypeNames} OneObjectTypeNames
 */
export type OneObjectTypeNames = OneUnversionedObjectTypeNames | OneVersionedObjectTypeNames;

/**
 * The union of all interfaces of ONE object types used by one.core and the application. The
 * type is created as union of {@link OneUnversionedObjectTypes} and
 * {@link OneVersionedObjectTypes}.
 * @global
 * @typedef {OneUnversionedObjectTypes | OneVersionedObjectTypes} OneObjectTypes
 */
export type OneObjectTypes = OneUnversionedObjectTypes | OneVersionedObjectTypes;

/**
 * The ID object of a Person with only "email" as the sole ID property of {@link Person}
 * objects.
 * @typedef {object} PersonId
 * @property {'Person'} $type$
 * @property {string} email
 */
export type PersonId = Pick<Person, '$type$' | 'email'>;

/**
 * This is a *virtual type* representing BLOB files. It is used as a generic parameter type
 * for {@link SHA256Hash} and {@link SHA256IdHash}.
 * @global
 * @typedef {object} BLOB
 */
export interface BLOB {
    _: 'BLOB';
}

/**
 * This is a *virtual type* representing CLOB (UTF-8 text) files. It is used as a generic
 * parameter type for {@link SHA256Hash} and {@link SHA256IdHash}.
 * @global
 * @typedef {object} CLOB
 */
export interface CLOB {
    _: 'CLOB';
}

/**
 * The union of all "`name`" property values of all {@link Recipe}s used by one.core and the
 * application plus 'BLOB' for binary objects and 'CLOB' for UTF-8 objects.
 * @global
 * @typedef {OneObjectTypeNames|BLOB|CLOB} HashTypes
 */
export type HashTypes = OneObjectTypes | BLOB | CLOB;

/**
 * There are two Access objects that are identical in structure and purpose except for in name:
 * 'Access' points to immutable graphs.
 *
 * ### Example
 *
 * Javascript representation:
 *
 * ```javascript
 * const obj = {
 *     $type$: 'Access',
 *     object: hash,
 *     person: [personIdHash]
 * };
 * ```
 *
 * HTML microdata representation:
 *
 * ```html
 * <div itemscope itemtype="//refin.io/Access">
 *   <a itemprop="object" href="${hash}">${hash}</a>
 *   <a itemprop="person" href="${personIdHash}">${personIdHash}</a>
 * </span>
 * ```
 * @global
 * @typedef {object} Access
 * @property {'Access'} type Fixed string 'Access'
 * @property {SHA256Hash} object A versioned or unversioned object link to a concrete
 * versioned or unversioned object that the access rights are being set for.
 * @property {SHA256IdHash[]} [person] An array of ID references to Person objects.
 * @property {SHA256IdHash[]} [group] An array of ID references to Group objects. *Only
 * the latest version* of a group is granted access!
 */
export interface Access {
    $type$: 'Access';
    object: SHA256Hash;
    person: Array<SHA256IdHash<Person>>;
    group: Array<SHA256IdHash<Group>>;
}

/**
 * Same as {@link Access|Access object} but instead of a reference to a concrete object it
 * has a reference to an ID object of a versioned object and grants access to all current *and
 * future* versions of the object.
 * @global
 * @typedef {object} IdAccess
 * @property {'IdAccess'} $type$ Fixed string 'IdAccess' (which is mutable, since the ID
 * reference also points to any future versions of the object) depending on if the directed
 * graph of ONE objects it points to an ID object.
 * @property {SHA256IdHash} id A reference to all versions of an ID object that the access
 * rights are being set for.
 * @property {SHA256IdHash[]} [person] An array of ID references to Person objects.
 * @property {SHA256IdHash[]} [group] An array of ID references to Group objects. *Only
 * the latest version* of a group is granted access!
 */
export interface IdAccess {
    $type$: 'IdAccess';
    id: SHA256IdHash;
    person: Array<SHA256IdHash<Person>>;
    group: Array<SHA256IdHash<Group>>;
}

/**
 * This type is included in {@link Chum} objects as well as used internally.
 * The numbers are without network overhead. The number of sent requests always includes the
 * connect request, so after communication finished both parties will have sent one more request
 * than they themselves received, since the connect request is to the communication server!
 * @global
 * @typedef {object} WebsocketStatistics
 * @property {number} requestsSentTotal Total number of requests made thus far
 * @property {number} requestsReceivedTotal Total number of requests received thus far
 * @property {number} requestsReceivedInvalid Total number of requests received that had to be
 * ignored because they were either unparsable (invalid JSON) or otherwise unusable
 */
export interface WebsocketStatistics {
    requestsSentTotal: number;
    requestsReceivedTotal: number;
    requestsReceivedInvalid: number;
}

/**
 * A Chum object defines a filter for exchanging data with other instances.
 * Initially it only filters by persons.
 *
 * ### Example
 *
 * ```html
 * <div itemscope itemtype="//refin.io/Chum">
 *   <span itemprop="name">ThisChumsName</span>
 *   <span itemprop="instance">${name1}</span>
 *   <span itemprop="instance">${name2}</span>
 *   <span itemprop="person">${hash1}</span>
 *   <span itemprop="person">${hash2}</span>
 *   <span itemprop="highestRemoteTimestamp">${timestamp1}</span>
 *   ...
 * </span>
 * ```
 * @global
 * @typedef {object} Chum
 * @property {'Chum'} $type$ - Fixed string 'Chum'
 * @property {string} name
 * @property {string[]} instance - A pair of instance names
 * @property {SHA256IdHash[]} person - A pair of Person ID hashes. The first one is the one the
 * local instance (owner of the Chum object) used for authentication to the remote instance,
 * the second one is the one the remote instance used to do the same for the local instance.
 * @property {number} highestRemoteTimestamp - A numeric timestamp specifying a date-time on
 * the remote system. We can be sure to have all objects accessible to us created or made
 * accessible to us before that time. NOTE: If older objects are made accessible it
 * will be through an object created at a later time. Since this cutoff date-time specifies
 * the root node of an accessible directed graph, which by necessity (hash links!) has to be
 * created after all others, we get older objects linked from such a root node in any case.
 * This timestamp is only used to filter root nodes.
 * @property {Array<SHA256Hash>} AtoBObjects - Record of all transfers from A to B
 * @property {Array<SHA256IdHash>} AtoBIdObjects - Record of all ID transfers from A to B
 * @property {Array<SHA256Hash>} AtoBBlob - Record of all BLOB transfers from A to B
 * @property {Array<SHA256Hash>} AtoBClob - Record of all CLOB transfers from A to B
 * @property {Array<SHA256Hash>} BtoAObjects - Record of all transfers from B to A
 * @property {Array<SHA256IdHash>} BtoAIdObjects - Record of all ID transfers from B to A
 * @property {Array<SHA256Hash>} BtoABlob - Record of all BLOB transfers from B to A
 * @property {Array<SHA256Hash>} BtoAClob - Record of all CLOB transfers from B to A
 * @property {number} BtoAExists - How many hashes we already had and did not need to import
 * @property {Error[]} errors - Record of exceptions encountered while running
 * @property {Set<SHA256Hash>} [unacknowledged] - List of hashes of files sent for which we did
 * not receive an acknowledgement that it was successfully stored by the importer.
 * **NOTE:** These hashes don't cause any reverse map entries to be created - this field is
 * somewhat optional and "FYI". The main fields are the other transfers-recording fields.
 * @property {WebsocketStatistics} [statistics] - Data about the exchange such as bytes
 * sent/received
 */
export interface Chum {
    $type$: 'Chum';
    name: string;
    instance: [string, string];
    person: [SHA256IdHash<Person>, undefined | SHA256IdHash<Person>];
    highestRemoteTimestamp: number;
    AtoBObjects: SHA256Hash[];
    AtoBIdObjects: SHA256IdHash[];
    AtoBBlob: Array<SHA256Hash<BLOB>>;
    AtoBClob: Array<SHA256Hash<CLOB>>;
    BtoAObjects: SHA256Hash[];
    BtoAIdObjects: SHA256IdHash[];
    BtoABlob: Array<SHA256Hash<BLOB>>;
    BtoAClob: Array<SHA256Hash<CLOB>>;
    BtoAExists: number;
    errors: Error[];
    unacknowledged?: Set<SHA256Hash<HashTypes> | SHA256IdHash>;
    statistics?: WebsocketStatistics;
}

/**
 * {@link Access|Access} objects link to `Group` objects to grant access rights.
 *
 * ### Example
 *
 * Javascript representation:
 *
 * ```javascript
 * const obj = {
 *     $type$: 'Group',
 *     name: name,
 *     person: [Person1-idHash, Person2-idHash]
 * };
 * ```
 *
 * HTML microdata representation:
 *
 * ```html
 * <span itemprop="item" itemscope itemtype="//refin.io/Group">
 *   <span itemprop="name">${name}</span>
 *   <span itemprop="person">${Person1-ID-hash}</span>
 *   <span itemprop="person">${Person2-ID-hash}</span>
 * </span>
 * ```
 * @global
 * @typedef {object} Group
 * @property {'Group'} $type$ - Fixed string 'Group'
 * @property {string} name
 * @property {SHA256IdHash[]} person
 *
 */
export interface Group {
    $type$: 'Group';
    name: string;
    person: Array<SHA256IdHash<Person>>;
}

/**
 * An Instance object describes one instance of the app. Multiple instances can co-exist on
 * one, and even in the same disk space. Both instances would share the same directory and
 * much of their respective data.
 *
 * The encryption key pair is generated by TweetNaCl.js and is stored in two Base 64 encoded
 * strings:
 *  - Base 64 character set: {@link https://tools.ietf.org/html/rfc4648#section-4}
 *  - Base 64 length: {@link https://stackoverflow.com/q/13378815/544779}
 *  - TweetNaCl length is n=32 bytes
 *  - Base 64 encoded length: Math.ceil(4 * n/3) => 43, plus trailing "=" padding character
 *  for a total length of 44 characters.
 *
 * ### Example
 *
 * Javascript representation:
 *
 * ```javascript
 * const obj = {
 *     $type$: 'Instance',
 *     name: 'InstanceName',
 *     owner: personIdHash,
 *     publicKey: blobHash,
 *     secretKey: blobHash,
 *     module: [moduleHash]
 * };
 * ```
 *
 * HTML microdata representation:
 *
 * ```html
 * <div itemscope itemtype="//refin.io/Instance">
 *   <div itemscope="name">InstanceName</span>
 *   <span itemprop="owner">${personIdHash}</span>
 *   <span itemprop="publicKey">${publicKey}</span>
 *   <a itemprop="module" href="${moduleHash}">${module1-hash}</a>
 * </span>
 * ```
 * @global
 * @typedef {object} Instance
 * @property {'Instance'} $type$
 * @property {string} name - The name of the instance chosen by the user
 * @property {SHA256IdHash} owner - A reference to a Person ID object
 * @property {SHA256Hash[]} recipe - A list of ONE object recipes
 * @property {SHA256Hash[]} module - A list of code modules
 * @property {Map<OneObjectTypeNames,null|Set<string>>} enabledReverseMapTypes - A Map
 * object with ONE object type names as keys and an array of "itemprop" properties from the
 * recipe for that particular ONE object type for which  reverse maps should be written. For
 * all other types and/or properties not mentioned in this Map reverse map entries are not
 * created.
 * @property {Map<OneObjectTypeNamesForIdObjects,null|Set<string>>}
 *     enabledReverseMapTypesForIdObjects - A Map object with ONE object type names as keys and an
 *     array of "itemprop" properties from the recipe for that particular ONE object type for which
 *     ID object reverse maps should be written. For all other types and/or properties not
 *     mentioned in this Map reverse map entries are not created.
 */
export interface Instance {
    $type$: 'Instance';
    name: string;
    owner: SHA256IdHash<Person>;
    recipe: Array<SHA256Hash<Recipe>>;
    module: Array<SHA256Hash<Module>>;
    enabledReverseMapTypes: Map<OneObjectTypeNames, Set<string>>;
    enabledReverseMapTypesForIdObjects: Map<OneVersionedObjectTypeNames, Set<string>>;
}

/**
 * This object stores the *public* encryption and sign keys for a given {@link Instance} or
 * {@link Person} ID. The secret keys are stored in a special non-shared private storage area.
 * @global
 * @typedef {object} Keys
 * @property {'Keys'} $type$
 * @property {SHA256IdHash} owner
 * @property {HexString} publicKey
 * @property {HexString} publicSignKey
 */
export interface Keys {
    $type$: 'Keys';
    owner: SHA256IdHash<Instance | Person>;
    publicKey: HexString;
    publicSignKey: HexString;
}

/**
 * ### Example
 *
 * Javascript representation:
 *
 * ```javascript
 * const obj = {
 *     $type$: 'Module',
 *     name: '@module/imap-mailbox-handler',
 *     version: '1.0',
 *     requires: [
 *         '@module/imap-promisifier.js',
 *         '@one/message-bus.js',
 *         '@one/storage.js'
 *     ],
 *     code: clobHash
 * };
 * ```
 *
 * HTML microdata representation:
 *
 * ```html
 * <div itemscope itemtype="//refin.io/Module">
 *   <span itemprop="name">@module/imap-mailbox-handler</span>
 *   <span itemprop="version">1.0</span>
 *   <span itemprop="requires">@module/imap-promisifier.js</span>
 *   <span itemprop="requires">lib/message-bus.js</span>
 *   <span itemprop="requires">lib/storage.js</span>
 *   <span itemprop="code">${CLOB-hash}</span>
 * </span>
 * ```
 * @global
 * @typedef {object} Module
 * @property {'Module'} $type$
 * @property {string} name The name that will be used by other modules to request this module
 * @property {string[]} [provides]
 * @property {string[]} [requires] The modules called from this module, extracted from
 * require() or import calls from its code
 * @property {string} [version] An optional version string
 * @property {SHA256Hash} [code] Link to a UTF-8 CLOB with the Javascript code for this module
 */
export interface Module {
    $type$: 'Module';
    name: string;
    provides?: string[];
    requires?: string[];
    version?: string;
    code?: SHA256Hash<CLOB>;
}

/**
 * ### Example
 *
 * Javascript representation:
 *
 * ```javascript
 * const obj = {
 *     $type$: 'Person',
 *     email: 'members@gmx.net',
 *     name: 'Foo Test'
 * };
 * ```
 *
 * HTML microdata representation:
 *
 * ```html
 * <div itemscope itemtype="//refin.io/Person">
 *   <span itemprop="email">members@gmx.net</span>
 * </span>
 * ```
 * @global
 * @typedef {object} Person
 * @property {'Person'} $type$
 * @property {string} email - A person-identifying unique email address
 * @property {string} [name]
 */
export interface Person {
    $type$: 'Person';
    email: string;
    name?: string;
}

/**
 * The array of rules determines the properties of the ONE object and their order (in the
 * written microdata representation).
 *
 * ### Example
 *
 * Javascript representation:
 *
 * ```javascript
 * const obj = {
 *     $type$: 'Recipe',
 *     name: ONE-object-type-name,
 *     version: 1,
 *     rule: [
 *         {
 *             itemprop: property1-name
 *             isId: true
 *             valueType: 'number'
 *         },
 *         {
 *             itemprop: property2-name,
 *             regexp: /^[\w]*+$/
 *         },
 *         {
 *             itemprop: property3-name
 *             valueType: 'map'
 *         },
 *         {
 *             itemprop: property4-name
 *             referenceToObj: new Set(['*'])
 *         }
 *     ]
 * };
 * ```
 *
 * HTML microdata representation:
 *
 * ```html
 * <div itemscope itemtype="//refin.io/Recipe">
 *   <span itemprop="name">${ONE-object-type-name}</span>
 *   <span itemprop="rule">
 *     <span itemprop="itemprop">${property1-name}</span>
 *     <span itemprop="isId">true</span>
 *     <span itemprop="valueType">number</span>
 *   </span>
 *   <span itemprop="rule">
 *     <span itemprop="itemprop">${property2-name}</span>
 *   </span>
 *   <span itemprop="rule">
 *     <span itemprop="itemprop">${property3-name}</span>
 *     <span itemprop="valueType">Map</span>
 *   </span>
 *   <span itemprop="rule">
 *     <span itemprop="itemprop">${property4-name}</span>
 *     <span itemprop="referenceToObj">['*']</span>
 *   </span>
 * </span>
 * ```
 * @global
 * @typedef {object} Recipe
 * @property {'Recipe'} $type$
 * easily load a specific version of this versioned Recipe object.
 * @property {string} name The type-string constant identifying this ONE object type
 * @property {RecipeRule[]} rule Array of rules
 */
export interface Recipe {
    $type$: 'Recipe';
    name: OneObjectTypeNames;
    rule: RecipeRule[];
    crdtConfig?: Map<string, CRDTImplementationNames>;
}

/**
 * Important note (from @sebastian). I decided to keep exclude 'list' even if we don't have
 * the list field anymore in {@link RecipeRule}. It will remove 'array','bag' or 'set' field in
 * the {@link RecipeRule.itemtype} and keep only the item type.
 *
 *
 * The recipe-rule property "inheritFrom" can be either a string (common inheritance) or an
 * object (inheritance with additional options). This is the type of the object in the latter case.
 * @global
 * @typedef {object} RuleInheritanceWithOptions
 * @property {string} rule - The rule to inherit from as "." separated path Recipe.itemprop
 * @property {'CollectionItemType' | 'MapItemType'} extract - Extracts the item type inside a
 * collection or map.
 */
export interface RuleInheritanceWithOptions {
    rule: string;
    extract: 'CollectionItemType' | 'MapItemType';
}

/**
 * This object is part of {@link Recipe} but has an extra type because the rule objects  appear
 * on their own in parts of the code, where those rules are given to functions to process parts
 * of ONE objects.
 *
 * NOTE: If this type is updated don't forget to also update function `ensureRecipeRule()` in
 * <PROJECT_HOME>/src/util/recipe-checks.js
 * @global
 * @typedef {object} RecipeRule
 * @property {string} itemprop - Create a microdata span tag with attribute "itemprop".
 * Equivalent to the "key" in a Javascript object literal. The equivalent of the "value"
 * is, again similar to a JS object, either another object (recursion: see "referenceToObj"
 * below) or an actual value (string).
 * @property {boolean} [isId=false] - Whether this is an ID field for this type. If not present
 * "false" is assumed.
 * @property {ValueType} [type] - The Javascript type of this property: After
 * reading microdata everything is a "string". Setting this to {@link ValueType}
 * lets the Microdata => (JS) Object converter know that it has to convert the strings to these
 * types. The default is "string", because everything already is a string anyway when stored as
 * HTML microdata.
 * @property {RecipeRule[]} [object] - An array of {@link RecipeRule} rules for a nested data
 * property. It works just like including a full ONE object on a `referenceToObj` enabled
 * property but without using an actual ONE object, only the data.
 * @property {(string|RuleInheritanceWithOptions)} [inheritFrom] - Inherit properties from the
 * given rule. The current rule inherits all properties of the linked rule and can override them
 * or add to them with its own properties. In the simple case it is a string of the for
 * Recipe[.itemprop].itemprop pointing to an itemprop in any known recipe. Recipe resolution
 * happens at runtime, when "inheritFrom" is encountered for the first time in a recipe rule.
 * The second case with options requires an object of the form {@link RuleInheritanceWithOptions}.
 *
 * ### Example
 *
 * Javascript representation:
 *
 * ```javascript
 * const obj = {
 *     itemprop: propertyValue,
 *     isId: true,
 *     list: 'array',
 *     valueType: {number: {max: 1000}}
 * };
 * ```
 *
 * HTML microdata representation:
 *
 * ```html
 * // Part of a "Recipe" object: Array of RecipeRule in property "rule"
 * ...
 *   <span itemprop="rule">
 *     <span itemprop="itemprop">${propertyValue}</span>
 *     <span itemprop="isId">true</span>
 *     <span itemprop="valueType">"{\"number\":{\"max\":1000}}"</span>
 *   </span>
 * ...
 * ```
 */
export interface RecipeRule {
    itemprop: string;
    itemtype?: ValueType;
    optional?: boolean;
    isId?: boolean;
    inheritFrom?: string | RuleInheritanceWithOptions;
}

/**
 * This type stores a collection of metadata objects for a specific object (version).
 *
 * Example:
 * If you have an object with hash AB34... and you want to get meta-object (e.g. creation
 * time or crdt metadata objects) you can get the corresponding meta-object list that holds
 * all metadata objects pointing to object AB34...
 *
 * ```
 * const metaObjectList = await getObjectByIdObj({
 *     $type$: 'MetaObjectList',
 *     object: 'AB34...'
 * });
 *
 * // The meta-objects are available via
 * const objs = metaObjectList.obj.metaobject;
 * ```
 *
 * We should add convenience functions to get meta-objects of a specific type for a specific
 * object.
 */
export interface MetaObjectList {
    $type$: 'MetaObjectList';
    object: SHA256Hash<OneObjectTypes>;
    metaobject: Array<SHA256Hash<OneUnversionedObjectTypes>>;
}

// ######## CRDT Types ########

/**
 * Interface that lists all crdt capable versioned one objects.
 *
 * This interface is very similar to OneVersionedObjectInterfaces. The only difference is,
 * that this interface collects the one types that use CRDTs for synchronization. All types
 * that appear here are automatically inherited by OneVersionedObjectInterfaces.
 */
declare module '@OneObjectInterfaces' {
    export interface OneCrdtObjectInterfaces {
        CrdtDummy: CrdtDummy;
        CrdtDummy2: CrdtDummy2;
    }
}

/**
 * The id types for the OneCrdtObjectInterfaces.
 */
declare module '@OneObjectInterfaces' {
    export interface OneCrdtIdObjectInterfaces {
        CrdtDummy: Pick<CrdtDummy, 'id' | '$type$'>;
        CrdtDummy2: Pick<CrdtDummy2, 'id' | '$type$'>;
    }
}

/**
 * The types of all crdt capable versioned objects.
 */
export type OneCrdtObjectTypes = OneCrdtObjectInterfaces[keyof OneCrdtObjectInterfaces];

/**
 * The type names of all crdt capable versioned objects.
 */
export type OneCrdtObjectTypeNames = keyof OneCrdtObjectInterfaces;

/**
 * This type only exists, so that the OneCrdtObjectInterfaces is initially not empty.
 *
 * The empty interface poses sometimes a problem. If you have a useful crdt capable object
 * in one.core, you can remove this. We don't register the recipe of this object, so that
 * nobody attempts to use it.
 */
export interface CrdtDummy {
    $type$: 'CrdtDummy';
    id: string;
}

export interface CrdtDummy2 {
    $type$: 'CrdtDummy2';
    id: string;
}

// ######## CRDT Metadata Types ########

/**
 * The typescript interface for crdt metadata objects.
 */
export interface CRDTMetaData<T extends OneCrdtObjectTypes> {
    $type$: `${T['$type$']}CrdtMeta`;
    data: SHA256Hash<T>; // Pointer to the data
    crdt: Record<string, any>; // The crdt metadata.
    source?: 'local' | 'remote'; // A source string that lets us distinguish between remote and
    // local merges. It is mostly for debugging purposes.
}

/**
 * This interface defines the metadata types for the crdt types.
 *
 * These types are the types that store the crdt metadata for the object that are specified
 * in OneCrdtObjectInterfaces. So there is a 1:1 relationship between them.
 */
declare module '@OneObjectInterfaces' {
    export type OneCrdtMetaObjectInterfaces = {
        [k in keyof OneCrdtObjectInterfaces as `${k}CrdtMeta`]: CRDTMetaData<
            OneCrdtObjectInterfaces[k]
        >;
    };

    export type OneCrdtToMetaObjectInterfaces = {
        [k in keyof OneCrdtObjectInterfaces]: CRDTMetaData<OneCrdtObjectInterfaces[k]>;
    };
}

/**
 * The types of all crdt metadata objects.
 */
export type OneCrdtMetaObjectTypes = OneCrdtMetaObjectInterfaces[keyof OneCrdtMetaObjectInterfaces];

/**
 * The type names of all crdt metadata objects.
 */
export type OneCrdtMetaObjectTypeNames = keyof OneCrdtMetaObjectInterfaces;

import type {
    OneCrdtMetaObjectInterfaces,
    OneCrdtObjectInterfaces,
    OneIdObjectInterfaces,
    OneUnversionedObjectInterfaces,
    OneVersionedObjectInterfaces
} from '@OneObjectInterfaces';
import type {CRDTImplementationNames} from './crdt/CRDTRegistry';
import type {HexString} from './util/arraybuffer-to-and-from-hex-string';
import type {SHA256Hash, SHA256IdHash} from './util/type-checks';

/**
 * The allowed values for {@link RecipeRule}'s property `list` for list properties (i.e. more
 * than one value for the given `itemprop`). Leave the property undefined if the value is not a
 * list but a single value.
 *
 * Background/reason: The same elements but a different order will produce a different SHA-256
 * hash of the content! Here is when you tell one that this is what is intended or not.
 *
 * - `ORDERED_BY.ONE`: When converting an array of values to microdata the result will be
 * alphabetically ordered to impose a reproducible order, in order to guarantee that the same
 * unordered "bag" of data produces the exact same microdata and the exact same SHA-256 hash.
 * This means that an array of the exact same elements in different order is going to lead to a
 * microdata representation that is always the same. IT also means that when reading the ONE
 * object back from storage the order will be whatever the alphabetical sorting of the microdata
 * elements produced, which may be different from the original order when the data was saved.
 *
 * - `ORDERED_BY.APP`: When converting the array of values the order is left as-is. This means
 * that an array of the exact same elements in different order is going to lead to a microdata
 * representation that is different, which leads to a different SHA-256 hash.
 * @static
 * @type {object}
 * @property {'orderedByONE'} ONE
 * @property {'orderedByApp'} APP
 */
export const ORDERED_BY = {
    ONE: 'orderedByONE',
    APP: 'orderedByApp'
} as const;

/**
 * Declare the private ONE storage for RECIPES. They describe how objects are built and are used
 * to create the Javascript, JSON and the microdata representations of ONE objects.
 *
 * Note: The examples for each ONE object type recipe are **with newlines and indentation** for
 * readability. The actual output will not have spaces or newlines!
 * @static
 * @type {Recipe[]}
 */
export const CORE_RECIPES: readonly Recipe[] = [
    {
        $type$: 'Recipe',
        name: 'Access',
        rule: [
            {
                itemprop: 'object',
                itemtype: {type: 'referenceToObj', allowedTypes: new Set(['*'])},
                isId: true
            },
            {
                // Reference to ID hashes of persons
                itemprop: 'person',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'referenceToId',
                        allowedTypes: new Set(['Person'])
                    }
                }
            },
            {
                // References to ID hashes of groups
                itemprop: 'group',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'referenceToId',
                        allowedTypes: new Set(['Group'])
                    }
                }
            }
        ]
    },

    {
        $type$: 'Recipe',
        name: 'IdAccess',
        rule: [
            {
                itemprop: 'id',
                itemtype: {
                    type: 'referenceToId',
                    allowedTypes: new Set(['*'])
                },
                isId: true
            },
            {
                itemprop: 'person',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'referenceToId',
                        allowedTypes: new Set(['Person'])
                    }
                }
            },
            {
                itemprop: 'group',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'referenceToId',
                        allowedTypes: new Set(['Group'])
                    }
                }
            }
        ]
    },

    {
        $type$: 'Recipe',
        name: 'Chum',
        rule: [
            {
                // Every chum is identified by a name.
                // <span itemprop="name">some-name</span>
                itemprop: 'name',
                isId: true
            },
            {
                // Names of the two instances that can exchange data via this chum.
                // <span itemprop="instance">instance name</span>
                itemprop: 'instance',
                isId: true,
                itemtype: {
                    type: 'array',
                    item: {
                        type: 'string'
                    }
                } // always 2
            },
            {
                // Person hash of the two persons who can exchange data via this Chum.
                // Also used to authenticate the remote user.
                // TODO Make it a referenceToIdObj
                // <span itemprop="person">personHash</span>
                itemprop: 'person',
                isId: true,
                itemtype: {
                    type: 'array',
                    item: {
                        type: 'string',
                        regexp: /^(undefined|[0-9a-f]{64})$/
                    }
                } // Our type "SHA256Hash"
            },
            {
                // Milliseconds-since 1/1/1970 timestamp
                // <span itemprop="highestRemoteTimestamp">timestamp</span>
                itemprop: 'highestRemoteTimestamp',
                itemtype: {
                    type: 'integer'
                }
            },
            // The transfer lists cannot be saved as "array of two" (pairs) because they already
            // are arrays (list:'orderedByONE'), and saving them as JSON-encoded strings is not
            // possible since that would prevent the reverse-map updates for the reference links
            // as well as any other hash-link based functionality.
            {
                itemprop: 'AtoBObjects',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'referenceToObj',
                        allowedTypes: new Set(['*'])
                    }
                }
            },
            {
                itemprop: 'AtoBIdObjects',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'referenceToId',
                        allowedTypes: new Set(['*'])
                    }
                }
            },
            {
                itemprop: 'AtoBBlob',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'referenceToBlob'
                    }
                }
            },
            {
                itemprop: 'AtoBClob',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'referenceToClob'
                    }
                }
            },
            {
                itemprop: 'BtoAObjects',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'referenceToObj',
                        allowedTypes: new Set(['*'])
                    }
                }
            },
            {
                itemprop: 'BtoAIdObjects',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'referenceToId',
                        allowedTypes: new Set(['*'])
                    }
                }
            },
            {
                itemprop: 'BtoABlob',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'referenceToBlob'
                    }
                }
            },
            {
                itemprop: 'BtoAClob',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'referenceToClob'
                    }
                }
            },
            {
                itemprop: 'BtoAExists',
                itemtype: {
                    type: 'integer'
                }
            },
            {
                // For logging purposes: List of hashes of files sent for which we did not
                // receive an acknowledgement
                // This is saved as string and not as reference because it is not meant to be a
                // graph-creating reference.
                // <span itemprop="unacknowledged">["hash1","hash2"]</span>
                itemprop: 'unacknowledged',
                optional: true,
                itemtype: {
                    type: 'set',
                    item: {
                        type: 'string'
                    }
                }
            },
            {
                // For logging purposes: statistical data
                // <span itemprop="statistics">{sent:...,received:...}</span>
                itemprop: 'statistics',
                itemtype: {type: 'stringifiable'},
                optional: true
            },
            {
                // For logging purposes: List of errors raised by the importer or exporter
                // <span itemprop="errors">["..."]</span>
                itemprop: 'errors',
                itemtype: {type: 'stringifiable'},
                optional: true
            }
        ]
    },

    {
        $type$: 'Recipe',
        name: 'Group',
        rule: [
            {
                // <span itemprop="name">${name}</a>
                itemprop: 'name',
                isId: true
            },
            {
                itemprop: 'person',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'referenceToId',
                        allowedTypes: new Set(['Person'])
                    }
                }
            }
        ]
    },

    {
        $type$: 'Recipe',
        name: 'Instance',
        rule: [
            {
                // Every instance is identified by name, which should be unique per user.
                itemprop: 'name',
                isId: true
            },
            {
                // The owner of the app represented by its person ID.
                itemprop: 'owner',
                itemtype: {
                    type: 'referenceToId',
                    allowedTypes: new Set(['Person'])
                },
                isId: true
            },
            {
                // Reference to Recipe objects.
                itemprop: 'recipe',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'referenceToObj',
                        allowedTypes: new Set(['Recipe'])
                    }
                }
            },
            {
                // Reference to Module objects.
                itemprop: 'module',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'referenceToObj',
                        allowedTypes: new Set(['Module'])
                    }
                }
            },
            {
                itemprop: 'enabledReverseMapTypes',
                itemtype: {
                    type: 'map',
                    key: {
                        type: 'string'
                    },
                    value: {
                        type: 'set',
                        item: {
                            type: 'string'
                        }
                    }
                }
            },
            {
                itemprop: 'enabledReverseMapTypesForIdObjects',
                itemtype: {
                    type: 'map',
                    key: {
                        type: 'string'
                    },
                    value: {
                        type: 'set',
                        item: {
                            type: 'string'
                        }
                    }
                }
            }
        ]
    },

    {
        $type$: 'Recipe',
        name: 'Keys',
        rule: [
            {
                itemprop: 'owner',
                itemtype: {
                    type: 'referenceToId',
                    allowedTypes: new Set(['Instance', 'Person'])
                }
            },
            {
                // ENCRYPTION KEY: Hex encoded public key (64 characters)
                itemprop: 'publicKey',
                // TweetNaCl length is n=32 bytes
                itemtype: {
                    type: 'string',
                    regexp: /^[A-Za-z0-9+/]{64}$/
                }
            },
            {
                // SIGN KEY: Hex encoded public key (64 characters)
                itemprop: 'publicSignKey',
                // TweetNaCl length is n=32 bytes
                itemtype: {
                    type: 'string',
                    regexp: /^[A-Za-z0-9+/]{64}$/
                }
            }
        ]
    },

    {
        $type$: 'Recipe',
        name: 'Module',
        rule: [
            {
                // The name should include namespace-components. The name is what other scripts use
                // to call (import) this module's code.
                // <span itemprop="name">some-name</span>
                itemprop: 'name',
                isId: true
            },
            {
                // <span itemprop="version">0.1</span>
                itemprop: 'version',
                optional: true
            },
            {
                // <span itemprop="provides">types provided by the Module</span>
                // list of type recipes (from this file or future alternatives)
                itemprop: 'provides',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'string'
                    }
                },
                optional: true
            },
            {
                // <span itemprop="requires">types required by the Module</span>
                // list of type recipes (from this file or future alternatives)
                itemprop: 'requires',
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'string'
                    }
                },
                optional: true
            },
            {
                // Pointer to a Javascript file stored as CLOB and named for its crypto-hash
                // <span itemprop="code">${hash}</span>
                itemprop: 'code',
                itemtype: {
                    type: 'referenceToClob'
                }
            }
        ]
    },
    {
        $type$: 'Recipe',
        name: 'Person',
        rule: [
            {
                itemprop: 'email',
                isId: true
            },
            {
                itemprop: 'name',
                optional: true
            }
        ]
    },

    {
        $type$: 'Recipe',
        name: 'Recipe', // This recipe describes _itself_ :-)
        rule: [
            {
                // <span itemprop="name">${name}</span>
                itemprop: 'name',
                isId: true
            },
            {
                itemprop: 'crdtConfig',
                optional: true,
                itemtype: {
                    type: 'map',
                    key: {type: 'string'},
                    value: {type: 'string'}
                }
            },
            {
                itemprop: 'rule',
                itemtype: {
                    type: 'array',
                    item: {
                        type: 'object',
                        rules: [
                            {
                                // <span itemprop="itemprop">${itemprop}</span>
                                itemprop: 'itemprop'
                            },
                            {
                                itemprop: 'optional',
                                itemtype: {
                                    type: 'boolean'
                                },
                                optional: true
                            },
                            {
                                // <span itemprop="isId">${true}</span>
                                itemprop: 'isId',
                                itemtype: {
                                    type: 'boolean'
                                },
                                optional: true
                            },
                            {
                                itemprop: 'itemtype',
                                itemtype: {type: 'stringifiable'},
                                optional: true
                            },
                            {
                                itemprop: 'inheritFrom',
                                itemtype: {
                                    type: 'stringifiable'
                                },
                                optional: true
                            }
                        ]
                    }
                }
            }
        ]
    },
    {
        $type$: 'Recipe',
        name: 'MetaObjectList',
        rule: [
            {
                itemprop: 'object',
                isId: true,
                itemtype: {
                    type: 'referenceToObj',
                    allowedTypes: new Set(['*'])
                }
            },
            {
                itemprop: 'metaobject',
                // TODO: This should be a map from type to object
                itemtype: {
                    type: 'bag',
                    item: {
                        type: 'referenceToObj',
                        allowedTypes: new Set(['*'])
                    }
                }
            }
        ]
    }
];