@airalogy/aira-core 0.1.0 → 0.2.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 +3 -1
- package/dist/index.d.ts +117 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +186 -155
- package/package.json +1 -1
- package/src/index.ts +105 -1
package/README.md
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
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
6
|
|
|
7
7
|
Supported archive kinds are `protocol`, `protocols`, and `records`.
|
|
8
8
|
|
|
9
9
|
Example archives covering these kinds are available in `examples/aira/`.
|
|
10
10
|
|
|
11
|
+
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`.
|
|
12
|
+
|
|
11
13
|
```ts
|
|
12
14
|
import { openAiraArchive } from '@airalogy/aira-core'
|
|
13
15
|
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
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 declare class AiraArchive {
|
|
95
|
+
readonly bytes: Uint8Array;
|
|
96
|
+
readonly entries: AiraEntry[];
|
|
97
|
+
readonly manifest: AiraManifest;
|
|
98
|
+
private readonly entryMap;
|
|
99
|
+
private constructor();
|
|
100
|
+
static open(input: ArrayBuffer | Blob | Uint8Array): Promise<AiraArchive>;
|
|
101
|
+
has(path: string): boolean;
|
|
102
|
+
summary(): AiraSummary;
|
|
103
|
+
readBytes(path: string): Promise<Uint8Array>;
|
|
104
|
+
readText(path: string): Promise<string>;
|
|
105
|
+
readJson<T = unknown>(path: string): Promise<T>;
|
|
106
|
+
validate(): Promise<AiraValidationResult>;
|
|
107
|
+
private validateProtocol;
|
|
108
|
+
private validateProtocolList;
|
|
109
|
+
private validateRecords;
|
|
110
|
+
private validateBlobs;
|
|
111
|
+
private validateFileReferences;
|
|
112
|
+
}
|
|
113
|
+
export declare function openAiraArchive(input: ArrayBuffer | Blob | Uint8Array): Promise<AiraArchive>;
|
|
114
|
+
export declare function readAiraArchiveSummary(input: ArrayBuffer | Blob | Uint8Array): Promise<AiraSummary>;
|
|
115
|
+
export declare function prettyPrintJson(value: unknown): string;
|
|
116
|
+
export declare function encodeUtf8(value: string): Uint8Array;
|
|
117
|
+
//# 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,112 @@
|
|
|
1
|
-
const
|
|
1
|
+
const y = "_airalogy_archive/manifest.json", g = "airalogy.archive", E = "airalogy.record", k = 1, R = new TextDecoder("utf-8"), x = new TextEncoder(), j = /^[0-9a-f]{64}$/;
|
|
2
2
|
function m(i, t) {
|
|
3
3
|
return i.getUint16(t, !0);
|
|
4
4
|
}
|
|
5
|
-
function
|
|
5
|
+
function u(i, t) {
|
|
6
6
|
return i.getUint32(t, !0);
|
|
7
7
|
}
|
|
8
|
-
function
|
|
9
|
-
return
|
|
8
|
+
function I(i) {
|
|
9
|
+
return R.decode(i);
|
|
10
10
|
}
|
|
11
11
|
function v(i) {
|
|
12
12
|
return !i || i.startsWith("/") ? `Archive member '${i}' uses an absolute or empty path.` : i.split("/").some((t) => t === "..") ? `Archive member '${i}' escapes the archive root.` : null;
|
|
13
13
|
}
|
|
14
|
-
function
|
|
14
|
+
function p(i) {
|
|
15
|
+
return !!i && typeof i == "object" && !Array.isArray(i);
|
|
16
|
+
}
|
|
17
|
+
function D(i, t) {
|
|
18
|
+
const e = [];
|
|
19
|
+
if (!p(i))
|
|
20
|
+
return [`${t} must be a JSON object.`];
|
|
21
|
+
i.format !== void 0 && i.format !== E && e.push(`${t} format must be '${E}' when present.`), i.schema_version !== void 0 && i.schema_version !== 1 && e.push(`${t} schema_version must be 1 when present.`), i.record_id !== void 0 && i.record_id !== null && (typeof i.record_id != "string" || i.record_id.length === 0) && e.push(`${t} record_id must be a non-empty string when present.`), i.airalogy_record_id !== void 0 && i.airalogy_record_id !== null && (typeof i.airalogy_record_id != "string" || i.airalogy_record_id.length === 0) && e.push(`${t} airalogy_record_id must be a non-empty string when present.`);
|
|
22
|
+
const o = i.record_version;
|
|
23
|
+
if (o != null && (typeof o != "number" || !Number.isInteger(o) || o < 1) && e.push(`${t} record_version must be a positive integer when present.`), i.metadata !== void 0 && i.metadata !== null && !p(i.metadata) && e.push(`${t} metadata must be an object when present.`), !p(i.data))
|
|
24
|
+
return e.push(`${t} data must be an object.`), e;
|
|
25
|
+
p(i.data.var) || e.push(`${t} data.var must be an object.`);
|
|
26
|
+
for (const s of ["step", "check", "quiz"]) {
|
|
27
|
+
const r = i.data[s];
|
|
28
|
+
r != null && !p(r) && e.push(`${t} data.${s} must be an object when present.`);
|
|
29
|
+
}
|
|
30
|
+
if (i.files !== void 0)
|
|
31
|
+
if (!Array.isArray(i.files))
|
|
32
|
+
e.push(`${t} files must be a list when present.`);
|
|
33
|
+
else
|
|
34
|
+
for (const [s, r] of i.files.entries()) {
|
|
35
|
+
if (!p(r)) {
|
|
36
|
+
e.push(`${t} files[${s + 1}] must be an object.`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
["file_id", "source_uri", "blob_id"].some((n) => typeof r[n] == "string" && r[n]) || e.push(`${t} files[${s + 1}] must include file_id, source_uri, or blob_id.`);
|
|
40
|
+
}
|
|
41
|
+
return e;
|
|
42
|
+
}
|
|
43
|
+
function C(i) {
|
|
15
44
|
const t = Math.max(0, i.length - 65535 - 22);
|
|
16
|
-
for (let
|
|
17
|
-
if (i[
|
|
18
|
-
return
|
|
45
|
+
for (let e = i.length - 22; e >= t; e -= 1)
|
|
46
|
+
if (i[e] === 80 && i[e + 1] === 75 && i[e + 2] === 5 && i[e + 3] === 6)
|
|
47
|
+
return e;
|
|
19
48
|
throw new Error("Archive is not a valid zip file: EOCD not found.");
|
|
20
49
|
}
|
|
21
|
-
function
|
|
22
|
-
const t = new DataView(i.buffer, i.byteOffset, i.byteLength),
|
|
23
|
-
let
|
|
24
|
-
for (let
|
|
25
|
-
if (
|
|
26
|
-
throw new Error(`Archive central directory is invalid at offset ${
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
50
|
+
function M(i) {
|
|
51
|
+
const t = new DataView(i.buffer, i.byteOffset, i.byteLength), e = C(i), o = m(t, e + 10), s = u(t, e + 16), r = [];
|
|
52
|
+
let n = s;
|
|
53
|
+
for (let a = 0; a < o; a += 1) {
|
|
54
|
+
if (u(t, n) !== 33639248)
|
|
55
|
+
throw new Error(`Archive central directory is invalid at offset ${n}.`);
|
|
56
|
+
const c = m(t, n + 10), f = u(t, n + 20), d = u(t, n + 24), h = m(t, n + 28), l = m(t, n + 30), _ = m(t, n + 32), b = u(t, n + 42), w = n + 46, S = I(i.slice(w, w + h));
|
|
57
|
+
if (u(t, b) !== 67324752)
|
|
29
58
|
throw new Error(`Archive local header is invalid for '${S}'.`);
|
|
30
|
-
const B = m(t,
|
|
31
|
-
|
|
59
|
+
const B = m(t, b + 26), O = m(t, b + 28), P = b + 30 + B + O;
|
|
60
|
+
r.push({
|
|
32
61
|
name: S,
|
|
33
|
-
compressionMethod:
|
|
34
|
-
compressedSize:
|
|
35
|
-
uncompressedSize:
|
|
36
|
-
localHeaderOffset:
|
|
37
|
-
compressedDataStart:
|
|
38
|
-
}),
|
|
62
|
+
compressionMethod: c,
|
|
63
|
+
compressedSize: f,
|
|
64
|
+
uncompressedSize: d,
|
|
65
|
+
localHeaderOffset: b,
|
|
66
|
+
compressedDataStart: P
|
|
67
|
+
}), n = w + h + l + _;
|
|
39
68
|
}
|
|
40
|
-
return
|
|
69
|
+
return r.filter((a) => !a.name.endsWith("/"));
|
|
41
70
|
}
|
|
42
|
-
async function
|
|
71
|
+
async function U(i) {
|
|
43
72
|
const t = globalThis.DecompressionStream;
|
|
44
73
|
if (!t)
|
|
45
74
|
throw new Error("This browser does not support DecompressionStream.");
|
|
46
|
-
const
|
|
47
|
-
return new Uint8Array(
|
|
75
|
+
const e = new Blob([i]).stream().pipeThrough(new t("deflate-raw")), o = await new Response(e).arrayBuffer();
|
|
76
|
+
return new Uint8Array(o);
|
|
48
77
|
}
|
|
49
|
-
async function
|
|
78
|
+
async function A(i) {
|
|
50
79
|
const t = await crypto.subtle.digest("SHA-256", i);
|
|
51
|
-
return Array.from(new Uint8Array(t)).map((
|
|
80
|
+
return Array.from(new Uint8Array(t)).map((e) => e.toString(16).padStart(2, "0")).join("");
|
|
52
81
|
}
|
|
53
|
-
class
|
|
82
|
+
class $ {
|
|
54
83
|
bytes;
|
|
55
84
|
entries;
|
|
56
85
|
manifest;
|
|
57
86
|
entryMap;
|
|
58
|
-
constructor(t,
|
|
59
|
-
this.bytes = t, this.entries =
|
|
87
|
+
constructor(t, e, o) {
|
|
88
|
+
this.bytes = t, this.entries = e.map(({ compressedDataStart: s, ...r }) => r), this.entryMap = new Map(e.map((s) => [s.name, s])), this.manifest = o;
|
|
60
89
|
}
|
|
61
90
|
static async open(t) {
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
64
|
-
throw new Error(`Archive does not contain '${
|
|
65
|
-
const
|
|
91
|
+
const e = t instanceof Uint8Array ? t : t instanceof Blob ? new Uint8Array(await t.arrayBuffer()) : new Uint8Array(t), o = M(e);
|
|
92
|
+
if (!o.find((a) => a.name === y))
|
|
93
|
+
throw new Error(`Archive does not contain '${y}'.`);
|
|
94
|
+
const n = await new $(e, o, {
|
|
66
95
|
format: g,
|
|
67
96
|
version: 0,
|
|
68
97
|
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
|
|
98
|
+
}).readJson(y);
|
|
99
|
+
if (n.format !== g)
|
|
100
|
+
throw new Error(`Unsupported archive format '${String(n.format)}'.`);
|
|
101
|
+
if (n.kind !== "protocol" && n.kind !== "protocols" && n.kind !== "records")
|
|
102
|
+
throw new Error(`Unsupported archive kind '${String(n.kind)}'.`);
|
|
103
|
+
return new $(e, o, n);
|
|
75
104
|
}
|
|
76
105
|
has(t) {
|
|
77
106
|
return this.entryMap.has(t);
|
|
78
107
|
}
|
|
79
108
|
summary() {
|
|
80
|
-
const t = Array.isArray(this.manifest.records) ? this.manifest.records.length : 0,
|
|
109
|
+
const t = Array.isArray(this.manifest.records) ? this.manifest.records.length : 0, e = this.manifest.kind === "protocol" ? 1 : Array.isArray(this.manifest.protocols) ? this.manifest.protocols.length : 0, o = Array.isArray(this.manifest.blobs) ? this.manifest.blobs.length : 0, s = Array.isArray(this.manifest.files) ? this.manifest.files.length : 0;
|
|
81
110
|
return {
|
|
82
111
|
format: this.manifest.format,
|
|
83
112
|
version: this.manifest.version,
|
|
@@ -85,207 +114,209 @@ class y {
|
|
|
85
114
|
createdAt: this.manifest.created_at,
|
|
86
115
|
memberCount: this.entries.length,
|
|
87
116
|
recordCount: t,
|
|
88
|
-
protocolCount:
|
|
89
|
-
blobCount:
|
|
117
|
+
protocolCount: e,
|
|
118
|
+
blobCount: o,
|
|
90
119
|
fileCount: s
|
|
91
120
|
};
|
|
92
121
|
}
|
|
93
122
|
async readBytes(t) {
|
|
94
|
-
const
|
|
95
|
-
if (!
|
|
123
|
+
const e = this.entryMap.get(t);
|
|
124
|
+
if (!e)
|
|
96
125
|
throw new Error(`Archive member '${t}' not found.`);
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
126
|
+
const o = this.bytes.slice(
|
|
127
|
+
e.compressedDataStart,
|
|
128
|
+
e.compressedDataStart + e.compressedSize
|
|
100
129
|
);
|
|
101
|
-
if (
|
|
102
|
-
return
|
|
103
|
-
if (
|
|
104
|
-
return
|
|
105
|
-
throw new Error(`Archive member '${t}' uses unsupported compression method ${
|
|
130
|
+
if (e.compressionMethod === 0)
|
|
131
|
+
return o;
|
|
132
|
+
if (e.compressionMethod === 8)
|
|
133
|
+
return U(o);
|
|
134
|
+
throw new Error(`Archive member '${t}' uses unsupported compression method ${e.compressionMethod}.`);
|
|
106
135
|
}
|
|
107
136
|
async readText(t) {
|
|
108
|
-
return
|
|
137
|
+
return R.decode(await this.readBytes(t));
|
|
109
138
|
}
|
|
110
139
|
async readJson(t) {
|
|
111
140
|
return JSON.parse(await this.readText(t));
|
|
112
141
|
}
|
|
113
142
|
async validate() {
|
|
114
|
-
const t = [],
|
|
115
|
-
for (const
|
|
116
|
-
const s = v(
|
|
143
|
+
const t = [], e = new Set(this.entries.map((o) => o.name));
|
|
144
|
+
for (const o of this.entries) {
|
|
145
|
+
const s = v(o.name);
|
|
117
146
|
s && t.push(s);
|
|
118
147
|
}
|
|
119
|
-
return
|
|
148
|
+
return e.has(y) || t.push(`Archive is missing '${y}'.`), this.manifest.format !== g && 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
149
|
}
|
|
121
|
-
async validateProtocol(t,
|
|
150
|
+
async validateProtocol(t, e, o) {
|
|
122
151
|
if (!t || typeof t != "object") {
|
|
123
|
-
|
|
152
|
+
o.push("Protocol manifest entry must be an object.");
|
|
124
153
|
return;
|
|
125
154
|
}
|
|
126
|
-
const s = t.entrypoint || "protocol.aimd",
|
|
127
|
-
this.has(
|
|
128
|
-
const
|
|
129
|
-
for (const
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
|
|
155
|
+
const s = t.entrypoint || "protocol.aimd", r = `${e}${s}`;
|
|
156
|
+
this.has(r) || o.push(`Protocol entrypoint '${r}' is missing.`);
|
|
157
|
+
const n = Array.isArray(t.files) ? t.files : [], a = t.file_hashes && typeof t.file_hashes == "object" ? t.file_hashes : {};
|
|
158
|
+
for (const c of n) {
|
|
159
|
+
const f = `${e}${c}`, d = v(f);
|
|
160
|
+
if (d) {
|
|
161
|
+
o.push(d);
|
|
133
162
|
continue;
|
|
134
163
|
}
|
|
135
|
-
if (!this.has(
|
|
136
|
-
|
|
164
|
+
if (!this.has(f)) {
|
|
165
|
+
o.push(`Protocol file '${f}' is missing.`);
|
|
137
166
|
continue;
|
|
138
167
|
}
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
141
|
-
const
|
|
142
|
-
|
|
168
|
+
const h = a[c];
|
|
169
|
+
if (h) {
|
|
170
|
+
const l = await A(await this.readBytes(f));
|
|
171
|
+
l !== h && o.push(`Protocol file '${f}' sha256 mismatch: expected ${h}, got ${l}.`);
|
|
143
172
|
}
|
|
144
173
|
}
|
|
145
174
|
}
|
|
146
|
-
async validateProtocolList(t,
|
|
175
|
+
async validateProtocolList(t, e, o = !1) {
|
|
147
176
|
const s = /* @__PURE__ */ new Set();
|
|
148
177
|
if (!Array.isArray(t))
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
for (const [
|
|
152
|
-
if (!
|
|
153
|
-
|
|
178
|
+
return e.push("Protocols manifest field must be a list."), s;
|
|
179
|
+
o && t.length === 0 && e.push("Protocols manifest field must include at least one protocol.");
|
|
180
|
+
for (const [r, n] of t.entries()) {
|
|
181
|
+
if (!n || typeof n != "object") {
|
|
182
|
+
e.push(`Protocol manifest entry #${r + 1} must be an object.`);
|
|
154
183
|
continue;
|
|
155
184
|
}
|
|
156
|
-
if (!
|
|
157
|
-
|
|
185
|
+
if (!n.archive_root) {
|
|
186
|
+
e.push(`Protocol manifest entry #${r + 1} is missing archive_root.`);
|
|
158
187
|
continue;
|
|
159
188
|
}
|
|
160
|
-
if (s.has(
|
|
161
|
-
|
|
189
|
+
if (s.has(n.archive_root)) {
|
|
190
|
+
e.push(`Protocol manifest entry #${r + 1} uses duplicate archive_root '${n.archive_root}'.`);
|
|
162
191
|
continue;
|
|
163
192
|
}
|
|
164
|
-
s.add(
|
|
193
|
+
s.add(n.archive_root), await this.validateProtocol(n, `${n.archive_root.replace(/\/+$/, "")}/`, e);
|
|
165
194
|
}
|
|
166
195
|
return s;
|
|
167
196
|
}
|
|
168
197
|
async validateRecords(t) {
|
|
169
|
-
const
|
|
170
|
-
for (const [
|
|
171
|
-
if (!
|
|
172
|
-
t.push(`Record manifest entry #${
|
|
198
|
+
const e = Array.isArray(this.manifest.records) ? this.manifest.records : [], o = await this.validateProtocolList(this.manifest.protocols, t), s = /* @__PURE__ */ new Set();
|
|
199
|
+
for (const [n, a] of e.entries()) {
|
|
200
|
+
if (!a || typeof a != "object") {
|
|
201
|
+
t.push(`Record manifest entry #${n + 1} must be an object.`);
|
|
173
202
|
continue;
|
|
174
203
|
}
|
|
175
|
-
const
|
|
176
|
-
if (!
|
|
177
|
-
t.push(`Record manifest entry #${
|
|
204
|
+
const c = a.path;
|
|
205
|
+
if (!c) {
|
|
206
|
+
t.push(`Record manifest entry #${n + 1} is missing a path.`);
|
|
178
207
|
continue;
|
|
179
208
|
}
|
|
180
|
-
const
|
|
181
|
-
if (
|
|
182
|
-
t.push(
|
|
209
|
+
const f = v(c);
|
|
210
|
+
if (f) {
|
|
211
|
+
t.push(f);
|
|
183
212
|
continue;
|
|
184
213
|
}
|
|
185
|
-
if (!this.has(
|
|
186
|
-
t.push(`Record file '${
|
|
214
|
+
if (!this.has(c)) {
|
|
215
|
+
t.push(`Record file '${c}' is missing.`);
|
|
187
216
|
continue;
|
|
188
217
|
}
|
|
189
|
-
s.add(
|
|
190
|
-
let
|
|
218
|
+
s.add(c);
|
|
219
|
+
let d, h;
|
|
191
220
|
try {
|
|
192
|
-
|
|
221
|
+
d = await this.readBytes(c), h = JSON.parse(R.decode(d));
|
|
193
222
|
} catch {
|
|
194
|
-
t.push(`Record file '${
|
|
223
|
+
t.push(`Record file '${c}' is not valid UTF-8 JSON.`);
|
|
195
224
|
continue;
|
|
196
225
|
}
|
|
197
|
-
if (
|
|
198
|
-
const
|
|
199
|
-
|
|
226
|
+
if (t.push(...D(h, `Record file '${c}'`)), a.sha256) {
|
|
227
|
+
const l = await A(d);
|
|
228
|
+
l !== a.sha256 && t.push(`Record file '${c}' sha256 mismatch: expected ${a.sha256}, got ${l}.`);
|
|
200
229
|
}
|
|
201
|
-
|
|
230
|
+
a.embedded_protocol_root && !o.has(a.embedded_protocol_root) && t.push(`Record file '${c}' references missing embedded protocol root '${a.embedded_protocol_root}'.`);
|
|
202
231
|
}
|
|
203
|
-
const
|
|
204
|
-
this.validateFileReferences(t,
|
|
232
|
+
const r = await this.validateBlobs(t);
|
|
233
|
+
this.validateFileReferences(t, r, s);
|
|
205
234
|
}
|
|
206
235
|
async validateBlobs(t) {
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
209
|
-
return
|
|
210
|
-
if (!Array.isArray(
|
|
211
|
-
return t.push("Blobs manifest field must be a list."),
|
|
236
|
+
const e = /* @__PURE__ */ new Set(), o = this.manifest.blobs;
|
|
237
|
+
if (o === void 0)
|
|
238
|
+
return e;
|
|
239
|
+
if (!Array.isArray(o))
|
|
240
|
+
return t.push("Blobs manifest field must be a list."), e;
|
|
212
241
|
const s = /* @__PURE__ */ new Set();
|
|
213
|
-
for (const [
|
|
214
|
-
if (!
|
|
215
|
-
t.push(`Blob manifest entry #${
|
|
242
|
+
for (const [r, n] of o.entries()) {
|
|
243
|
+
if (!n || typeof n != "object") {
|
|
244
|
+
t.push(`Blob manifest entry #${r + 1} must be an object.`);
|
|
216
245
|
continue;
|
|
217
246
|
}
|
|
218
|
-
const
|
|
219
|
-
if (!
|
|
220
|
-
t.push(`Blob manifest entry #${
|
|
247
|
+
const a = n.blob_id, c = n.archive_path, f = n.sha256;
|
|
248
|
+
if (!a) {
|
|
249
|
+
t.push(`Blob manifest entry #${r + 1} is missing blob_id.`);
|
|
221
250
|
continue;
|
|
222
251
|
}
|
|
223
|
-
if (
|
|
224
|
-
t.push(`Blob manifest entry #${
|
|
252
|
+
if (e.has(a)) {
|
|
253
|
+
t.push(`Blob manifest entry #${r + 1} uses duplicate blob_id '${a}'.`);
|
|
225
254
|
continue;
|
|
226
255
|
}
|
|
227
|
-
if (
|
|
228
|
-
t.push(`Blob '${
|
|
256
|
+
if (e.add(a), !f || !j.test(f)) {
|
|
257
|
+
t.push(`Blob '${a}' must include a valid sha256 hash.`);
|
|
229
258
|
continue;
|
|
230
259
|
}
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
233
|
-
t.push(`Blob '${
|
|
260
|
+
const d = `sha256:${f}`;
|
|
261
|
+
if (a !== d && t.push(`Blob '${a}' does not match sha256-derived id '${d}'.`), !c) {
|
|
262
|
+
t.push(`Blob '${a}' is missing archive_path.`);
|
|
234
263
|
continue;
|
|
235
264
|
}
|
|
236
|
-
const
|
|
237
|
-
if (
|
|
238
|
-
t.push(
|
|
265
|
+
const h = v(c);
|
|
266
|
+
if (h) {
|
|
267
|
+
t.push(h);
|
|
239
268
|
continue;
|
|
240
269
|
}
|
|
241
|
-
if (
|
|
242
|
-
t.push(`Blob '${
|
|
270
|
+
if (c.startsWith("blobs/sha256/") || t.push(`Blob '${a}' archive_path must be under 'blobs/sha256/'.`), s.has(c)) {
|
|
271
|
+
t.push(`Blob '${a}' uses duplicate archive_path '${c}'.`);
|
|
243
272
|
continue;
|
|
244
273
|
}
|
|
245
|
-
if (s.add(
|
|
246
|
-
t.push(`Blob file '${
|
|
274
|
+
if (s.add(c), !this.has(c)) {
|
|
275
|
+
t.push(`Blob file '${c}' is missing.`);
|
|
247
276
|
continue;
|
|
248
277
|
}
|
|
249
|
-
const
|
|
250
|
-
|
|
278
|
+
const l = await this.readBytes(c), _ = await A(l);
|
|
279
|
+
_ !== f && t.push(`Blob file '${c}' sha256 mismatch: expected ${f}, got ${_}.`), typeof n.size == "number" && n.size !== l.byteLength ? t.push(`Blob file '${c}' size mismatch: expected ${n.size}, got ${l.byteLength}.`) : n.size !== void 0 && typeof n.size != "number" && t.push(`Blob '${a}' size must be a number when present.`);
|
|
251
280
|
}
|
|
252
|
-
return
|
|
281
|
+
return e;
|
|
253
282
|
}
|
|
254
|
-
validateFileReferences(t,
|
|
283
|
+
validateFileReferences(t, e, o) {
|
|
255
284
|
const s = this.manifest.files;
|
|
256
285
|
if (s !== void 0) {
|
|
257
286
|
if (!Array.isArray(s)) {
|
|
258
287
|
t.push("Files manifest field must be a list.");
|
|
259
288
|
return;
|
|
260
289
|
}
|
|
261
|
-
for (const [
|
|
262
|
-
if (!
|
|
263
|
-
t.push(`File manifest entry #${
|
|
290
|
+
for (const [r, n] of s.entries()) {
|
|
291
|
+
if (!n || typeof n != "object") {
|
|
292
|
+
t.push(`File manifest entry #${r + 1} must be an object.`);
|
|
264
293
|
continue;
|
|
265
294
|
}
|
|
266
|
-
!
|
|
295
|
+
!n.file_id && !n.source_uri && !n.blob_id && t.push(`File manifest entry #${r + 1} must include file_id, source_uri, or blob_id.`), n.blob_id && !e.has(n.blob_id) && t.push(`File manifest entry #${r + 1} references missing blob_id '${n.blob_id}'.`), n.record_path && !o.has(n.record_path) && t.push(`File manifest entry #${r + 1} references missing record_path '${n.record_path}'.`), n.field_path !== void 0 && typeof n.field_path != "string" && t.push(`File manifest entry #${r + 1} field_path must be a string.`);
|
|
267
296
|
}
|
|
268
297
|
}
|
|
269
298
|
}
|
|
270
299
|
}
|
|
271
|
-
async function
|
|
272
|
-
return
|
|
300
|
+
async function L(i) {
|
|
301
|
+
return $.open(i);
|
|
273
302
|
}
|
|
274
|
-
async function
|
|
275
|
-
return (await
|
|
303
|
+
async function N(i) {
|
|
304
|
+
return (await $.open(i)).summary();
|
|
276
305
|
}
|
|
277
|
-
function
|
|
306
|
+
function H(i) {
|
|
278
307
|
return JSON.stringify(i, null, 2);
|
|
279
308
|
}
|
|
280
|
-
function
|
|
281
|
-
return
|
|
309
|
+
function z(i) {
|
|
310
|
+
return x.encode(i);
|
|
282
311
|
}
|
|
283
312
|
export {
|
|
313
|
+
E as AIRALOGY_RECORD_FORMAT,
|
|
314
|
+
k as AIRALOGY_RECORD_SCHEMA_VERSION,
|
|
284
315
|
g as AIRA_ARCHIVE_FORMAT,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
316
|
+
y as AIRA_MANIFEST_PATH,
|
|
317
|
+
$ as AiraArchive,
|
|
318
|
+
z as encodeUtf8,
|
|
319
|
+
L as openAiraArchive,
|
|
320
|
+
H as prettyPrintJson,
|
|
321
|
+
N as readAiraArchiveSummary
|
|
291
322
|
};
|
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
|
|
@@ -112,6 +132,88 @@ function validateMemberPath(name: string): string | null {
|
|
|
112
132
|
return null
|
|
113
133
|
}
|
|
114
134
|
|
|
135
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
136
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function validateRecordPayload(record: unknown, label: string): string[] {
|
|
140
|
+
const issues: string[] = []
|
|
141
|
+
if (!isPlainObject(record)) {
|
|
142
|
+
return [`${label} must be a JSON object.`]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (record.format !== undefined && record.format !== AIRALOGY_RECORD_FORMAT) {
|
|
146
|
+
issues.push(`${label} format must be '${AIRALOGY_RECORD_FORMAT}' when present.`)
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
record.schema_version !== undefined
|
|
150
|
+
&& record.schema_version !== AIRALOGY_RECORD_SCHEMA_VERSION
|
|
151
|
+
) {
|
|
152
|
+
issues.push(`${label} schema_version must be ${AIRALOGY_RECORD_SCHEMA_VERSION} when present.`)
|
|
153
|
+
}
|
|
154
|
+
if (
|
|
155
|
+
record.record_id !== undefined
|
|
156
|
+
&& record.record_id !== null
|
|
157
|
+
&& (typeof record.record_id !== 'string' || record.record_id.length === 0)
|
|
158
|
+
) {
|
|
159
|
+
issues.push(`${label} record_id must be a non-empty string when present.`)
|
|
160
|
+
}
|
|
161
|
+
if (
|
|
162
|
+
record.airalogy_record_id !== undefined
|
|
163
|
+
&& record.airalogy_record_id !== null
|
|
164
|
+
&& (typeof record.airalogy_record_id !== 'string' || record.airalogy_record_id.length === 0)
|
|
165
|
+
) {
|
|
166
|
+
issues.push(`${label} airalogy_record_id must be a non-empty string when present.`)
|
|
167
|
+
}
|
|
168
|
+
const recordVersion = record.record_version
|
|
169
|
+
if (
|
|
170
|
+
recordVersion !== undefined
|
|
171
|
+
&& recordVersion !== null
|
|
172
|
+
&& (typeof recordVersion !== 'number' || !Number.isInteger(recordVersion) || recordVersion < 1)
|
|
173
|
+
) {
|
|
174
|
+
issues.push(`${label} record_version must be a positive integer when present.`)
|
|
175
|
+
}
|
|
176
|
+
if (
|
|
177
|
+
record.metadata !== undefined
|
|
178
|
+
&& record.metadata !== null
|
|
179
|
+
&& !isPlainObject(record.metadata)
|
|
180
|
+
) {
|
|
181
|
+
issues.push(`${label} metadata must be an object when present.`)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!isPlainObject(record.data)) {
|
|
185
|
+
issues.push(`${label} data must be an object.`)
|
|
186
|
+
return issues
|
|
187
|
+
}
|
|
188
|
+
if (!isPlainObject(record.data.var)) {
|
|
189
|
+
issues.push(`${label} data.var must be an object.`)
|
|
190
|
+
}
|
|
191
|
+
for (const section of ['step', 'check', 'quiz']) {
|
|
192
|
+
const value = record.data[section]
|
|
193
|
+
if (value !== undefined && value !== null && !isPlainObject(value)) {
|
|
194
|
+
issues.push(`${label} data.${section} must be an object when present.`)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (record.files !== undefined) {
|
|
199
|
+
if (!Array.isArray(record.files)) {
|
|
200
|
+
issues.push(`${label} files must be a list when present.`)
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
for (const [index, fileRef] of record.files.entries()) {
|
|
204
|
+
if (!isPlainObject(fileRef)) {
|
|
205
|
+
issues.push(`${label} files[${index + 1}] must be an object.`)
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
if (!['file_id', 'source_uri', 'blob_id'].some(key => typeof fileRef[key] === 'string' && fileRef[key])) {
|
|
209
|
+
issues.push(`${label} files[${index + 1}] must include file_id, source_uri, or blob_id.`)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return issues
|
|
215
|
+
}
|
|
216
|
+
|
|
115
217
|
function findEndOfCentralDirectory(bytes: Uint8Array): number {
|
|
116
218
|
const minOffset = Math.max(0, bytes.length - 0xffff - 22)
|
|
117
219
|
for (let offset = bytes.length - 22; offset >= minOffset; offset -= 1) {
|
|
@@ -407,14 +509,16 @@ export class AiraArchive {
|
|
|
407
509
|
}
|
|
408
510
|
recordPaths.add(path)
|
|
409
511
|
let raw: Uint8Array
|
|
512
|
+
let parsedRecord: unknown
|
|
410
513
|
try {
|
|
411
514
|
raw = await this.readBytes(path)
|
|
412
|
-
JSON.parse(textDecoder.decode(raw))
|
|
515
|
+
parsedRecord = JSON.parse(textDecoder.decode(raw))
|
|
413
516
|
}
|
|
414
517
|
catch {
|
|
415
518
|
issues.push(`Record file '${path}' is not valid UTF-8 JSON.`)
|
|
416
519
|
continue
|
|
417
520
|
}
|
|
521
|
+
issues.push(...validateRecordPayload(parsedRecord, `Record file '${path}'`))
|
|
418
522
|
if (record.sha256) {
|
|
419
523
|
const actualHash = await sha256Hex(raw)
|
|
420
524
|
if (actualHash !== record.sha256) {
|