feat: add metadata structures and ser/der functions
This commit is contained in:
parent
892dd6ccd4
commit
4693117c76
|
@ -0,0 +1,3 @@
|
|||
export default abstract class Metadata {
|
||||
abstract toJson(): { [key: string]: any };
|
||||
}
|
|
@ -0,0 +1,437 @@
|
|||
import Metadata from "#serialization/metadata/base.js";
|
||||
import Packer from "#serialization/pack.js";
|
||||
import { metadataMagicByte } from "lib/index.js";
|
||||
import { METADATA_TYPES } from "#constants.js";
|
||||
import Unpacker from "#serialization/unpack.js";
|
||||
import ExtraMetadata from "#serialization/metadata/extra.js";
|
||||
import { Buffer } from "buffer";
|
||||
import CID from "#cid.js";
|
||||
import { Multihash } from "#multihash.js";
|
||||
import { base64url } from "multiformats/bases/base64";
|
||||
import EncryptedCID from "#encrypted_cid.js";
|
||||
|
||||
export default class DirectoryMetadata extends Metadata {
|
||||
details: DirectoryMetadataDetails;
|
||||
directories: { [key: string]: DirectoryReference };
|
||||
files: { [key: string]: FileReference };
|
||||
extraMetadata: ExtraMetadata;
|
||||
|
||||
constructor(
|
||||
details: DirectoryMetadataDetails,
|
||||
directories: { [key: string]: DirectoryReference },
|
||||
files: { [key: string]: FileReference },
|
||||
extraMetadata: ExtraMetadata,
|
||||
) {
|
||||
super();
|
||||
this.details = details;
|
||||
this.directories = directories;
|
||||
this.files = files;
|
||||
this.extraMetadata = extraMetadata;
|
||||
}
|
||||
|
||||
serialize(): Uint8Array {
|
||||
const p = new Packer();
|
||||
p.packInt(metadataMagicByte);
|
||||
p.packInt(METADATA_TYPES.DIRECTORY);
|
||||
|
||||
p.packListLength(4);
|
||||
|
||||
p.pack(this.details.data);
|
||||
|
||||
p.packMapLength(Object.keys(this.directories).length);
|
||||
Object.entries(this.directories).forEach(([key, value]) => {
|
||||
p.packString(key);
|
||||
p.pack(value.encode());
|
||||
});
|
||||
|
||||
p.packMapLength(Object.keys(this.files).length);
|
||||
Object.entries(this.files).forEach(([key, value]) => {
|
||||
p.packString(key);
|
||||
p.pack(value.encode());
|
||||
});
|
||||
|
||||
p.pack(this.extraMetadata.data);
|
||||
|
||||
return p.takeBytes();
|
||||
}
|
||||
|
||||
toJson(): { [key: string]: any } {
|
||||
return {
|
||||
type: "directory",
|
||||
details: this.details,
|
||||
directories: this.directories,
|
||||
files: this.files,
|
||||
extraMetadata: this.extraMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
static deserialize(bytes: Uint8Array): DirectoryMetadata {
|
||||
const u = new Unpacker(Buffer.from(bytes));
|
||||
|
||||
const magicByte = u.unpackInt();
|
||||
if (magicByte !== metadataMagicByte) {
|
||||
throw new Error("Invalid metadata: Unsupported magic byte");
|
||||
}
|
||||
const typeAndVersion = u.unpackInt();
|
||||
if (typeAndVersion !== METADATA_TYPES.DIRECTORY) {
|
||||
throw new Error("Invalid metadata: Wrong metadata type");
|
||||
}
|
||||
|
||||
u.unpackListLength();
|
||||
|
||||
const dir = new DirectoryMetadata(
|
||||
new DirectoryMetadataDetails(u.unpackMap()),
|
||||
{},
|
||||
{},
|
||||
new ExtraMetadata({}),
|
||||
);
|
||||
|
||||
const dirCount = u.unpackMapLength();
|
||||
for (let i = 0; i < dirCount; i++) {
|
||||
const key = u.unpackString();
|
||||
dir.directories[key as string] = DirectoryReference.decode(u.unpackMap());
|
||||
}
|
||||
|
||||
const fileCount = u.unpackMapLength();
|
||||
for (let i = 0; i < fileCount; i++) {
|
||||
const key = u.unpackString();
|
||||
dir.files[key as string] = FileReference.decode(u.unpackMap());
|
||||
}
|
||||
|
||||
Object.assign(dir.extraMetadata.data, u.unpackMap());
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
|
||||
class DirectoryMetadataDetails {
|
||||
data: Map<number, any>;
|
||||
|
||||
constructor(data: Map<number, any> | object) {
|
||||
if (data instanceof Map && typeof data == "object") {
|
||||
data = Object.entries(data).map(([key, value]) => [Number(key), value]);
|
||||
}
|
||||
this.data = data as Map<number, any>;
|
||||
}
|
||||
|
||||
get isShared(): boolean {
|
||||
return this.data.has(3);
|
||||
}
|
||||
|
||||
get isSharedReadOnly(): boolean {
|
||||
return this.data.get(3)?.[1] ?? false;
|
||||
}
|
||||
|
||||
get isSharedReadWrite(): boolean {
|
||||
return this.data.get(3)?.[2] ?? false;
|
||||
}
|
||||
|
||||
setShared(value: boolean, write: boolean): void {
|
||||
if (!this.data.has(3)) {
|
||||
this.data.set(3, {});
|
||||
}
|
||||
this.data.get(3)[write ? 2 : 1] = value;
|
||||
}
|
||||
|
||||
toJson(): { [key: string]: any } {
|
||||
// Convert the data Map to a JSON object
|
||||
const jsonObject: { [key: string]: any } = {};
|
||||
this.data.forEach((value, key) => {
|
||||
jsonObject[key.toString()] = value;
|
||||
});
|
||||
return jsonObject;
|
||||
}
|
||||
}
|
||||
|
||||
class DirectoryReference {
|
||||
created: number;
|
||||
name: string;
|
||||
encryptedWriteKey: Uint8Array;
|
||||
publicKey: Uint8Array;
|
||||
encryptionKey: Uint8Array | null;
|
||||
ext: { [key: string]: any } | null;
|
||||
|
||||
uri: string | null; // For internal operations
|
||||
key: string | null; // For internal operations
|
||||
size: number | null; // For internal operations
|
||||
|
||||
constructor(
|
||||
created: number,
|
||||
name: string,
|
||||
encryptedWriteKey: Uint8Array,
|
||||
publicKey: Uint8Array,
|
||||
encryptionKey: Uint8Array | null,
|
||||
ext: { [key: string]: any } | null,
|
||||
) {
|
||||
this.created = created;
|
||||
this.name = name;
|
||||
this.encryptedWriteKey = encryptedWriteKey;
|
||||
this.publicKey = publicKey;
|
||||
this.encryptionKey = encryptionKey;
|
||||
this.ext = ext;
|
||||
this.uri = null;
|
||||
this.key = null;
|
||||
this.size = null;
|
||||
}
|
||||
|
||||
toJson(): { [key: string]: any } {
|
||||
return {
|
||||
name: this.name,
|
||||
created: this.created,
|
||||
publicKey: base64url.encode(this.publicKey),
|
||||
encryptedWriteKey: base64url.encode(this.encryptedWriteKey),
|
||||
encryptionKey: this.encryptionKey
|
||||
? base64url.encode(this.encryptionKey)
|
||||
: null,
|
||||
ext: this.ext,
|
||||
};
|
||||
}
|
||||
|
||||
static decode(data: { [key: number]: any }): DirectoryReference {
|
||||
return new DirectoryReference(
|
||||
data[2],
|
||||
data[1],
|
||||
data[4],
|
||||
data[3],
|
||||
data[5],
|
||||
data[6] ? (data[6] as { [key: string]: any }) : null,
|
||||
);
|
||||
}
|
||||
|
||||
encode(): { [key: number]: any } {
|
||||
const map: { [key: number]: any } = {
|
||||
1: this.name,
|
||||
2: this.created,
|
||||
3: this.publicKey,
|
||||
4: this.encryptedWriteKey,
|
||||
};
|
||||
|
||||
if (this.encryptionKey !== null) {
|
||||
map[5] = this.encryptionKey;
|
||||
}
|
||||
if (this.ext !== null) {
|
||||
map[6] = this.ext;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
class FileReference {
|
||||
created: number;
|
||||
file: FileVersion;
|
||||
history: Map<number, FileVersion> | null;
|
||||
mimeType: string | null;
|
||||
name: string;
|
||||
version: number;
|
||||
ext: { [key: string]: any } | null;
|
||||
|
||||
uri: string | null; // For internal operations
|
||||
key: string | null; // For internal operations
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
created: number,
|
||||
version: number,
|
||||
file: FileVersion,
|
||||
ext: { [key: string]: any } | null = null,
|
||||
history: Map<number, FileVersion> | null = null,
|
||||
mimeType: string | null = null,
|
||||
) {
|
||||
this.name = name;
|
||||
this.created = created;
|
||||
this.version = version;
|
||||
this.file = file;
|
||||
this.ext = ext;
|
||||
this.history = history;
|
||||
this.mimeType = mimeType;
|
||||
this.uri = null;
|
||||
this.key = null;
|
||||
}
|
||||
|
||||
get modified(): number {
|
||||
return this.file.ts;
|
||||
}
|
||||
|
||||
toJson(): { [key: string]: any } {
|
||||
return {
|
||||
name: this.name,
|
||||
created: this.created,
|
||||
modified: this.modified,
|
||||
version: this.version,
|
||||
mimeType: this.mimeType,
|
||||
file: this.file.toJson(),
|
||||
ext: this.ext,
|
||||
history: this.history
|
||||
? Array.from(this.history.values()).map((fv) => fv.toJson())
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
static decode(data: { [key: number]: any }): FileReference {
|
||||
const historyData = data[8] as { [key: number]: any } | undefined;
|
||||
const history = historyData
|
||||
? new Map(
|
||||
Object.entries(historyData).map(([k, v]) => [
|
||||
Number(k),
|
||||
FileVersion.decode(v),
|
||||
]),
|
||||
)
|
||||
: null;
|
||||
|
||||
return new FileReference(
|
||||
data[1],
|
||||
data[2],
|
||||
data[5],
|
||||
FileVersion.decode(data[4]),
|
||||
data[7] ? (data[7] as { [key: string]: any }) : null,
|
||||
history,
|
||||
data[6],
|
||||
);
|
||||
}
|
||||
|
||||
encode(): { [key: number]: any } {
|
||||
const data: { [key: number]: any } = {
|
||||
1: this.name,
|
||||
2: this.created,
|
||||
4: this.file.encode(),
|
||||
5: this.version,
|
||||
};
|
||||
|
||||
if (this.mimeType !== null) {
|
||||
data[6] = this.mimeType;
|
||||
}
|
||||
if (this.ext !== null) {
|
||||
data[7] = this.ext;
|
||||
}
|
||||
if (this.history !== null) {
|
||||
data[8] = Array.from(this.history.entries()).reduce(
|
||||
(obj, [key, value]) => {
|
||||
obj[key] = value.encode();
|
||||
return obj;
|
||||
},
|
||||
{} as { [key: number]: any },
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class FileVersion {
|
||||
ts: number;
|
||||
encryptedCID?: EncryptedCID;
|
||||
plaintextCID?: CID;
|
||||
thumbnail?: FileVersionThumbnail;
|
||||
hashes?: Multihash[];
|
||||
ext?: { [key: string]: any };
|
||||
|
||||
constructor(
|
||||
ts: number,
|
||||
encryptedCID?: EncryptedCID,
|
||||
plaintextCID?: CID,
|
||||
thumbnail?: FileVersionThumbnail,
|
||||
hashes?: Multihash[],
|
||||
ext?: { [key: string]: any },
|
||||
) {
|
||||
this.ts = ts;
|
||||
this.encryptedCID = encryptedCID;
|
||||
this.plaintextCID = plaintextCID;
|
||||
this.thumbnail = thumbnail;
|
||||
this.hashes = hashes;
|
||||
this.ext = ext;
|
||||
}
|
||||
|
||||
get cid(): CID {
|
||||
return this.plaintextCID ?? this.encryptedCID!.originalCID;
|
||||
}
|
||||
|
||||
static decode(data: { [key: number]: any }): FileVersion {
|
||||
return new FileVersion(
|
||||
data[8],
|
||||
data[1] == null ? undefined : EncryptedCID.fromBytes(data[1]),
|
||||
data[2] == null ? undefined : CID.fromBytes(data[2]),
|
||||
data[10] == null ? undefined : FileVersionThumbnail.decode(data[10]),
|
||||
data[9] ? data[9].map((e: any) => new Multihash(e)) : null,
|
||||
);
|
||||
}
|
||||
|
||||
encode(): { [key: number]: any } {
|
||||
const data: { [key: number]: any } = { 8: this.ts };
|
||||
|
||||
if (!!this.encryptedCID) {
|
||||
data[1] = this.encryptedCID.toBytes();
|
||||
}
|
||||
if (!!this.plaintextCID) {
|
||||
data[2] = this.plaintextCID.toBytes();
|
||||
}
|
||||
if (!!this.hashes) {
|
||||
data[9] = this.hashes.map((e) => e.fullBytes);
|
||||
}
|
||||
if (!!this.thumbnail) {
|
||||
data[10] = this.thumbnail.encode();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
toJson(): { [key: string]: any } {
|
||||
return {
|
||||
ts: this.ts,
|
||||
encryptedCID: this.encryptedCID?.toBase58(),
|
||||
cid: this.cid.toBase58(),
|
||||
hashes: this.hashes?.map((e) => e.toBase64Url()),
|
||||
thumbnail: this.thumbnail?.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
class FileVersionThumbnail {
|
||||
imageType: string | null;
|
||||
aspectRatio: number;
|
||||
cid: EncryptedCID;
|
||||
thumbhash: Uint8Array | null;
|
||||
|
||||
constructor(
|
||||
imageType: string | null,
|
||||
aspectRatio: number,
|
||||
cid: EncryptedCID,
|
||||
thumbhash: Uint8Array | null,
|
||||
) {
|
||||
this.imageType = imageType || "webp"; // Default to 'webp' if not provided
|
||||
this.aspectRatio = aspectRatio;
|
||||
this.cid = cid;
|
||||
this.thumbhash = thumbhash;
|
||||
}
|
||||
|
||||
toJson(): { [key: string]: any } {
|
||||
return {
|
||||
imageType: this.imageType,
|
||||
aspectRatio: this.aspectRatio,
|
||||
cid: this.cid.toBase58(),
|
||||
thumbhash: this.thumbhash ? base64url.encode(this.thumbhash) : null,
|
||||
};
|
||||
}
|
||||
|
||||
static decode(data: { [key: number]: any }): FileVersionThumbnail {
|
||||
return new FileVersionThumbnail(
|
||||
data[1],
|
||||
data[2],
|
||||
EncryptedCID.fromBytes(data[3]),
|
||||
data[4],
|
||||
);
|
||||
}
|
||||
|
||||
encode(): { [key: number]: any } {
|
||||
const data: { [key: number]: any } = {
|
||||
2: this.aspectRatio,
|
||||
3: this.cid.toBytes(),
|
||||
};
|
||||
|
||||
if (this.imageType !== null) {
|
||||
data[1] = this.imageType;
|
||||
}
|
||||
if (this.thumbhash !== null) {
|
||||
data[4] = this.thumbhash;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import {
|
||||
metadataExtensionBasicMediaMetadata,
|
||||
metadataExtensionBridge,
|
||||
metadataExtensionCategories,
|
||||
metadataExtensionDonationKeys,
|
||||
metadataExtensionLanguages,
|
||||
metadataExtensionLicenses,
|
||||
metadataExtensionOriginalTimestamp,
|
||||
metadataExtensionPreviousVersions,
|
||||
metadataExtensionRoutingHints,
|
||||
metadataExtensionSourceUris,
|
||||
metadataExtensionTags,
|
||||
metadataExtensionTimestamp,
|
||||
metadataExtensionUpdateCID,
|
||||
metadataExtensionViewTypes,
|
||||
metadataExtensionWikidataClaims,
|
||||
} from "#constants.js";
|
||||
import CID from "#cid.js";
|
||||
|
||||
export default class ExtraMetadata {
|
||||
data: Map<number, any>;
|
||||
|
||||
constructor(data: Map<number, any> | object) {
|
||||
if (data instanceof Map && typeof data == "object") {
|
||||
data = Object.entries(data).map(([key, value]) => [Number(key), value]);
|
||||
}
|
||||
this.data = data as Map<number, any>;
|
||||
}
|
||||
|
||||
toJson(): { [key: string]: any } {
|
||||
const map: { [key: string]: any } = {};
|
||||
const names: { [key: number]: string } = {
|
||||
[metadataExtensionLicenses]: "licenses",
|
||||
[metadataExtensionDonationKeys]: "donationKeys",
|
||||
[metadataExtensionWikidataClaims]: "wikidataClaims",
|
||||
[metadataExtensionLanguages]: "languages",
|
||||
[metadataExtensionSourceUris]: "sourceUris",
|
||||
// metadataExtensionUpdateCID: 'updateCID',
|
||||
[metadataExtensionPreviousVersions]: "previousVersions",
|
||||
[metadataExtensionTimestamp]: "timestamp",
|
||||
[metadataExtensionOriginalTimestamp]: "originalTimestamp",
|
||||
[metadataExtensionTags]: "tags",
|
||||
[metadataExtensionCategories]: "categories",
|
||||
[metadataExtensionBasicMediaMetadata]: "basicMediaMetadata",
|
||||
[metadataExtensionViewTypes]: "viewTypes",
|
||||
[metadataExtensionBridge]: "bridge",
|
||||
[metadataExtensionRoutingHints]: "routingHints",
|
||||
};
|
||||
|
||||
this.data.forEach((value, key) => {
|
||||
if (key === metadataExtensionUpdateCID) {
|
||||
map["updateCID"] = CID.fromBytes(value).toString();
|
||||
} else {
|
||||
map[names[key]] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,525 @@
|
|||
import Metadata from "#serialization/metadata/base.js";
|
||||
import CID from "#cid.js";
|
||||
import {
|
||||
CID_HASH_TYPES,
|
||||
METADATA_TYPES,
|
||||
metadataMagicByte,
|
||||
metadataMediaDetailsDuration,
|
||||
metadataMediaDetailsIsLive,
|
||||
metadataProofTypeSignature,
|
||||
parentLinkTypeUserIdentity,
|
||||
} from "#constants.js";
|
||||
import ExtraMetadata from "#serialization/metadata/extra.js";
|
||||
import { MetadataParentLink } from "#serialization/metadata/parent.js";
|
||||
import { Multihash } from "#multihash.js";
|
||||
import { decodeEndian, encodeEndian } from "#util.js";
|
||||
import Unpacker from "#serialization/unpack.js";
|
||||
import { Buffer } from "buffer";
|
||||
import { blake3 } from "@noble/hashes/blake3";
|
||||
import { ed25519 } from "@noble/curves/ed25519";
|
||||
import KeyPairEd25519 from "#ed25519.js";
|
||||
import Packer from "#serialization/pack.js";
|
||||
|
||||
export default class MediaMetadata extends Metadata {
|
||||
name: string;
|
||||
mediaTypes: { [key: string]: MediaFormat[] };
|
||||
parents: MetadataParentLink[];
|
||||
details: MediaMetadataDetails;
|
||||
links: MediaMetadataLinks | null;
|
||||
extraMetadata: ExtraMetadata;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
details: MediaMetadataDetails,
|
||||
parents: MetadataParentLink[],
|
||||
mediaTypes: { [key: string]: MediaFormat[] },
|
||||
links: MediaMetadataLinks | null,
|
||||
extraMetadata: ExtraMetadata,
|
||||
) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.details = details;
|
||||
this.parents = parents;
|
||||
this.mediaTypes = mediaTypes;
|
||||
this.links = links;
|
||||
this.extraMetadata = extraMetadata;
|
||||
}
|
||||
|
||||
toJson(): { [key: string]: any } {
|
||||
return {
|
||||
type: "media",
|
||||
name: this.name,
|
||||
details: this.details,
|
||||
parents: this.parents,
|
||||
mediaTypes: this.mediaTypes,
|
||||
links: this.links,
|
||||
extraMetadata: this.extraMetadata,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MediaMetadataLinks {
|
||||
count: number;
|
||||
head: CID[];
|
||||
collapsed: CID[] | null;
|
||||
tail: CID[] | null;
|
||||
|
||||
constructor(head: CID[]) {
|
||||
this.head = head;
|
||||
this.count = head.length;
|
||||
this.collapsed = null;
|
||||
this.tail = null;
|
||||
}
|
||||
|
||||
toJson(): { [key: string]: any } {
|
||||
const map: { [key: string]: any } = {
|
||||
count: this.count,
|
||||
head: this.head.map((e) => e.toString()),
|
||||
};
|
||||
if (this.collapsed != null) {
|
||||
map["collapsed"] = this.collapsed.map((e) => e.toString());
|
||||
}
|
||||
if (this.tail != null) {
|
||||
map["tail"] = this.tail.map((e) => e.toString());
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
static decode(links: { [key: number]: any }): MediaMetadataLinks {
|
||||
const count = links[1] as number;
|
||||
const head = (links[2] as Uint8Array[]).map((bytes) =>
|
||||
CID.fromBytes(bytes),
|
||||
);
|
||||
const collapsed = links[3]
|
||||
? (links[3] as Uint8Array[]).map((bytes) => CID.fromBytes(bytes))
|
||||
: null;
|
||||
const tail = links[4]
|
||||
? (links[4] as Uint8Array[]).map((bytes) => CID.fromBytes(bytes))
|
||||
: null;
|
||||
|
||||
const instance = new MediaMetadataLinks(head);
|
||||
instance.count = count;
|
||||
instance.collapsed = collapsed;
|
||||
instance.tail = tail;
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
encode(): { [key: number]: any } {
|
||||
const data: { [key: number]: any } = {
|
||||
1: this.count,
|
||||
2: this.head,
|
||||
};
|
||||
|
||||
const addNotNull = (key: number, value: any) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
data[key] = value;
|
||||
}
|
||||
};
|
||||
|
||||
addNotNull(3, this.collapsed);
|
||||
addNotNull(4, this.tail);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class MediaMetadataDetails {
|
||||
data: { [key: number]: any };
|
||||
|
||||
constructor(data: { [key: number]: any }) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
toJson(): { [key: string]: any } {
|
||||
const map: { [key: string]: any } = {};
|
||||
const names: { [key: number]: string } = {
|
||||
[metadataMediaDetailsDuration]: "duration",
|
||||
[metadataMediaDetailsIsLive]: "live",
|
||||
};
|
||||
Object.entries(this.data).forEach(([key, value]) => {
|
||||
map[names[+key]] = value;
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
get duration(): number | null {
|
||||
return this.data[metadataMediaDetailsDuration];
|
||||
}
|
||||
|
||||
get isLive(): boolean {
|
||||
return !!this.data[metadataMediaDetailsIsLive];
|
||||
}
|
||||
}
|
||||
|
||||
class MediaFormat {
|
||||
subtype: string;
|
||||
role: string | null;
|
||||
ext: string | null;
|
||||
cid: CID | null;
|
||||
height: number | null;
|
||||
width: number | null;
|
||||
languages: string[] | null;
|
||||
asr: number | null;
|
||||
fps: number | null;
|
||||
bitrate: number | null;
|
||||
audioChannels: number | null;
|
||||
vcodec: string | null;
|
||||
acodec: string | null;
|
||||
container: string | null;
|
||||
dynamicRange: string | null;
|
||||
charset: string | null;
|
||||
value: Uint8Array | null;
|
||||
duration: number | null;
|
||||
rows: number | null;
|
||||
columns: number | null;
|
||||
index: number | null;
|
||||
initRange: string | null;
|
||||
indexRange: string | null;
|
||||
caption: string | null;
|
||||
|
||||
constructor(
|
||||
subtype: string,
|
||||
role: string | null,
|
||||
ext: string | null,
|
||||
cid: CID | null,
|
||||
height: number | null,
|
||||
width: number | null,
|
||||
languages: string[] | null,
|
||||
asr: number | null,
|
||||
fps: number | null,
|
||||
bitrate: number | null,
|
||||
audioChannels: number | null,
|
||||
vcodec: string | null,
|
||||
acodec: string | null,
|
||||
container: string | null,
|
||||
dynamicRange: string | null,
|
||||
charset: string | null,
|
||||
value: Uint8Array | null,
|
||||
duration: number | null,
|
||||
rows: number | null,
|
||||
columns: number | null,
|
||||
index: number | null,
|
||||
initRange: string | null,
|
||||
indexRange: string | null,
|
||||
caption: string | null,
|
||||
) {
|
||||
this.subtype = subtype;
|
||||
this.role = role;
|
||||
this.ext = ext;
|
||||
this.cid = cid;
|
||||
this.height = height;
|
||||
this.width = width;
|
||||
this.languages = languages;
|
||||
this.asr = asr;
|
||||
this.fps = fps;
|
||||
this.bitrate = bitrate;
|
||||
this.audioChannels = audioChannels;
|
||||
this.vcodec = vcodec;
|
||||
this.acodec = acodec;
|
||||
this.container = container;
|
||||
this.dynamicRange = dynamicRange;
|
||||
this.charset = charset;
|
||||
this.value = value;
|
||||
this.duration = duration;
|
||||
this.rows = rows;
|
||||
this.columns = columns;
|
||||
this.index = index;
|
||||
this.initRange = initRange;
|
||||
this.indexRange = indexRange;
|
||||
this.caption = caption;
|
||||
}
|
||||
|
||||
get valueAsString(): string | null {
|
||||
if (this.value === null) {
|
||||
return null;
|
||||
}
|
||||
return new TextDecoder().decode(this.value);
|
||||
}
|
||||
|
||||
static decode(data: { [key: number]: any }): MediaFormat {
|
||||
return new MediaFormat(
|
||||
data[2], // subtype
|
||||
data[3], // role
|
||||
data[4], // ext
|
||||
data[1] == null ? null : CID.fromBytes(Uint8Array.from(data[1])),
|
||||
data[10], // height
|
||||
data[11], // width
|
||||
data[12] ? (data[12] as string[]) : null, // languages
|
||||
data[13], // asr
|
||||
data[14], // fps
|
||||
data[15], // bitrate
|
||||
data[18], // audioChannels
|
||||
data[19], // vcodec
|
||||
data[20], // acodec
|
||||
data[21], // container
|
||||
data[22], // dynamicRange
|
||||
data[23], // charset
|
||||
data[24] == null ? null : Uint8Array.from(data[24]), // value
|
||||
data[25], // duration
|
||||
data[26], // rows
|
||||
data[27], // columns
|
||||
data[28], // index
|
||||
data[29], // initRange
|
||||
data[30], // indexRange
|
||||
data[31], // caption
|
||||
);
|
||||
}
|
||||
|
||||
encode(): { [key: number]: any } {
|
||||
const data: { [key: number]: any } = {};
|
||||
|
||||
const addNotNull = (key: number, value: any) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
data[key] = value;
|
||||
}
|
||||
};
|
||||
|
||||
addNotNull(1, this.cid?.toBytes());
|
||||
addNotNull(2, this.subtype);
|
||||
addNotNull(3, this.role);
|
||||
addNotNull(4, this.ext);
|
||||
addNotNull(10, this.height);
|
||||
addNotNull(11, this.width);
|
||||
addNotNull(12, this.languages);
|
||||
addNotNull(13, this.asr);
|
||||
addNotNull(14, this.fps);
|
||||
addNotNull(15, this.bitrate);
|
||||
// addNotNull(16, this.abr);
|
||||
// addNotNull(17, this.vbr);
|
||||
addNotNull(18, this.audioChannels);
|
||||
addNotNull(19, this.vcodec);
|
||||
addNotNull(20, this.acodec);
|
||||
addNotNull(21, this.container);
|
||||
addNotNull(22, this.dynamicRange);
|
||||
addNotNull(23, this.charset);
|
||||
addNotNull(24, this.value);
|
||||
addNotNull(25, this.duration);
|
||||
addNotNull(26, this.rows);
|
||||
addNotNull(27, this.columns);
|
||||
addNotNull(28, this.index);
|
||||
addNotNull(29, this.initRange);
|
||||
addNotNull(30, this.indexRange);
|
||||
addNotNull(31, this.caption);
|
||||
|
||||
return data;
|
||||
}
|
||||
toJson(): { [key: string]: any } {
|
||||
const data: { [key: string]: any } = {};
|
||||
|
||||
const addNotNull = (key: string, value: any) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
data[key] = value;
|
||||
}
|
||||
};
|
||||
|
||||
addNotNull("cid", this.cid?.toBase64Url());
|
||||
addNotNull("subtype", this.subtype);
|
||||
addNotNull("role", this.role);
|
||||
addNotNull("ext", this.ext);
|
||||
addNotNull("height", this.height);
|
||||
addNotNull("width", this.width);
|
||||
addNotNull("languages", this.languages);
|
||||
addNotNull("asr", this.asr);
|
||||
addNotNull("fps", this.fps);
|
||||
addNotNull("bitrate", this.bitrate);
|
||||
// addNotNull('abr', this.abr);
|
||||
// addNotNull('vbr', this.vbr);
|
||||
addNotNull("audioChannels", this.audioChannels);
|
||||
addNotNull("vcodec", this.vcodec);
|
||||
addNotNull("acodec", this.acodec);
|
||||
addNotNull("container", this.container);
|
||||
addNotNull("dynamicRange", this.dynamicRange);
|
||||
addNotNull("charset", this.charset);
|
||||
addNotNull("value", this.value ? this.valueAsString : null); // Assuming valueAsString() is a method to convert value to string
|
||||
addNotNull("duration", this.duration);
|
||||
addNotNull("rows", this.rows);
|
||||
addNotNull("columns", this.columns);
|
||||
addNotNull("index", this.index);
|
||||
addNotNull("initRange", this.initRange);
|
||||
addNotNull("indexRange", this.indexRange);
|
||||
addNotNull("caption", this.caption);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deserialize(bytes: Uint8Array): Promise<MediaMetadata> {
|
||||
const magicByte = bytes[0];
|
||||
if (magicByte !== metadataMagicByte) {
|
||||
throw new Error("Invalid metadata: Unsupported magic byte");
|
||||
}
|
||||
const typeAndVersion = bytes[1];
|
||||
let bodyBytes: Uint8Array;
|
||||
|
||||
const provenPubKeys: Multihash[] = [];
|
||||
|
||||
if (typeAndVersion === METADATA_TYPES.PROOF) {
|
||||
const proofSectionLength = decodeEndian(bytes.subarray(2, 4));
|
||||
|
||||
bodyBytes = bytes.subarray(4 + proofSectionLength);
|
||||
|
||||
if (proofSectionLength > 0) {
|
||||
const proofUnpacker = new Unpacker(
|
||||
Buffer.from(bytes.subarray(4, 4 + proofSectionLength)),
|
||||
);
|
||||
|
||||
const b3hash = Uint8Array.from([
|
||||
CID_HASH_TYPES.BLAKE3,
|
||||
...blake3(bodyBytes),
|
||||
]);
|
||||
|
||||
const proofCount = proofUnpacker.unpackListLength();
|
||||
|
||||
for (let i = 0; i < proofCount; i++) {
|
||||
const parts = proofUnpacker.unpackList();
|
||||
const proofType = parts[0] as number;
|
||||
|
||||
if (proofType === metadataProofTypeSignature) {
|
||||
const mhashType = parts[1] as number;
|
||||
const pubkey = parts[2] as Uint8Array;
|
||||
const signature = parts[3] as Uint8Array;
|
||||
|
||||
if (mhashType !== CID_HASH_TYPES.BLAKE3) {
|
||||
throw new Error(`Hash type ${mhashType} not supported`);
|
||||
}
|
||||
|
||||
if (pubkey[0] !== CID_HASH_TYPES.ED25519) {
|
||||
throw new Error("Only ed25519 keys are supported");
|
||||
}
|
||||
if (pubkey.length !== 33) {
|
||||
throw new Error("Invalid userId");
|
||||
}
|
||||
|
||||
const isValid = await ed25519.verify(
|
||||
signature,
|
||||
b3hash,
|
||||
pubkey.subarray(1),
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error("Invalid signature found");
|
||||
}
|
||||
provenPubKeys.push(new Multihash(pubkey));
|
||||
} else {
|
||||
// Unsupported proof type
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeAndVersion === METADATA_TYPES.MEDIA) {
|
||||
bodyBytes = bytes.subarray(1);
|
||||
} else {
|
||||
throw new Error(`Invalid metadata: Unsupported type ${typeAndVersion}`);
|
||||
}
|
||||
|
||||
// Start of body section
|
||||
const u = new Unpacker(Buffer.from(bodyBytes));
|
||||
const type = u.unpackInt();
|
||||
|
||||
if (type !== METADATA_TYPES.MEDIA) {
|
||||
throw new Error(`Invalid metadata: Unsupported type ${type}`);
|
||||
}
|
||||
|
||||
u.unpackListLength();
|
||||
const name = u.unpackString();
|
||||
const details = new MediaMetadataDetails(u.unpackMap());
|
||||
|
||||
const parents: MetadataParentLink[] = [];
|
||||
const userCount = u.unpackListLength();
|
||||
for (let i = 0; i < userCount; i++) {
|
||||
const m = u.unpackMap();
|
||||
const cid = CID.fromBytes(m[1] as Uint8Array);
|
||||
|
||||
parents.push(
|
||||
new MetadataParentLink(
|
||||
cid,
|
||||
(m[0] ?? parentLinkTypeUserIdentity) as number,
|
||||
m[2],
|
||||
provenPubKeys.some((pk) => pk.equals(cid.hash)), // Assuming Multihash class has an equals method
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const mediaTypesMap = u.unpackMap() as Record<string, any>;
|
||||
const mediaTypes: Record<string, MediaFormat[]> = {};
|
||||
|
||||
Object.entries(mediaTypesMap).forEach(([type, formats]) => {
|
||||
mediaTypes[type] = formats.map((e: any) =>
|
||||
MediaFormat.decode(e as Record<number, any>),
|
||||
);
|
||||
});
|
||||
|
||||
const links = u.unpackMap();
|
||||
const extraMetadata = u.unpackMap();
|
||||
|
||||
return new MediaMetadata(
|
||||
name || "",
|
||||
details,
|
||||
parents,
|
||||
mediaTypes,
|
||||
links.size > 0 ? MediaMetadataLinks.decode(links) : null,
|
||||
new ExtraMetadata(extraMetadata),
|
||||
);
|
||||
}
|
||||
|
||||
export async function serialize(
|
||||
m: MediaMetadata,
|
||||
keyPairs: KeyPairEd25519[] = [],
|
||||
): Promise<Uint8Array> {
|
||||
const c = new Packer();
|
||||
c.packInt(METADATA_TYPES.MEDIA);
|
||||
c.packListLength(6);
|
||||
|
||||
c.packString(m.name);
|
||||
c.pack(m.details.data);
|
||||
|
||||
c.packListLength(m.parents.length);
|
||||
for (const parent of m.parents) {
|
||||
c.pack({ 0: parent.type, 1: parent.cid.toBytes() });
|
||||
}
|
||||
|
||||
c.packMapLength(Object.keys(m.mediaTypes).length);
|
||||
for (const [key, value] of Object.entries(m.mediaTypes)) {
|
||||
c.packString(key);
|
||||
c.pack(value);
|
||||
}
|
||||
|
||||
if (m.links === null) {
|
||||
c.packMapLength(0);
|
||||
} else {
|
||||
c.pack(m.links.encode());
|
||||
}
|
||||
|
||||
c.pack(m.extraMetadata.data);
|
||||
|
||||
const bodyBytes = c.takeBytes();
|
||||
|
||||
if (keyPairs.length === 0) {
|
||||
return Uint8Array.from([metadataMagicByte, ...bodyBytes]);
|
||||
}
|
||||
|
||||
const b3hash = Uint8Array.from([CID_HASH_TYPES.BLAKE3, ...blake3(bodyBytes)]);
|
||||
|
||||
const proofPacker = new Packer();
|
||||
proofPacker.packListLength(keyPairs.length);
|
||||
|
||||
for (const kp of keyPairs) {
|
||||
const signature = await ed25519.sign(b3hash, kp.extractBytes());
|
||||
proofPacker.pack([
|
||||
metadataProofTypeSignature,
|
||||
CID_HASH_TYPES.BLAKE3,
|
||||
kp.publicKey,
|
||||
signature,
|
||||
]);
|
||||
}
|
||||
const proofBytes = proofPacker.takeBytes();
|
||||
|
||||
const header = [
|
||||
metadataMagicByte,
|
||||
METADATA_TYPES.PROOF,
|
||||
...encodeEndian(proofBytes.length, 2),
|
||||
];
|
||||
|
||||
return Uint8Array.from([...header, ...proofBytes, ...bodyBytes]);
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import CID from "#cid.js";
|
||||
import { parentLinkTypeUserIdentity } from "#constants.js";
|
||||
|
||||
export class MetadataParentLink {
|
||||
cid: CID;
|
||||
type: number;
|
||||
role: string | null;
|
||||
signed: boolean;
|
||||
|
||||
constructor(
|
||||
cid: CID,
|
||||
type: number = parentLinkTypeUserIdentity,
|
||||
role: string | null = null,
|
||||
signed: boolean = false,
|
||||
) {
|
||||
this.cid = cid;
|
||||
this.type = type;
|
||||
this.role = role;
|
||||
this.signed = signed;
|
||||
}
|
||||
|
||||
toJson(): { [key: string]: any } {
|
||||
const map: { [key: string]: any } = {};
|
||||
|
||||
map["cid"] = this.cid.toString();
|
||||
map["type"] = this.type;
|
||||
if (this.role !== null) {
|
||||
map["role"] = this.role;
|
||||
}
|
||||
map["signed"] = this.signed;
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import CID from "#cid.js";
|
||||
|
||||
export default class UserIdentityMetadata {
|
||||
userID?: CID;
|
||||
details: UserIdentityMetadataDetails;
|
||||
signingKeys: UserIdentityPublicKey[];
|
||||
encryptionKeys: UserIdentityPublicKey[];
|
||||
links: Map<number, CID>;
|
||||
|
||||
constructor(
|
||||
details: UserIdentityMetadataDetails,
|
||||
signingKeys: UserIdentityPublicKey[],
|
||||
encryptionKeys: UserIdentityPublicKey[],
|
||||
links: Map<number, CID>,
|
||||
) {
|
||||
this.details = details;
|
||||
this.signingKeys = signingKeys;
|
||||
this.encryptionKeys = encryptionKeys;
|
||||
this.links = links;
|
||||
}
|
||||
}
|
||||
|
||||
class UserIdentityMetadataDetails {
|
||||
created: number;
|
||||
createdBy: string;
|
||||
|
||||
constructor(created: number, createdBy: string) {
|
||||
this.created = created;
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
}
|
||||
|
||||
class UserIdentityPublicKey {
|
||||
key: Uint8Array;
|
||||
|
||||
constructor(key: Uint8Array) {
|
||||
this.key = key;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
import Metadata from "#serialization/metadata/base.js";
|
||||
import ExtraMetadata from "#serialization/metadata/extra.js";
|
||||
import CID from "#cid.js";
|
||||
import Unpacker from "#serialization/unpack.js";
|
||||
import { METADATA_TYPES, metadataMagicByte } from "#constants.js";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
export default class WebAppMetadata extends Metadata {
|
||||
name: string | null;
|
||||
tryFiles: string[];
|
||||
errorPages: Map<number, string>;
|
||||
extraMetadata: ExtraMetadata;
|
||||
paths: { [key: string]: WebAppMetadataFileReference };
|
||||
|
||||
constructor(
|
||||
name: string | null,
|
||||
tryFiles: string[],
|
||||
errorPages: Map<number, string>,
|
||||
paths: { [key: string]: WebAppMetadataFileReference },
|
||||
extraMetadata: ExtraMetadata,
|
||||
) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.tryFiles = tryFiles;
|
||||
this.errorPages = errorPages;
|
||||
this.paths = paths;
|
||||
this.extraMetadata = extraMetadata;
|
||||
}
|
||||
|
||||
toJson(): { [key: string]: any } {
|
||||
return {
|
||||
type: "web_app",
|
||||
name: this.name,
|
||||
tryFiles: this.tryFiles,
|
||||
errorPages: Array.from(this.errorPages.entries()).reduce(
|
||||
(obj, [key, value]) => {
|
||||
obj[key.toString()] = value;
|
||||
return obj;
|
||||
},
|
||||
{} as { [key: string]: string },
|
||||
),
|
||||
paths: this.paths,
|
||||
extraMetadata: this.extraMetadata,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class WebAppMetadataFileReference {
|
||||
contentType: string | null;
|
||||
cid: CID;
|
||||
|
||||
constructor(cid: CID, contentType: string | null) {
|
||||
this.cid = cid;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.cid.size ?? 0;
|
||||
}
|
||||
|
||||
toJson(): { [key: string]: any } {
|
||||
return {
|
||||
cid: this.cid.toBase64Url(),
|
||||
contentType: this.contentType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function deserialize(bytes: Uint8Array): Promise<WebAppMetadata> {
|
||||
const u = new Unpacker(Buffer.from(bytes));
|
||||
|
||||
const magicByte = u.unpackInt();
|
||||
if (magicByte !== metadataMagicByte) {
|
||||
throw new Error("Invalid metadata: Unsupported magic byte");
|
||||
}
|
||||
const typeAndVersion = u.unpackInt();
|
||||
if (typeAndVersion !== METADATA_TYPES.WEBAPP) {
|
||||
throw new Error("Invalid metadata: Wrong metadata type");
|
||||
}
|
||||
|
||||
u.unpackListLength();
|
||||
|
||||
const name = u.unpackString();
|
||||
|
||||
const tryFiles = u.unpackList() as string[];
|
||||
|
||||
const errorPages = u.unpackMap() as Record<number, string>;
|
||||
|
||||
const length = u.unpackListLength();
|
||||
|
||||
const paths: Record<string, WebAppMetadataFileReference> = {};
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
u.unpackListLength();
|
||||
const path = u.unpackString();
|
||||
const cid = CID.fromBytes(u.unpackBinary());
|
||||
paths[path as string] = new WebAppMetadataFileReference(
|
||||
cid,
|
||||
u.unpackString(),
|
||||
);
|
||||
}
|
||||
|
||||
const extraMetadata = u.unpackMap() as Record<number, any>;
|
||||
|
||||
return new WebAppMetadata(
|
||||
name,
|
||||
tryFiles,
|
||||
new Map<number, string>(
|
||||
Object.entries(errorPages).map(([key, value]) => [Number(key), value]),
|
||||
),
|
||||
paths,
|
||||
new ExtraMetadata(extraMetadata),
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue