@airalogy/aira-core 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -1
- package/dist/index.d.ts +129 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +343 -175
- package/package.json +1 -1
- package/src/index.ts +371 -1
package/README.md
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Core TypeScript parser and validator for Airalogy `.aira` archives.
|
|
4
4
|
|
|
5
|
-
It opens `.aira` files in browser-compatible JavaScript, reads `_airalogy_archive/manifest.json`, lists archive members, loads JSON/text payloads, and validates manifest references, Record hashes, Protocol file hashes, and offline blob hashes.
|
|
5
|
+
It opens `.aira` files in browser-compatible JavaScript, reads `_airalogy_archive/manifest.json`, lists archive members, loads JSON/text payloads, and validates manifest references, Record payload structure, Record hashes, Protocol file hashes, and offline blob hashes.
|
|
6
|
+
|
|
7
|
+
It can also create single-Protocol `.aira` archives in the browser. This is useful for editors that let users attach protocol-local assets such as `files/workflow-diagram.svg` and reference them from AIMD `fig` blocks.
|
|
6
8
|
|
|
7
9
|
Supported archive kinds are `protocol`, `protocols`, and `records`.
|
|
8
10
|
|
|
9
11
|
Example archives covering these kinds are available in `examples/aira/`.
|
|
10
12
|
|
|
13
|
+
The public manifest schema is available at `schemas/aira/manifest.v1.schema.json`; the public Record schema is available at `schemas/aira/record.v1.schema.json`.
|
|
14
|
+
|
|
11
15
|
```ts
|
|
12
16
|
import { openAiraArchive } from '@airalogy/aira-core'
|
|
13
17
|
|
|
@@ -17,4 +21,29 @@ const validation = await archive.validate()
|
|
|
17
21
|
const manifest = archive.manifest
|
|
18
22
|
```
|
|
19
23
|
|
|
24
|
+
Create a Protocol archive with protocol-local figure files:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { createProtocolAiraArchive } from '@airalogy/aira-core'
|
|
28
|
+
|
|
29
|
+
const bytes = await createProtocolAiraArchive({
|
|
30
|
+
aimd: [
|
|
31
|
+
'# Figure Protocol',
|
|
32
|
+
'',
|
|
33
|
+
'```fig',
|
|
34
|
+
'id: workflow_diagram',
|
|
35
|
+
'src: files/workflow-diagram.svg',
|
|
36
|
+
'title: Workflow Diagram',
|
|
37
|
+
'```',
|
|
38
|
+
'',
|
|
39
|
+
].join('\n'),
|
|
40
|
+
files: [
|
|
41
|
+
{
|
|
42
|
+
path: 'files/workflow-diagram.svg',
|
|
43
|
+
data: svgFile,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
20
49
|
`.aira` archives are standard ZIP containers. The core package keeps reading independent from the full Airalogy platform, database, or execution engine.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export declare const AIRA_MANIFEST_PATH = "_airalogy_archive/manifest.json";
|
|
2
|
+
export declare const AIRA_ARCHIVE_FORMAT = "airalogy.archive";
|
|
3
|
+
export declare const AIRALOGY_RECORD_FORMAT = "airalogy.record";
|
|
4
|
+
export declare const AIRALOGY_RECORD_SCHEMA_VERSION = 1;
|
|
5
|
+
export type AiraArchiveKind = 'protocol' | 'protocols' | 'records';
|
|
6
|
+
export interface AiraEntry {
|
|
7
|
+
name: string;
|
|
8
|
+
compressedSize: number;
|
|
9
|
+
uncompressedSize: number;
|
|
10
|
+
compressionMethod: number;
|
|
11
|
+
localHeaderOffset: number;
|
|
12
|
+
}
|
|
13
|
+
export interface AiraProtocolManifest {
|
|
14
|
+
protocol_id?: string | null;
|
|
15
|
+
protocol_version?: string | null;
|
|
16
|
+
protocol_name?: string | null;
|
|
17
|
+
entrypoint?: string;
|
|
18
|
+
archive_root?: string;
|
|
19
|
+
files?: string[];
|
|
20
|
+
file_hashes?: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
export interface AiraRecordManifest {
|
|
23
|
+
path: string;
|
|
24
|
+
record_id?: string | null;
|
|
25
|
+
record_version?: string | number | null;
|
|
26
|
+
protocol_id?: string | null;
|
|
27
|
+
protocol_version?: string | null;
|
|
28
|
+
sha1?: string | null;
|
|
29
|
+
sha256?: string | null;
|
|
30
|
+
source_path?: string;
|
|
31
|
+
source_index?: number;
|
|
32
|
+
embedded_protocol_root?: string | null;
|
|
33
|
+
}
|
|
34
|
+
export interface AiralogyRecordPayload {
|
|
35
|
+
format?: string;
|
|
36
|
+
schema_version?: number;
|
|
37
|
+
airalogy_record_id?: string | null;
|
|
38
|
+
record_id?: string | null;
|
|
39
|
+
record_version?: number | null;
|
|
40
|
+
metadata?: Record<string, unknown> | null;
|
|
41
|
+
data?: {
|
|
42
|
+
var?: Record<string, unknown>;
|
|
43
|
+
step?: Record<string, unknown>;
|
|
44
|
+
check?: Record<string, unknown>;
|
|
45
|
+
quiz?: Record<string, unknown>;
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
};
|
|
48
|
+
files?: Array<Record<string, unknown>>;
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
}
|
|
51
|
+
export interface AiraBlobManifest {
|
|
52
|
+
blob_id: string;
|
|
53
|
+
archive_path: string;
|
|
54
|
+
sha256: string;
|
|
55
|
+
size?: number;
|
|
56
|
+
}
|
|
57
|
+
export interface AiraFileManifest {
|
|
58
|
+
file_id?: string | null;
|
|
59
|
+
source_uri?: string | null;
|
|
60
|
+
blob_id?: string | null;
|
|
61
|
+
filename?: string | null;
|
|
62
|
+
mime_type?: string | null;
|
|
63
|
+
size?: number | null;
|
|
64
|
+
record_path?: string | null;
|
|
65
|
+
field_path?: string | null;
|
|
66
|
+
}
|
|
67
|
+
export interface AiraManifest {
|
|
68
|
+
format: string;
|
|
69
|
+
version: number;
|
|
70
|
+
kind: AiraArchiveKind;
|
|
71
|
+
created_at?: string;
|
|
72
|
+
protocol?: AiraProtocolManifest;
|
|
73
|
+
records?: AiraRecordManifest[];
|
|
74
|
+
protocols?: AiraProtocolManifest[];
|
|
75
|
+
blobs?: AiraBlobManifest[];
|
|
76
|
+
files?: AiraFileManifest[];
|
|
77
|
+
[key: string]: unknown;
|
|
78
|
+
}
|
|
79
|
+
export interface AiraSummary {
|
|
80
|
+
format: string;
|
|
81
|
+
version: number;
|
|
82
|
+
kind: AiraArchiveKind;
|
|
83
|
+
createdAt?: string;
|
|
84
|
+
memberCount: number;
|
|
85
|
+
recordCount: number;
|
|
86
|
+
protocolCount: number;
|
|
87
|
+
blobCount: number;
|
|
88
|
+
fileCount: number;
|
|
89
|
+
}
|
|
90
|
+
export interface AiraValidationResult {
|
|
91
|
+
ok: boolean;
|
|
92
|
+
issues: string[];
|
|
93
|
+
}
|
|
94
|
+
export type AiraArchiveEntryData = string | Blob | ArrayBuffer | ArrayBufferView;
|
|
95
|
+
export interface CreateProtocolAiraArchiveFile {
|
|
96
|
+
path: string;
|
|
97
|
+
data: AiraArchiveEntryData;
|
|
98
|
+
}
|
|
99
|
+
export interface CreateProtocolAiraArchiveOptions {
|
|
100
|
+
aimd: string;
|
|
101
|
+
protocol?: Pick<AiraProtocolManifest, 'protocol_id' | 'protocol_version' | 'protocol_name' | 'entrypoint'>;
|
|
102
|
+
files?: CreateProtocolAiraArchiveFile[];
|
|
103
|
+
createdAt?: string;
|
|
104
|
+
}
|
|
105
|
+
export declare function createProtocolAiraArchive(options: CreateProtocolAiraArchiveOptions): Promise<Uint8Array>;
|
|
106
|
+
export declare class AiraArchive {
|
|
107
|
+
readonly bytes: Uint8Array;
|
|
108
|
+
readonly entries: AiraEntry[];
|
|
109
|
+
readonly manifest: AiraManifest;
|
|
110
|
+
private readonly entryMap;
|
|
111
|
+
private constructor();
|
|
112
|
+
static open(input: ArrayBuffer | Blob | Uint8Array): Promise<AiraArchive>;
|
|
113
|
+
has(path: string): boolean;
|
|
114
|
+
summary(): AiraSummary;
|
|
115
|
+
readBytes(path: string): Promise<Uint8Array>;
|
|
116
|
+
readText(path: string): Promise<string>;
|
|
117
|
+
readJson<T = unknown>(path: string): Promise<T>;
|
|
118
|
+
validate(): Promise<AiraValidationResult>;
|
|
119
|
+
private validateProtocol;
|
|
120
|
+
private validateProtocolList;
|
|
121
|
+
private validateRecords;
|
|
122
|
+
private validateBlobs;
|
|
123
|
+
private validateFileReferences;
|
|
124
|
+
}
|
|
125
|
+
export declare function openAiraArchive(input: ArrayBuffer | Blob | Uint8Array): Promise<AiraArchive>;
|
|
126
|
+
export declare function readAiraArchiveSummary(input: ArrayBuffer | Blob | Uint8Array): Promise<AiraSummary>;
|
|
127
|
+
export declare function prettyPrintJson(value: unknown): string;
|
|
128
|
+
export declare function encodeUtf8(value: string): Uint8Array;
|
|
129
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,83 +1,248 @@
|
|
|
1
|
-
const
|
|
2
|
-
function
|
|
3
|
-
return
|
|
1
|
+
const m = "_airalogy_archive/manifest.json", A = "airalogy.archive", B = "airalogy.record", Q = 1, E = new TextDecoder("utf-8"), w = new TextEncoder(), M = /^[0-9a-f]{64}$/, O = 0, L = 2048, j = 4294967295;
|
|
2
|
+
function d(e, t) {
|
|
3
|
+
return e.getUint16(t, !0);
|
|
4
4
|
}
|
|
5
|
-
function p(
|
|
6
|
-
return
|
|
5
|
+
function p(e, t) {
|
|
6
|
+
return e.getUint32(t, !0);
|
|
7
7
|
}
|
|
8
|
-
function
|
|
9
|
-
return
|
|
8
|
+
function H(e) {
|
|
9
|
+
return E.decode(e);
|
|
10
10
|
}
|
|
11
|
-
function v(
|
|
12
|
-
return !
|
|
11
|
+
function v(e) {
|
|
12
|
+
return !e || e.startsWith("/") ? `Archive member '${e}' uses an absolute or empty path.` : e.split("/").some((t) => t === "..") ? `Archive member '${e}' escapes the archive root.` : null;
|
|
13
13
|
}
|
|
14
|
-
function
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
function P(e, t) {
|
|
15
|
+
const n = e.replace(/\\/g, "/").split("/").filter((r) => r && r !== ".").join("/"), i = v(n);
|
|
16
|
+
if (i || e.startsWith("/") || e.replace(/\\/g, "/").split("/").some((r) => r === ".."))
|
|
17
|
+
throw new Error(i ?? `${t} '${e}' is not a safe relative archive path.`);
|
|
18
|
+
if (n === m || n.startsWith("_airalogy_archive/"))
|
|
19
|
+
throw new Error(`${t} '${e}' conflicts with Airalogy archive metadata.`);
|
|
20
|
+
return n;
|
|
21
|
+
}
|
|
22
|
+
function y(e) {
|
|
23
|
+
return !!e && typeof e == "object" && !Array.isArray(e);
|
|
24
|
+
}
|
|
25
|
+
function N(e, t) {
|
|
26
|
+
const n = [];
|
|
27
|
+
if (!y(e))
|
|
28
|
+
return [`${t} must be a JSON object.`];
|
|
29
|
+
e.format !== void 0 && e.format !== B && n.push(`${t} format must be '${B}' when present.`), e.schema_version !== void 0 && e.schema_version !== 1 && n.push(`${t} schema_version must be 1 when present.`), e.record_id !== void 0 && e.record_id !== null && (typeof e.record_id != "string" || e.record_id.length === 0) && n.push(`${t} record_id must be a non-empty string when present.`), e.airalogy_record_id !== void 0 && e.airalogy_record_id !== null && (typeof e.airalogy_record_id != "string" || e.airalogy_record_id.length === 0) && n.push(`${t} airalogy_record_id must be a non-empty string when present.`);
|
|
30
|
+
const i = e.record_version;
|
|
31
|
+
if (i != null && (typeof i != "number" || !Number.isInteger(i) || i < 1) && n.push(`${t} record_version must be a positive integer when present.`), e.metadata !== void 0 && e.metadata !== null && !y(e.metadata) && n.push(`${t} metadata must be an object when present.`), !y(e.data))
|
|
32
|
+
return n.push(`${t} data must be an object.`), n;
|
|
33
|
+
y(e.data.var) || n.push(`${t} data.var must be an object.`);
|
|
34
|
+
for (const r of ["step", "check", "quiz"]) {
|
|
35
|
+
const s = e.data[r];
|
|
36
|
+
s != null && !y(s) && n.push(`${t} data.${r} must be an object when present.`);
|
|
37
|
+
}
|
|
38
|
+
if (e.files !== void 0)
|
|
39
|
+
if (!Array.isArray(e.files))
|
|
40
|
+
n.push(`${t} files must be a list when present.`);
|
|
41
|
+
else
|
|
42
|
+
for (const [r, s] of e.files.entries()) {
|
|
43
|
+
if (!y(s)) {
|
|
44
|
+
n.push(`${t} files[${r + 1}] must be an object.`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
["file_id", "source_uri", "blob_id"].some((o) => typeof s[o] == "string" && s[o]) || n.push(`${t} files[${r + 1}] must include file_id, source_uri, or blob_id.`);
|
|
48
|
+
}
|
|
49
|
+
return n;
|
|
50
|
+
}
|
|
51
|
+
function T(e) {
|
|
52
|
+
const t = Math.max(0, e.length - 65535 - 22);
|
|
53
|
+
for (let n = e.length - 22; n >= t; n -= 1)
|
|
54
|
+
if (e[n] === 80 && e[n + 1] === 75 && e[n + 2] === 5 && e[n + 3] === 6)
|
|
55
|
+
return n;
|
|
19
56
|
throw new Error("Archive is not a valid zip file: EOCD not found.");
|
|
20
57
|
}
|
|
21
|
-
function
|
|
22
|
-
const t = new DataView(
|
|
23
|
-
let
|
|
24
|
-
for (let
|
|
25
|
-
if (p(t,
|
|
26
|
-
throw new Error(`Archive central directory is invalid at offset ${
|
|
27
|
-
const
|
|
28
|
-
if (p(t,
|
|
29
|
-
throw new Error(`Archive local header is invalid for '${
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
name:
|
|
33
|
-
compressionMethod:
|
|
34
|
-
compressedSize:
|
|
35
|
-
uncompressedSize:
|
|
36
|
-
localHeaderOffset:
|
|
37
|
-
compressedDataStart:
|
|
38
|
-
}),
|
|
58
|
+
function C(e) {
|
|
59
|
+
const t = new DataView(e.buffer, e.byteOffset, e.byteLength), n = T(e), i = d(t, n + 10), r = p(t, n + 16), s = [];
|
|
60
|
+
let o = r;
|
|
61
|
+
for (let a = 0; a < i; a += 1) {
|
|
62
|
+
if (p(t, o) !== 33639248)
|
|
63
|
+
throw new Error(`Archive central directory is invalid at offset ${o}.`);
|
|
64
|
+
const c = d(t, o + 10), f = p(t, o + 20), h = p(t, o + 24), l = d(t, o + 28), u = d(t, o + 30), $ = d(t, o + 32), g = p(t, o + 42), S = o + 46, R = H(e.slice(S, S + l));
|
|
65
|
+
if (p(t, g) !== 67324752)
|
|
66
|
+
throw new Error(`Archive local header is invalid for '${R}'.`);
|
|
67
|
+
const D = d(t, g + 26), x = d(t, g + 28), I = g + 30 + D + x;
|
|
68
|
+
s.push({
|
|
69
|
+
name: R,
|
|
70
|
+
compressionMethod: c,
|
|
71
|
+
compressedSize: f,
|
|
72
|
+
uncompressedSize: h,
|
|
73
|
+
localHeaderOffset: g,
|
|
74
|
+
compressedDataStart: I
|
|
75
|
+
}), o = S + l + u + $;
|
|
39
76
|
}
|
|
40
|
-
return
|
|
77
|
+
return s.filter((a) => !a.name.endsWith("/"));
|
|
41
78
|
}
|
|
42
|
-
async function
|
|
79
|
+
async function k(e) {
|
|
43
80
|
const t = globalThis.DecompressionStream;
|
|
44
81
|
if (!t)
|
|
45
82
|
throw new Error("This browser does not support DecompressionStream.");
|
|
46
|
-
const
|
|
47
|
-
return new Uint8Array(
|
|
83
|
+
const n = new Blob([e]).stream().pipeThrough(new t("deflate-raw")), i = await new Response(n).arrayBuffer();
|
|
84
|
+
return new Uint8Array(i);
|
|
85
|
+
}
|
|
86
|
+
async function U(e) {
|
|
87
|
+
const t = await crypto.subtle.digest("SHA-256", e);
|
|
88
|
+
return Array.from(new Uint8Array(t)).map((n) => n.toString(16).padStart(2, "0")).join("");
|
|
89
|
+
}
|
|
90
|
+
function z() {
|
|
91
|
+
const e = new Uint32Array(256);
|
|
92
|
+
for (let t = 0; t < 256; t += 1) {
|
|
93
|
+
let n = t;
|
|
94
|
+
for (let i = 0; i < 8; i += 1)
|
|
95
|
+
n = n & 1 ? 3988292384 ^ n >>> 1 : n >>> 1;
|
|
96
|
+
e[t] = n >>> 0;
|
|
97
|
+
}
|
|
98
|
+
return e;
|
|
99
|
+
}
|
|
100
|
+
const F = z();
|
|
101
|
+
function V(e) {
|
|
102
|
+
let t = 4294967295;
|
|
103
|
+
for (const n of e)
|
|
104
|
+
t = F[(t ^ n) & 255] ^ t >>> 8;
|
|
105
|
+
return (t ^ 4294967295) >>> 0;
|
|
106
|
+
}
|
|
107
|
+
function Z(e) {
|
|
108
|
+
const t = e.reduce((r, s) => r + s.byteLength, 0), n = new Uint8Array(t);
|
|
109
|
+
let i = 0;
|
|
110
|
+
for (const r of e)
|
|
111
|
+
n.set(r, i), i += r.byteLength;
|
|
112
|
+
return n;
|
|
113
|
+
}
|
|
114
|
+
function J(e) {
|
|
115
|
+
const t = Math.max(1980, Math.min(2107, e.getFullYear())), n = e.getMonth() + 1, i = e.getDate(), r = e.getHours(), s = e.getMinutes(), o = Math.floor(e.getSeconds() / 2);
|
|
116
|
+
return {
|
|
117
|
+
time: r << 11 | s << 5 | o,
|
|
118
|
+
date: t - 1980 << 9 | n << 5 | i
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function b(e, t) {
|
|
122
|
+
if (!Number.isInteger(e) || e < 0 || e > j)
|
|
123
|
+
throw new Error(`${t} exceeds the ZIP32 size limit.`);
|
|
124
|
+
}
|
|
125
|
+
function W(e, t) {
|
|
126
|
+
const n = w.encode(e.name);
|
|
127
|
+
b(e.bytes.byteLength, `Archive member '${e.name}'`);
|
|
128
|
+
const i = new Uint8Array(30 + n.byteLength), r = new DataView(i.buffer, i.byteOffset, i.byteLength);
|
|
129
|
+
return r.setUint32(0, 67324752, !0), r.setUint16(4, 20, !0), r.setUint16(6, L, !0), r.setUint16(8, O, !0), r.setUint16(10, t.time, !0), r.setUint16(12, t.date, !0), r.setUint32(14, e.crc32, !0), r.setUint32(18, e.bytes.byteLength, !0), r.setUint32(22, e.bytes.byteLength, !0), r.setUint16(26, n.byteLength, !0), r.setUint16(28, 0, !0), i.set(n, 30), i;
|
|
130
|
+
}
|
|
131
|
+
function G(e, t) {
|
|
132
|
+
const n = w.encode(e.name), i = new Uint8Array(46 + n.byteLength), r = new DataView(i.buffer, i.byteOffset, i.byteLength);
|
|
133
|
+
return r.setUint32(0, 33639248, !0), r.setUint16(4, 20, !0), r.setUint16(6, 20, !0), r.setUint16(8, L, !0), r.setUint16(10, O, !0), r.setUint16(12, t.time, !0), r.setUint16(14, t.date, !0), r.setUint32(16, e.crc32, !0), r.setUint32(20, e.bytes.byteLength, !0), r.setUint32(24, e.bytes.byteLength, !0), r.setUint16(28, n.byteLength, !0), r.setUint16(30, 0, !0), r.setUint16(32, 0, !0), r.setUint16(34, 0, !0), r.setUint16(36, 0, !0), r.setUint32(38, 0, !0), r.setUint32(42, e.localHeaderOffset, !0), i.set(n, 46), i;
|
|
134
|
+
}
|
|
135
|
+
function Y(e, t, n) {
|
|
136
|
+
if (b(e, "Archive entry count"), b(t, "Central directory"), b(n, "Central directory offset"), e > 65535)
|
|
137
|
+
throw new Error("Archive entry count exceeds the ZIP32 entry limit.");
|
|
138
|
+
const i = new Uint8Array(22), r = new DataView(i.buffer);
|
|
139
|
+
return r.setUint32(0, 101010256, !0), r.setUint16(4, 0, !0), r.setUint16(6, 0, !0), r.setUint16(8, e, !0), r.setUint16(10, e, !0), r.setUint32(12, t, !0), r.setUint32(16, n, !0), r.setUint16(20, 0, !0), i;
|
|
140
|
+
}
|
|
141
|
+
function q(e, t = /* @__PURE__ */ new Date()) {
|
|
142
|
+
const n = J(t), i = [], r = [], s = [];
|
|
143
|
+
let o = 0;
|
|
144
|
+
for (const f of e) {
|
|
145
|
+
const h = {
|
|
146
|
+
...f,
|
|
147
|
+
crc32: V(f.bytes),
|
|
148
|
+
localHeaderOffset: o
|
|
149
|
+
}, l = W(h, n);
|
|
150
|
+
r.push(l, h.bytes), o += l.byteLength + h.bytes.byteLength, b(o, "Archive size"), i.push(h);
|
|
151
|
+
}
|
|
152
|
+
const a = o;
|
|
153
|
+
for (const f of i) {
|
|
154
|
+
const h = G(f, n);
|
|
155
|
+
s.push(h), o += h.byteLength, b(o, "Archive size");
|
|
156
|
+
}
|
|
157
|
+
const c = o - a;
|
|
158
|
+
return Z([
|
|
159
|
+
...r,
|
|
160
|
+
...s,
|
|
161
|
+
Y(i.length, c, a)
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
async function X(e) {
|
|
165
|
+
if (typeof e == "string")
|
|
166
|
+
return w.encode(e);
|
|
167
|
+
if (typeof Blob < "u" && e instanceof Blob)
|
|
168
|
+
return new Uint8Array(await e.arrayBuffer());
|
|
169
|
+
if (e instanceof ArrayBuffer)
|
|
170
|
+
return new Uint8Array(e);
|
|
171
|
+
if (ArrayBuffer.isView(e))
|
|
172
|
+
return new Uint8Array(e.buffer, e.byteOffset, e.byteLength);
|
|
173
|
+
throw new Error("Archive entry data must be a string, Blob, ArrayBuffer, or ArrayBufferView.");
|
|
48
174
|
}
|
|
49
|
-
|
|
50
|
-
const t
|
|
51
|
-
|
|
175
|
+
function K(e) {
|
|
176
|
+
for (const t of e.split(/\r?\n/)) {
|
|
177
|
+
const n = t.trim();
|
|
178
|
+
if (n.startsWith("# "))
|
|
179
|
+
return n.slice(2).trim() || null;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
async function tt(e) {
|
|
184
|
+
const t = P(e.protocol?.entrypoint ?? "protocol.aimd", "Protocol entrypoint"), n = /* @__PURE__ */ new Map();
|
|
185
|
+
n.set(t, w.encode(e.aimd));
|
|
186
|
+
for (const a of e.files ?? []) {
|
|
187
|
+
const c = P(a.path, "Protocol file");
|
|
188
|
+
if (c === t || n.has(c))
|
|
189
|
+
throw new Error(`Protocol archive contains duplicate file path '${c}'.`);
|
|
190
|
+
n.set(c, await X(a.data));
|
|
191
|
+
}
|
|
192
|
+
const i = Array.from(n.keys()), r = {};
|
|
193
|
+
for (const [a, c] of n)
|
|
194
|
+
r[a] = await U(c);
|
|
195
|
+
const s = {
|
|
196
|
+
format: A,
|
|
197
|
+
version: 1,
|
|
198
|
+
kind: "protocol",
|
|
199
|
+
created_at: e.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
200
|
+
protocol: {
|
|
201
|
+
protocol_id: e.protocol?.protocol_id ?? null,
|
|
202
|
+
protocol_version: e.protocol?.protocol_version ?? null,
|
|
203
|
+
protocol_name: e.protocol?.protocol_name ?? K(e.aimd) ?? null,
|
|
204
|
+
entrypoint: t,
|
|
205
|
+
files: i,
|
|
206
|
+
file_hashes: r
|
|
207
|
+
}
|
|
208
|
+
}, o = [
|
|
209
|
+
{
|
|
210
|
+
name: m,
|
|
211
|
+
bytes: w.encode(`${JSON.stringify(s, null, 2)}
|
|
212
|
+
`)
|
|
213
|
+
},
|
|
214
|
+
...i.map((a) => ({ name: a, bytes: n.get(a) }))
|
|
215
|
+
];
|
|
216
|
+
return q(o);
|
|
52
217
|
}
|
|
53
|
-
class
|
|
218
|
+
class _ {
|
|
54
219
|
bytes;
|
|
55
220
|
entries;
|
|
56
221
|
manifest;
|
|
57
222
|
entryMap;
|
|
58
|
-
constructor(t,
|
|
59
|
-
this.bytes = t, this.entries =
|
|
223
|
+
constructor(t, n, i) {
|
|
224
|
+
this.bytes = t, this.entries = n.map(({ compressedDataStart: r, ...s }) => s), this.entryMap = new Map(n.map((r) => [r.name, r])), this.manifest = i;
|
|
60
225
|
}
|
|
61
226
|
static async open(t) {
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
64
|
-
throw new Error(`Archive does not contain '${
|
|
65
|
-
const
|
|
66
|
-
format:
|
|
227
|
+
const n = t instanceof Uint8Array ? t : t instanceof Blob ? new Uint8Array(await t.arrayBuffer()) : new Uint8Array(t), i = C(n);
|
|
228
|
+
if (!i.find((a) => a.name === m))
|
|
229
|
+
throw new Error(`Archive does not contain '${m}'.`);
|
|
230
|
+
const o = await new _(n, i, {
|
|
231
|
+
format: A,
|
|
67
232
|
version: 0,
|
|
68
233
|
kind: "records"
|
|
69
|
-
}).readJson(
|
|
70
|
-
if (
|
|
71
|
-
throw new Error(`Unsupported archive format '${String(
|
|
72
|
-
if (
|
|
73
|
-
throw new Error(`Unsupported archive kind '${String(
|
|
74
|
-
return new
|
|
234
|
+
}).readJson(m);
|
|
235
|
+
if (o.format !== A)
|
|
236
|
+
throw new Error(`Unsupported archive format '${String(o.format)}'.`);
|
|
237
|
+
if (o.kind !== "protocol" && o.kind !== "protocols" && o.kind !== "records")
|
|
238
|
+
throw new Error(`Unsupported archive kind '${String(o.kind)}'.`);
|
|
239
|
+
return new _(n, i, o);
|
|
75
240
|
}
|
|
76
241
|
has(t) {
|
|
77
242
|
return this.entryMap.has(t);
|
|
78
243
|
}
|
|
79
244
|
summary() {
|
|
80
|
-
const t = Array.isArray(this.manifest.records) ? this.manifest.records.length : 0,
|
|
245
|
+
const t = Array.isArray(this.manifest.records) ? this.manifest.records.length : 0, n = this.manifest.kind === "protocol" ? 1 : Array.isArray(this.manifest.protocols) ? this.manifest.protocols.length : 0, i = Array.isArray(this.manifest.blobs) ? this.manifest.blobs.length : 0, r = Array.isArray(this.manifest.files) ? this.manifest.files.length : 0;
|
|
81
246
|
return {
|
|
82
247
|
format: this.manifest.format,
|
|
83
248
|
version: this.manifest.version,
|
|
@@ -85,207 +250,210 @@ class y {
|
|
|
85
250
|
createdAt: this.manifest.created_at,
|
|
86
251
|
memberCount: this.entries.length,
|
|
87
252
|
recordCount: t,
|
|
88
|
-
protocolCount:
|
|
89
|
-
blobCount:
|
|
90
|
-
fileCount:
|
|
253
|
+
protocolCount: n,
|
|
254
|
+
blobCount: i,
|
|
255
|
+
fileCount: r
|
|
91
256
|
};
|
|
92
257
|
}
|
|
93
258
|
async readBytes(t) {
|
|
94
|
-
const
|
|
95
|
-
if (!
|
|
259
|
+
const n = this.entryMap.get(t);
|
|
260
|
+
if (!n)
|
|
96
261
|
throw new Error(`Archive member '${t}' not found.`);
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
262
|
+
const i = this.bytes.slice(
|
|
263
|
+
n.compressedDataStart,
|
|
264
|
+
n.compressedDataStart + n.compressedSize
|
|
100
265
|
);
|
|
101
|
-
if (
|
|
102
|
-
return
|
|
103
|
-
if (
|
|
104
|
-
return
|
|
105
|
-
throw new Error(`Archive member '${t}' uses unsupported compression method ${
|
|
266
|
+
if (n.compressionMethod === 0)
|
|
267
|
+
return i;
|
|
268
|
+
if (n.compressionMethod === 8)
|
|
269
|
+
return k(i);
|
|
270
|
+
throw new Error(`Archive member '${t}' uses unsupported compression method ${n.compressionMethod}.`);
|
|
106
271
|
}
|
|
107
272
|
async readText(t) {
|
|
108
|
-
return
|
|
273
|
+
return E.decode(await this.readBytes(t));
|
|
109
274
|
}
|
|
110
275
|
async readJson(t) {
|
|
111
276
|
return JSON.parse(await this.readText(t));
|
|
112
277
|
}
|
|
113
278
|
async validate() {
|
|
114
|
-
const t = [],
|
|
115
|
-
for (const
|
|
116
|
-
const
|
|
117
|
-
|
|
279
|
+
const t = [], n = new Set(this.entries.map((i) => i.name));
|
|
280
|
+
for (const i of this.entries) {
|
|
281
|
+
const r = v(i.name);
|
|
282
|
+
r && t.push(r);
|
|
118
283
|
}
|
|
119
|
-
return
|
|
284
|
+
return n.has(m) || t.push(`Archive is missing '${m}'.`), this.manifest.format !== A && t.push(`Unsupported archive format '${String(this.manifest.format)}'.`), this.manifest.version !== 1 && t.push(`Unsupported archive version '${String(this.manifest.version)}'.`), this.manifest.kind === "protocol" ? await this.validateProtocol(this.manifest.protocol, "", t) : this.manifest.kind === "protocols" ? await this.validateProtocolList(this.manifest.protocols, t, !0) : this.manifest.kind === "records" ? await this.validateRecords(t) : t.push(`Unsupported archive kind '${String(this.manifest.kind)}'.`), { ok: t.length === 0, issues: t };
|
|
120
285
|
}
|
|
121
|
-
async validateProtocol(t,
|
|
286
|
+
async validateProtocol(t, n, i) {
|
|
122
287
|
if (!t || typeof t != "object") {
|
|
123
|
-
|
|
288
|
+
i.push("Protocol manifest entry must be an object.");
|
|
124
289
|
return;
|
|
125
290
|
}
|
|
126
|
-
const
|
|
127
|
-
this.has(
|
|
128
|
-
const
|
|
129
|
-
for (const
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
|
|
291
|
+
const r = t.entrypoint || "protocol.aimd", s = `${n}${r}`;
|
|
292
|
+
this.has(s) || i.push(`Protocol entrypoint '${s}' is missing.`);
|
|
293
|
+
const o = Array.isArray(t.files) ? t.files : [], a = t.file_hashes && typeof t.file_hashes == "object" ? t.file_hashes : {};
|
|
294
|
+
for (const c of o) {
|
|
295
|
+
const f = `${n}${c}`, h = v(f);
|
|
296
|
+
if (h) {
|
|
297
|
+
i.push(h);
|
|
133
298
|
continue;
|
|
134
299
|
}
|
|
135
|
-
if (!this.has(
|
|
136
|
-
|
|
300
|
+
if (!this.has(f)) {
|
|
301
|
+
i.push(`Protocol file '${f}' is missing.`);
|
|
137
302
|
continue;
|
|
138
303
|
}
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
141
|
-
const
|
|
142
|
-
|
|
304
|
+
const l = a[c];
|
|
305
|
+
if (l) {
|
|
306
|
+
const u = await U(await this.readBytes(f));
|
|
307
|
+
u !== l && i.push(`Protocol file '${f}' sha256 mismatch: expected ${l}, got ${u}.`);
|
|
143
308
|
}
|
|
144
309
|
}
|
|
145
310
|
}
|
|
146
|
-
async validateProtocolList(t,
|
|
147
|
-
const
|
|
311
|
+
async validateProtocolList(t, n, i = !1) {
|
|
312
|
+
const r = /* @__PURE__ */ new Set();
|
|
148
313
|
if (!Array.isArray(t))
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
for (const [
|
|
152
|
-
if (!
|
|
153
|
-
|
|
314
|
+
return n.push("Protocols manifest field must be a list."), r;
|
|
315
|
+
i && t.length === 0 && n.push("Protocols manifest field must include at least one protocol.");
|
|
316
|
+
for (const [s, o] of t.entries()) {
|
|
317
|
+
if (!o || typeof o != "object") {
|
|
318
|
+
n.push(`Protocol manifest entry #${s + 1} must be an object.`);
|
|
154
319
|
continue;
|
|
155
320
|
}
|
|
156
|
-
if (!
|
|
157
|
-
|
|
321
|
+
if (!o.archive_root) {
|
|
322
|
+
n.push(`Protocol manifest entry #${s + 1} is missing archive_root.`);
|
|
158
323
|
continue;
|
|
159
324
|
}
|
|
160
|
-
if (
|
|
161
|
-
|
|
325
|
+
if (r.has(o.archive_root)) {
|
|
326
|
+
n.push(`Protocol manifest entry #${s + 1} uses duplicate archive_root '${o.archive_root}'.`);
|
|
162
327
|
continue;
|
|
163
328
|
}
|
|
164
|
-
|
|
329
|
+
r.add(o.archive_root), await this.validateProtocol(o, `${o.archive_root.replace(/\/+$/, "")}/`, n);
|
|
165
330
|
}
|
|
166
|
-
return
|
|
331
|
+
return r;
|
|
167
332
|
}
|
|
168
333
|
async validateRecords(t) {
|
|
169
|
-
const
|
|
170
|
-
for (const [
|
|
171
|
-
if (!
|
|
172
|
-
t.push(`Record manifest entry #${
|
|
334
|
+
const n = Array.isArray(this.manifest.records) ? this.manifest.records : [], i = await this.validateProtocolList(this.manifest.protocols, t), r = /* @__PURE__ */ new Set();
|
|
335
|
+
for (const [o, a] of n.entries()) {
|
|
336
|
+
if (!a || typeof a != "object") {
|
|
337
|
+
t.push(`Record manifest entry #${o + 1} must be an object.`);
|
|
173
338
|
continue;
|
|
174
339
|
}
|
|
175
|
-
const
|
|
176
|
-
if (!
|
|
177
|
-
t.push(`Record manifest entry #${
|
|
340
|
+
const c = a.path;
|
|
341
|
+
if (!c) {
|
|
342
|
+
t.push(`Record manifest entry #${o + 1} is missing a path.`);
|
|
178
343
|
continue;
|
|
179
344
|
}
|
|
180
|
-
const
|
|
181
|
-
if (
|
|
182
|
-
t.push(
|
|
345
|
+
const f = v(c);
|
|
346
|
+
if (f) {
|
|
347
|
+
t.push(f);
|
|
183
348
|
continue;
|
|
184
349
|
}
|
|
185
|
-
if (!this.has(
|
|
186
|
-
t.push(`Record file '${
|
|
350
|
+
if (!this.has(c)) {
|
|
351
|
+
t.push(`Record file '${c}' is missing.`);
|
|
187
352
|
continue;
|
|
188
353
|
}
|
|
189
|
-
|
|
190
|
-
let l;
|
|
354
|
+
r.add(c);
|
|
355
|
+
let h, l;
|
|
191
356
|
try {
|
|
192
|
-
|
|
357
|
+
h = await this.readBytes(c), l = JSON.parse(E.decode(h));
|
|
193
358
|
} catch {
|
|
194
|
-
t.push(`Record file '${
|
|
359
|
+
t.push(`Record file '${c}' is not valid UTF-8 JSON.`);
|
|
195
360
|
continue;
|
|
196
361
|
}
|
|
197
|
-
if (
|
|
198
|
-
const
|
|
199
|
-
|
|
362
|
+
if (t.push(...N(l, `Record file '${c}'`)), a.sha256) {
|
|
363
|
+
const u = await U(h);
|
|
364
|
+
u !== a.sha256 && t.push(`Record file '${c}' sha256 mismatch: expected ${a.sha256}, got ${u}.`);
|
|
200
365
|
}
|
|
201
|
-
|
|
366
|
+
a.embedded_protocol_root && !i.has(a.embedded_protocol_root) && t.push(`Record file '${c}' references missing embedded protocol root '${a.embedded_protocol_root}'.`);
|
|
202
367
|
}
|
|
203
|
-
const
|
|
204
|
-
this.validateFileReferences(t,
|
|
368
|
+
const s = await this.validateBlobs(t);
|
|
369
|
+
this.validateFileReferences(t, s, r);
|
|
205
370
|
}
|
|
206
371
|
async validateBlobs(t) {
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
209
|
-
return
|
|
210
|
-
if (!Array.isArray(
|
|
211
|
-
return t.push("Blobs manifest field must be a list."),
|
|
212
|
-
const
|
|
213
|
-
for (const [
|
|
214
|
-
if (!
|
|
215
|
-
t.push(`Blob manifest entry #${
|
|
372
|
+
const n = /* @__PURE__ */ new Set(), i = this.manifest.blobs;
|
|
373
|
+
if (i === void 0)
|
|
374
|
+
return n;
|
|
375
|
+
if (!Array.isArray(i))
|
|
376
|
+
return t.push("Blobs manifest field must be a list."), n;
|
|
377
|
+
const r = /* @__PURE__ */ new Set();
|
|
378
|
+
for (const [s, o] of i.entries()) {
|
|
379
|
+
if (!o || typeof o != "object") {
|
|
380
|
+
t.push(`Blob manifest entry #${s + 1} must be an object.`);
|
|
216
381
|
continue;
|
|
217
382
|
}
|
|
218
|
-
const
|
|
219
|
-
if (!
|
|
220
|
-
t.push(`Blob manifest entry #${
|
|
383
|
+
const a = o.blob_id, c = o.archive_path, f = o.sha256;
|
|
384
|
+
if (!a) {
|
|
385
|
+
t.push(`Blob manifest entry #${s + 1} is missing blob_id.`);
|
|
221
386
|
continue;
|
|
222
387
|
}
|
|
223
|
-
if (
|
|
224
|
-
t.push(`Blob manifest entry #${
|
|
388
|
+
if (n.has(a)) {
|
|
389
|
+
t.push(`Blob manifest entry #${s + 1} uses duplicate blob_id '${a}'.`);
|
|
225
390
|
continue;
|
|
226
391
|
}
|
|
227
|
-
if (
|
|
228
|
-
t.push(`Blob '${
|
|
392
|
+
if (n.add(a), !f || !M.test(f)) {
|
|
393
|
+
t.push(`Blob '${a}' must include a valid sha256 hash.`);
|
|
229
394
|
continue;
|
|
230
395
|
}
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
233
|
-
t.push(`Blob '${
|
|
396
|
+
const h = `sha256:${f}`;
|
|
397
|
+
if (a !== h && t.push(`Blob '${a}' does not match sha256-derived id '${h}'.`), !c) {
|
|
398
|
+
t.push(`Blob '${a}' is missing archive_path.`);
|
|
234
399
|
continue;
|
|
235
400
|
}
|
|
236
|
-
const
|
|
237
|
-
if (
|
|
238
|
-
t.push(
|
|
401
|
+
const l = v(c);
|
|
402
|
+
if (l) {
|
|
403
|
+
t.push(l);
|
|
239
404
|
continue;
|
|
240
405
|
}
|
|
241
|
-
if (
|
|
242
|
-
t.push(`Blob '${
|
|
406
|
+
if (c.startsWith("blobs/sha256/") || t.push(`Blob '${a}' archive_path must be under 'blobs/sha256/'.`), r.has(c)) {
|
|
407
|
+
t.push(`Blob '${a}' uses duplicate archive_path '${c}'.`);
|
|
243
408
|
continue;
|
|
244
409
|
}
|
|
245
|
-
if (
|
|
246
|
-
t.push(`Blob file '${
|
|
410
|
+
if (r.add(c), !this.has(c)) {
|
|
411
|
+
t.push(`Blob file '${c}' is missing.`);
|
|
247
412
|
continue;
|
|
248
413
|
}
|
|
249
|
-
const
|
|
250
|
-
$ !==
|
|
414
|
+
const u = await this.readBytes(c), $ = await U(u);
|
|
415
|
+
$ !== f && t.push(`Blob file '${c}' sha256 mismatch: expected ${f}, got ${$}.`), typeof o.size == "number" && o.size !== u.byteLength ? t.push(`Blob file '${c}' size mismatch: expected ${o.size}, got ${u.byteLength}.`) : o.size !== void 0 && typeof o.size != "number" && t.push(`Blob '${a}' size must be a number when present.`);
|
|
251
416
|
}
|
|
252
|
-
return
|
|
417
|
+
return n;
|
|
253
418
|
}
|
|
254
|
-
validateFileReferences(t,
|
|
255
|
-
const
|
|
256
|
-
if (
|
|
257
|
-
if (!Array.isArray(
|
|
419
|
+
validateFileReferences(t, n, i) {
|
|
420
|
+
const r = this.manifest.files;
|
|
421
|
+
if (r !== void 0) {
|
|
422
|
+
if (!Array.isArray(r)) {
|
|
258
423
|
t.push("Files manifest field must be a list.");
|
|
259
424
|
return;
|
|
260
425
|
}
|
|
261
|
-
for (const [
|
|
262
|
-
if (!
|
|
263
|
-
t.push(`File manifest entry #${
|
|
426
|
+
for (const [s, o] of r.entries()) {
|
|
427
|
+
if (!o || typeof o != "object") {
|
|
428
|
+
t.push(`File manifest entry #${s + 1} must be an object.`);
|
|
264
429
|
continue;
|
|
265
430
|
}
|
|
266
|
-
!
|
|
431
|
+
!o.file_id && !o.source_uri && !o.blob_id && t.push(`File manifest entry #${s + 1} must include file_id, source_uri, or blob_id.`), o.blob_id && !n.has(o.blob_id) && t.push(`File manifest entry #${s + 1} references missing blob_id '${o.blob_id}'.`), o.record_path && !i.has(o.record_path) && t.push(`File manifest entry #${s + 1} references missing record_path '${o.record_path}'.`), o.field_path !== void 0 && typeof o.field_path != "string" && t.push(`File manifest entry #${s + 1} field_path must be a string.`);
|
|
267
432
|
}
|
|
268
433
|
}
|
|
269
434
|
}
|
|
270
435
|
}
|
|
271
|
-
async function
|
|
272
|
-
return
|
|
436
|
+
async function et(e) {
|
|
437
|
+
return _.open(e);
|
|
273
438
|
}
|
|
274
|
-
async function
|
|
275
|
-
return (await
|
|
439
|
+
async function nt(e) {
|
|
440
|
+
return (await _.open(e)).summary();
|
|
276
441
|
}
|
|
277
|
-
function
|
|
278
|
-
return JSON.stringify(
|
|
442
|
+
function rt(e) {
|
|
443
|
+
return JSON.stringify(e, null, 2);
|
|
279
444
|
}
|
|
280
|
-
function
|
|
281
|
-
return
|
|
445
|
+
function it(e) {
|
|
446
|
+
return w.encode(e);
|
|
282
447
|
}
|
|
283
448
|
export {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
449
|
+
B as AIRALOGY_RECORD_FORMAT,
|
|
450
|
+
Q as AIRALOGY_RECORD_SCHEMA_VERSION,
|
|
451
|
+
A as AIRA_ARCHIVE_FORMAT,
|
|
452
|
+
m as AIRA_MANIFEST_PATH,
|
|
453
|
+
_ as AiraArchive,
|
|
454
|
+
tt as createProtocolAiraArchive,
|
|
455
|
+
it as encodeUtf8,
|
|
456
|
+
et as openAiraArchive,
|
|
457
|
+
rt as prettyPrintJson,
|
|
458
|
+
nt as readAiraArchiveSummary
|
|
291
459
|
};
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export const AIRA_MANIFEST_PATH = '_airalogy_archive/manifest.json'
|
|
2
2
|
export const AIRA_ARCHIVE_FORMAT = 'airalogy.archive'
|
|
3
|
+
export const AIRALOGY_RECORD_FORMAT = 'airalogy.record'
|
|
4
|
+
export const AIRALOGY_RECORD_SCHEMA_VERSION = 1
|
|
3
5
|
|
|
4
6
|
export type AiraArchiveKind = 'protocol' | 'protocols' | 'records'
|
|
5
7
|
|
|
@@ -34,6 +36,24 @@ export interface AiraRecordManifest {
|
|
|
34
36
|
embedded_protocol_root?: string | null
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
export interface AiralogyRecordPayload {
|
|
40
|
+
format?: string
|
|
41
|
+
schema_version?: number
|
|
42
|
+
airalogy_record_id?: string | null
|
|
43
|
+
record_id?: string | null
|
|
44
|
+
record_version?: number | null
|
|
45
|
+
metadata?: Record<string, unknown> | null
|
|
46
|
+
data?: {
|
|
47
|
+
var?: Record<string, unknown>
|
|
48
|
+
step?: Record<string, unknown>
|
|
49
|
+
check?: Record<string, unknown>
|
|
50
|
+
quiz?: Record<string, unknown>
|
|
51
|
+
[key: string]: unknown
|
|
52
|
+
}
|
|
53
|
+
files?: Array<Record<string, unknown>>
|
|
54
|
+
[key: string]: unknown
|
|
55
|
+
}
|
|
56
|
+
|
|
37
57
|
export interface AiraBlobManifest {
|
|
38
58
|
blob_id: string
|
|
39
59
|
archive_path: string
|
|
@@ -82,13 +102,40 @@ export interface AiraValidationResult {
|
|
|
82
102
|
issues: string[]
|
|
83
103
|
}
|
|
84
104
|
|
|
105
|
+
export type AiraArchiveEntryData = string | Blob | ArrayBuffer | ArrayBufferView
|
|
106
|
+
|
|
107
|
+
export interface CreateProtocolAiraArchiveFile {
|
|
108
|
+
path: string
|
|
109
|
+
data: AiraArchiveEntryData
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface CreateProtocolAiraArchiveOptions {
|
|
113
|
+
aimd: string
|
|
114
|
+
protocol?: Pick<AiraProtocolManifest, 'protocol_id' | 'protocol_version' | 'protocol_name' | 'entrypoint'>
|
|
115
|
+
files?: CreateProtocolAiraArchiveFile[]
|
|
116
|
+
createdAt?: string
|
|
117
|
+
}
|
|
118
|
+
|
|
85
119
|
type ZipEntryInternal = AiraEntry & {
|
|
86
120
|
compressedDataStart: number
|
|
87
121
|
}
|
|
88
122
|
|
|
123
|
+
type ZipEntryPayload = {
|
|
124
|
+
name: string
|
|
125
|
+
bytes: Uint8Array
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type ZipEntryPrepared = ZipEntryPayload & {
|
|
129
|
+
crc32: number
|
|
130
|
+
localHeaderOffset: number
|
|
131
|
+
}
|
|
132
|
+
|
|
89
133
|
const textDecoder = new TextDecoder('utf-8')
|
|
90
134
|
const textEncoder = new TextEncoder()
|
|
91
135
|
const sha256Pattern = /^[0-9a-f]{64}$/
|
|
136
|
+
const ZIP_STORED_METHOD = 0
|
|
137
|
+
const ZIP_UTF8_FLAG = 0x0800
|
|
138
|
+
const ZIP_UINT32_MAX = 0xffffffff
|
|
92
139
|
|
|
93
140
|
function getUint16(view: DataView, offset: number): number {
|
|
94
141
|
return view.getUint16(offset, true)
|
|
@@ -112,6 +159,104 @@ function validateMemberPath(name: string): string | null {
|
|
|
112
159
|
return null
|
|
113
160
|
}
|
|
114
161
|
|
|
162
|
+
function normalizeArchiveMemberPath(path: string, label: string): string {
|
|
163
|
+
const normalized = path
|
|
164
|
+
.replace(/\\/g, '/')
|
|
165
|
+
.split('/')
|
|
166
|
+
.filter(part => part && part !== '.')
|
|
167
|
+
.join('/')
|
|
168
|
+
const issue = validateMemberPath(normalized)
|
|
169
|
+
if (issue || path.startsWith('/') || path.replace(/\\/g, '/').split('/').some(part => part === '..')) {
|
|
170
|
+
throw new Error(issue ?? `${label} '${path}' is not a safe relative archive path.`)
|
|
171
|
+
}
|
|
172
|
+
if (normalized === AIRA_MANIFEST_PATH || normalized.startsWith('_airalogy_archive/')) {
|
|
173
|
+
throw new Error(`${label} '${path}' conflicts with Airalogy archive metadata.`)
|
|
174
|
+
}
|
|
175
|
+
return normalized
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
179
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function validateRecordPayload(record: unknown, label: string): string[] {
|
|
183
|
+
const issues: string[] = []
|
|
184
|
+
if (!isPlainObject(record)) {
|
|
185
|
+
return [`${label} must be a JSON object.`]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (record.format !== undefined && record.format !== AIRALOGY_RECORD_FORMAT) {
|
|
189
|
+
issues.push(`${label} format must be '${AIRALOGY_RECORD_FORMAT}' when present.`)
|
|
190
|
+
}
|
|
191
|
+
if (
|
|
192
|
+
record.schema_version !== undefined
|
|
193
|
+
&& record.schema_version !== AIRALOGY_RECORD_SCHEMA_VERSION
|
|
194
|
+
) {
|
|
195
|
+
issues.push(`${label} schema_version must be ${AIRALOGY_RECORD_SCHEMA_VERSION} when present.`)
|
|
196
|
+
}
|
|
197
|
+
if (
|
|
198
|
+
record.record_id !== undefined
|
|
199
|
+
&& record.record_id !== null
|
|
200
|
+
&& (typeof record.record_id !== 'string' || record.record_id.length === 0)
|
|
201
|
+
) {
|
|
202
|
+
issues.push(`${label} record_id must be a non-empty string when present.`)
|
|
203
|
+
}
|
|
204
|
+
if (
|
|
205
|
+
record.airalogy_record_id !== undefined
|
|
206
|
+
&& record.airalogy_record_id !== null
|
|
207
|
+
&& (typeof record.airalogy_record_id !== 'string' || record.airalogy_record_id.length === 0)
|
|
208
|
+
) {
|
|
209
|
+
issues.push(`${label} airalogy_record_id must be a non-empty string when present.`)
|
|
210
|
+
}
|
|
211
|
+
const recordVersion = record.record_version
|
|
212
|
+
if (
|
|
213
|
+
recordVersion !== undefined
|
|
214
|
+
&& recordVersion !== null
|
|
215
|
+
&& (typeof recordVersion !== 'number' || !Number.isInteger(recordVersion) || recordVersion < 1)
|
|
216
|
+
) {
|
|
217
|
+
issues.push(`${label} record_version must be a positive integer when present.`)
|
|
218
|
+
}
|
|
219
|
+
if (
|
|
220
|
+
record.metadata !== undefined
|
|
221
|
+
&& record.metadata !== null
|
|
222
|
+
&& !isPlainObject(record.metadata)
|
|
223
|
+
) {
|
|
224
|
+
issues.push(`${label} metadata must be an object when present.`)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!isPlainObject(record.data)) {
|
|
228
|
+
issues.push(`${label} data must be an object.`)
|
|
229
|
+
return issues
|
|
230
|
+
}
|
|
231
|
+
if (!isPlainObject(record.data.var)) {
|
|
232
|
+
issues.push(`${label} data.var must be an object.`)
|
|
233
|
+
}
|
|
234
|
+
for (const section of ['step', 'check', 'quiz']) {
|
|
235
|
+
const value = record.data[section]
|
|
236
|
+
if (value !== undefined && value !== null && !isPlainObject(value)) {
|
|
237
|
+
issues.push(`${label} data.${section} must be an object when present.`)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (record.files !== undefined) {
|
|
242
|
+
if (!Array.isArray(record.files)) {
|
|
243
|
+
issues.push(`${label} files must be a list when present.`)
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
for (const [index, fileRef] of record.files.entries()) {
|
|
247
|
+
if (!isPlainObject(fileRef)) {
|
|
248
|
+
issues.push(`${label} files[${index + 1}] must be an object.`)
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
if (!['file_id', 'source_uri', 'blob_id'].some(key => typeof fileRef[key] === 'string' && fileRef[key])) {
|
|
252
|
+
issues.push(`${label} files[${index + 1}] must include file_id, source_uri, or blob_id.`)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return issues
|
|
258
|
+
}
|
|
259
|
+
|
|
115
260
|
function findEndOfCentralDirectory(bytes: Uint8Array): number {
|
|
116
261
|
const minOffset = Math.max(0, bytes.length - 0xffff - 22)
|
|
117
262
|
for (let offset = bytes.length - 22; offset >= minOffset; offset -= 1) {
|
|
@@ -189,6 +334,229 @@ async function sha256Hex(bytes: Uint8Array): Promise<string> {
|
|
|
189
334
|
.join('')
|
|
190
335
|
}
|
|
191
336
|
|
|
337
|
+
function makeCrc32Table(): Uint32Array {
|
|
338
|
+
const table = new Uint32Array(256)
|
|
339
|
+
for (let index = 0; index < 256; index += 1) {
|
|
340
|
+
let value = index
|
|
341
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
342
|
+
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1
|
|
343
|
+
}
|
|
344
|
+
table[index] = value >>> 0
|
|
345
|
+
}
|
|
346
|
+
return table
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const crc32Table = makeCrc32Table()
|
|
350
|
+
|
|
351
|
+
function crc32(bytes: Uint8Array): number {
|
|
352
|
+
let crc = 0xffffffff
|
|
353
|
+
for (const byte of bytes) {
|
|
354
|
+
crc = crc32Table[(crc ^ byte) & 0xff] ^ (crc >>> 8)
|
|
355
|
+
}
|
|
356
|
+
return (crc ^ 0xffffffff) >>> 0
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function concatBytes(parts: Uint8Array[]): Uint8Array {
|
|
360
|
+
const totalLength = parts.reduce((sum, part) => sum + part.byteLength, 0)
|
|
361
|
+
const output = new Uint8Array(totalLength)
|
|
362
|
+
let offset = 0
|
|
363
|
+
for (const part of parts) {
|
|
364
|
+
output.set(part, offset)
|
|
365
|
+
offset += part.byteLength
|
|
366
|
+
}
|
|
367
|
+
return output
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function dateToDosTimeAndDate(date: Date): { time: number; date: number } {
|
|
371
|
+
const year = Math.max(1980, Math.min(2107, date.getFullYear()))
|
|
372
|
+
const month = date.getMonth() + 1
|
|
373
|
+
const day = date.getDate()
|
|
374
|
+
const hours = date.getHours()
|
|
375
|
+
const minutes = date.getMinutes()
|
|
376
|
+
const seconds = Math.floor(date.getSeconds() / 2)
|
|
377
|
+
return {
|
|
378
|
+
time: (hours << 11) | (minutes << 5) | seconds,
|
|
379
|
+
date: ((year - 1980) << 9) | (month << 5) | day,
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function assertZipUint32(value: number, label: string): void {
|
|
384
|
+
if (!Number.isInteger(value) || value < 0 || value > ZIP_UINT32_MAX) {
|
|
385
|
+
throw new Error(`${label} exceeds the ZIP32 size limit.`)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function writeZipLocalHeader(entry: ZipEntryPrepared, dos: { time: number; date: number }): Uint8Array {
|
|
390
|
+
const nameBytes = textEncoder.encode(entry.name)
|
|
391
|
+
assertZipUint32(entry.bytes.byteLength, `Archive member '${entry.name}'`)
|
|
392
|
+
const header = new Uint8Array(30 + nameBytes.byteLength)
|
|
393
|
+
const view = new DataView(header.buffer, header.byteOffset, header.byteLength)
|
|
394
|
+
view.setUint32(0, 0x04034b50, true)
|
|
395
|
+
view.setUint16(4, 20, true)
|
|
396
|
+
view.setUint16(6, ZIP_UTF8_FLAG, true)
|
|
397
|
+
view.setUint16(8, ZIP_STORED_METHOD, true)
|
|
398
|
+
view.setUint16(10, dos.time, true)
|
|
399
|
+
view.setUint16(12, dos.date, true)
|
|
400
|
+
view.setUint32(14, entry.crc32, true)
|
|
401
|
+
view.setUint32(18, entry.bytes.byteLength, true)
|
|
402
|
+
view.setUint32(22, entry.bytes.byteLength, true)
|
|
403
|
+
view.setUint16(26, nameBytes.byteLength, true)
|
|
404
|
+
view.setUint16(28, 0, true)
|
|
405
|
+
header.set(nameBytes, 30)
|
|
406
|
+
return header
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function writeZipCentralDirectoryEntry(entry: ZipEntryPrepared, dos: { time: number; date: number }): Uint8Array {
|
|
410
|
+
const nameBytes = textEncoder.encode(entry.name)
|
|
411
|
+
const header = new Uint8Array(46 + nameBytes.byteLength)
|
|
412
|
+
const view = new DataView(header.buffer, header.byteOffset, header.byteLength)
|
|
413
|
+
view.setUint32(0, 0x02014b50, true)
|
|
414
|
+
view.setUint16(4, 20, true)
|
|
415
|
+
view.setUint16(6, 20, true)
|
|
416
|
+
view.setUint16(8, ZIP_UTF8_FLAG, true)
|
|
417
|
+
view.setUint16(10, ZIP_STORED_METHOD, true)
|
|
418
|
+
view.setUint16(12, dos.time, true)
|
|
419
|
+
view.setUint16(14, dos.date, true)
|
|
420
|
+
view.setUint32(16, entry.crc32, true)
|
|
421
|
+
view.setUint32(20, entry.bytes.byteLength, true)
|
|
422
|
+
view.setUint32(24, entry.bytes.byteLength, true)
|
|
423
|
+
view.setUint16(28, nameBytes.byteLength, true)
|
|
424
|
+
view.setUint16(30, 0, true)
|
|
425
|
+
view.setUint16(32, 0, true)
|
|
426
|
+
view.setUint16(34, 0, true)
|
|
427
|
+
view.setUint16(36, 0, true)
|
|
428
|
+
view.setUint32(38, 0, true)
|
|
429
|
+
view.setUint32(42, entry.localHeaderOffset, true)
|
|
430
|
+
header.set(nameBytes, 46)
|
|
431
|
+
return header
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function writeZipEndOfCentralDirectory(entryCount: number, centralDirectorySize: number, centralDirectoryOffset: number): Uint8Array {
|
|
435
|
+
assertZipUint32(entryCount, 'Archive entry count')
|
|
436
|
+
assertZipUint32(centralDirectorySize, 'Central directory')
|
|
437
|
+
assertZipUint32(centralDirectoryOffset, 'Central directory offset')
|
|
438
|
+
if (entryCount > 0xffff) {
|
|
439
|
+
throw new Error('Archive entry count exceeds the ZIP32 entry limit.')
|
|
440
|
+
}
|
|
441
|
+
const end = new Uint8Array(22)
|
|
442
|
+
const view = new DataView(end.buffer)
|
|
443
|
+
view.setUint32(0, 0x06054b50, true)
|
|
444
|
+
view.setUint16(4, 0, true)
|
|
445
|
+
view.setUint16(6, 0, true)
|
|
446
|
+
view.setUint16(8, entryCount, true)
|
|
447
|
+
view.setUint16(10, entryCount, true)
|
|
448
|
+
view.setUint32(12, centralDirectorySize, true)
|
|
449
|
+
view.setUint32(16, centralDirectoryOffset, true)
|
|
450
|
+
view.setUint16(20, 0, true)
|
|
451
|
+
return end
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function createStoredZip(entries: ZipEntryPayload[], date = new Date()): Uint8Array {
|
|
455
|
+
const dos = dateToDosTimeAndDate(date)
|
|
456
|
+
const preparedEntries: ZipEntryPrepared[] = []
|
|
457
|
+
const localParts: Uint8Array[] = []
|
|
458
|
+
const centralParts: Uint8Array[] = []
|
|
459
|
+
let offset = 0
|
|
460
|
+
|
|
461
|
+
for (const entry of entries) {
|
|
462
|
+
const prepared: ZipEntryPrepared = {
|
|
463
|
+
...entry,
|
|
464
|
+
crc32: crc32(entry.bytes),
|
|
465
|
+
localHeaderOffset: offset,
|
|
466
|
+
}
|
|
467
|
+
const localHeader = writeZipLocalHeader(prepared, dos)
|
|
468
|
+
localParts.push(localHeader, prepared.bytes)
|
|
469
|
+
offset += localHeader.byteLength + prepared.bytes.byteLength
|
|
470
|
+
assertZipUint32(offset, 'Archive size')
|
|
471
|
+
preparedEntries.push(prepared)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const centralDirectoryOffset = offset
|
|
475
|
+
for (const entry of preparedEntries) {
|
|
476
|
+
const central = writeZipCentralDirectoryEntry(entry, dos)
|
|
477
|
+
centralParts.push(central)
|
|
478
|
+
offset += central.byteLength
|
|
479
|
+
assertZipUint32(offset, 'Archive size')
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const centralDirectorySize = offset - centralDirectoryOffset
|
|
483
|
+
return concatBytes([
|
|
484
|
+
...localParts,
|
|
485
|
+
...centralParts,
|
|
486
|
+
writeZipEndOfCentralDirectory(preparedEntries.length, centralDirectorySize, centralDirectoryOffset),
|
|
487
|
+
])
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function toArchiveBytes(data: AiraArchiveEntryData): Promise<Uint8Array> {
|
|
491
|
+
if (typeof data === 'string') {
|
|
492
|
+
return textEncoder.encode(data)
|
|
493
|
+
}
|
|
494
|
+
if (typeof Blob !== 'undefined' && data instanceof Blob) {
|
|
495
|
+
return new Uint8Array(await data.arrayBuffer())
|
|
496
|
+
}
|
|
497
|
+
if (data instanceof ArrayBuffer) {
|
|
498
|
+
return new Uint8Array(data)
|
|
499
|
+
}
|
|
500
|
+
if (ArrayBuffer.isView(data)) {
|
|
501
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
502
|
+
}
|
|
503
|
+
throw new Error('Archive entry data must be a string, Blob, ArrayBuffer, or ArrayBufferView.')
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function inferProtocolNameFromAimd(aimd: string): string | null {
|
|
507
|
+
for (const line of aimd.split(/\r?\n/)) {
|
|
508
|
+
const trimmed = line.trim()
|
|
509
|
+
if (trimmed.startsWith('# ')) {
|
|
510
|
+
return trimmed.slice(2).trim() || null
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return null
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export async function createProtocolAiraArchive(options: CreateProtocolAiraArchiveOptions): Promise<Uint8Array> {
|
|
517
|
+
const entrypoint = normalizeArchiveMemberPath(options.protocol?.entrypoint ?? 'protocol.aimd', 'Protocol entrypoint')
|
|
518
|
+
const entries = new Map<string, Uint8Array>()
|
|
519
|
+
entries.set(entrypoint, textEncoder.encode(options.aimd))
|
|
520
|
+
|
|
521
|
+
for (const file of options.files ?? []) {
|
|
522
|
+
const path = normalizeArchiveMemberPath(file.path, 'Protocol file')
|
|
523
|
+
if (path === entrypoint || entries.has(path)) {
|
|
524
|
+
throw new Error(`Protocol archive contains duplicate file path '${path}'.`)
|
|
525
|
+
}
|
|
526
|
+
entries.set(path, await toArchiveBytes(file.data))
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const fileNames = Array.from(entries.keys())
|
|
530
|
+
const fileHashes: Record<string, string> = {}
|
|
531
|
+
for (const [path, bytes] of entries) {
|
|
532
|
+
fileHashes[path] = await sha256Hex(bytes)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const manifest: AiraManifest = {
|
|
536
|
+
format: AIRA_ARCHIVE_FORMAT,
|
|
537
|
+
version: 1,
|
|
538
|
+
kind: 'protocol',
|
|
539
|
+
created_at: options.createdAt ?? new Date().toISOString(),
|
|
540
|
+
protocol: {
|
|
541
|
+
protocol_id: options.protocol?.protocol_id ?? null,
|
|
542
|
+
protocol_version: options.protocol?.protocol_version ?? null,
|
|
543
|
+
protocol_name: options.protocol?.protocol_name ?? inferProtocolNameFromAimd(options.aimd) ?? null,
|
|
544
|
+
entrypoint,
|
|
545
|
+
files: fileNames,
|
|
546
|
+
file_hashes: fileHashes,
|
|
547
|
+
},
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const archiveEntries: ZipEntryPayload[] = [
|
|
551
|
+
{
|
|
552
|
+
name: AIRA_MANIFEST_PATH,
|
|
553
|
+
bytes: textEncoder.encode(`${JSON.stringify(manifest, null, 2)}\n`),
|
|
554
|
+
},
|
|
555
|
+
...fileNames.map(name => ({ name, bytes: entries.get(name)! })),
|
|
556
|
+
]
|
|
557
|
+
return createStoredZip(archiveEntries)
|
|
558
|
+
}
|
|
559
|
+
|
|
192
560
|
export class AiraArchive {
|
|
193
561
|
readonly bytes: Uint8Array
|
|
194
562
|
readonly entries: AiraEntry[]
|
|
@@ -407,14 +775,16 @@ export class AiraArchive {
|
|
|
407
775
|
}
|
|
408
776
|
recordPaths.add(path)
|
|
409
777
|
let raw: Uint8Array
|
|
778
|
+
let parsedRecord: unknown
|
|
410
779
|
try {
|
|
411
780
|
raw = await this.readBytes(path)
|
|
412
|
-
JSON.parse(textDecoder.decode(raw))
|
|
781
|
+
parsedRecord = JSON.parse(textDecoder.decode(raw))
|
|
413
782
|
}
|
|
414
783
|
catch {
|
|
415
784
|
issues.push(`Record file '${path}' is not valid UTF-8 JSON.`)
|
|
416
785
|
continue
|
|
417
786
|
}
|
|
787
|
+
issues.push(...validateRecordPayload(parsedRecord, `Record file '${path}'`))
|
|
418
788
|
if (record.sha256) {
|
|
419
789
|
const actualHash = await sha256Hex(raw)
|
|
420
790
|
if (actualHash !== record.sha256) {
|