@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 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.
@@ -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 b = "_airalogy_archive/manifest.json", g = "airalogy.archive", A = new TextDecoder("utf-8"), E = new TextEncoder(), R = /^[0-9a-f]{64}$/;
2
- function m(i, t) {
3
- return i.getUint16(t, !0);
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(i, t) {
6
- return i.getUint32(t, !0);
5
+ function p(e, t) {
6
+ return e.getUint32(t, !0);
7
7
  }
8
- function U(i) {
9
- return A.decode(i);
8
+ function H(e) {
9
+ return E.decode(e);
10
10
  }
11
- function v(i) {
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;
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 k(i) {
15
- const t = Math.max(0, i.length - 65535 - 22);
16
- for (let o = i.length - 22; o >= t; o -= 1)
17
- if (i[o] === 80 && i[o + 1] === 75 && i[o + 2] === 5 && i[o + 3] === 6)
18
- return o;
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 D(i) {
22
- const t = new DataView(i.buffer, i.byteOffset, i.byteLength), o = k(i), n = m(t, o + 10), s = p(t, o + 16), c = [];
23
- let e = s;
24
- for (let r = 0; r < n; r += 1) {
25
- if (p(t, e) !== 33639248)
26
- throw new Error(`Archive central directory is invalid at offset ${e}.`);
27
- const a = m(t, e + 10), h = p(t, e + 20), l = p(t, e + 24), f = m(t, e + 28), d = m(t, e + 30), $ = m(t, e + 32), u = p(t, e + 42), w = e + 46, S = U(i.slice(w, w + f));
28
- if (p(t, u) !== 67324752)
29
- throw new Error(`Archive local header is invalid for '${S}'.`);
30
- const B = m(t, u + 26), P = m(t, u + 28), x = u + 30 + B + P;
31
- c.push({
32
- name: S,
33
- compressionMethod: a,
34
- compressedSize: h,
35
- uncompressedSize: l,
36
- localHeaderOffset: u,
37
- compressedDataStart: x
38
- }), e = w + f + d + $;
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 c.filter((r) => !r.name.endsWith("/"));
77
+ return s.filter((a) => !a.name.endsWith("/"));
41
78
  }
42
- async function j(i) {
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 o = new Blob([i]).stream().pipeThrough(new t("deflate-raw")), n = await new Response(o).arrayBuffer();
47
- return new Uint8Array(n);
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
- async function _(i) {
50
- const t = await crypto.subtle.digest("SHA-256", i);
51
- return Array.from(new Uint8Array(t)).map((o) => o.toString(16).padStart(2, "0")).join("");
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 y {
218
+ class _ {
54
219
  bytes;
55
220
  entries;
56
221
  manifest;
57
222
  entryMap;
58
- constructor(t, o, n) {
59
- this.bytes = t, this.entries = o.map(({ compressedDataStart: s, ...c }) => c), this.entryMap = new Map(o.map((s) => [s.name, s])), this.manifest = n;
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 o = t instanceof Uint8Array ? t : t instanceof Blob ? new Uint8Array(await t.arrayBuffer()) : new Uint8Array(t), n = D(o);
63
- if (!n.find((r) => r.name === b))
64
- throw new Error(`Archive does not contain '${b}'.`);
65
- const e = await new y(o, n, {
66
- format: g,
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(b);
70
- if (e.format !== g)
71
- throw new Error(`Unsupported archive format '${String(e.format)}'.`);
72
- if (e.kind !== "protocol" && e.kind !== "protocols" && e.kind !== "records")
73
- throw new Error(`Unsupported archive kind '${String(e.kind)}'.`);
74
- return new y(o, n, e);
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, o = this.manifest.kind === "protocol" ? 1 : Array.isArray(this.manifest.protocols) ? this.manifest.protocols.length : 0, n = Array.isArray(this.manifest.blobs) ? this.manifest.blobs.length : 0, s = Array.isArray(this.manifest.files) ? this.manifest.files.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: o,
89
- blobCount: n,
90
- fileCount: s
253
+ protocolCount: n,
254
+ blobCount: i,
255
+ fileCount: r
91
256
  };
92
257
  }
93
258
  async readBytes(t) {
94
- const o = this.entryMap.get(t);
95
- if (!o)
259
+ const n = this.entryMap.get(t);
260
+ if (!n)
96
261
  throw new Error(`Archive member '${t}' not found.`);
97
- const n = this.bytes.slice(
98
- o.compressedDataStart,
99
- o.compressedDataStart + o.compressedSize
262
+ const i = this.bytes.slice(
263
+ n.compressedDataStart,
264
+ n.compressedDataStart + n.compressedSize
100
265
  );
101
- if (o.compressionMethod === 0)
102
- return n;
103
- if (o.compressionMethod === 8)
104
- return j(n);
105
- throw new Error(`Archive member '${t}' uses unsupported compression method ${o.compressionMethod}.`);
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 A.decode(await this.readBytes(t));
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 = [], o = new Set(this.entries.map((n) => n.name));
115
- for (const n of this.entries) {
116
- const s = v(n.name);
117
- s && t.push(s);
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 o.has(b) || t.push(`Archive is missing '${b}'.`), 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 };
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, o, n) {
286
+ async validateProtocol(t, n, i) {
122
287
  if (!t || typeof t != "object") {
123
- n.push("Protocol manifest entry must be an object.");
288
+ i.push("Protocol manifest entry must be an object.");
124
289
  return;
125
290
  }
126
- const s = t.entrypoint || "protocol.aimd", c = `${o}${s}`;
127
- this.has(c) || n.push(`Protocol entrypoint '${c}' is missing.`);
128
- const e = Array.isArray(t.files) ? t.files : [], r = t.file_hashes && typeof t.file_hashes == "object" ? t.file_hashes : {};
129
- for (const a of e) {
130
- const h = `${o}${a}`, l = v(h);
131
- if (l) {
132
- n.push(l);
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(h)) {
136
- n.push(`Protocol file '${h}' is missing.`);
300
+ if (!this.has(f)) {
301
+ i.push(`Protocol file '${f}' is missing.`);
137
302
  continue;
138
303
  }
139
- const f = r[a];
140
- if (f) {
141
- const d = await _(await this.readBytes(h));
142
- d !== f && n.push(`Protocol file '${h}' sha256 mismatch: expected ${f}, got ${d}.`);
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, o, n = !1) {
147
- const s = /* @__PURE__ */ new Set();
311
+ async validateProtocolList(t, n, i = !1) {
312
+ const r = /* @__PURE__ */ new Set();
148
313
  if (!Array.isArray(t))
149
- return o.push("Protocols manifest field must be a list."), s;
150
- n && t.length === 0 && o.push("Protocols manifest field must include at least one protocol.");
151
- for (const [c, e] of t.entries()) {
152
- if (!e || typeof e != "object") {
153
- o.push(`Protocol manifest entry #${c + 1} must be an object.`);
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 (!e.archive_root) {
157
- o.push(`Protocol manifest entry #${c + 1} is missing archive_root.`);
321
+ if (!o.archive_root) {
322
+ n.push(`Protocol manifest entry #${s + 1} is missing archive_root.`);
158
323
  continue;
159
324
  }
160
- if (s.has(e.archive_root)) {
161
- o.push(`Protocol manifest entry #${c + 1} uses duplicate archive_root '${e.archive_root}'.`);
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
- s.add(e.archive_root), await this.validateProtocol(e, `${e.archive_root.replace(/\/+$/, "")}/`, o);
329
+ r.add(o.archive_root), await this.validateProtocol(o, `${o.archive_root.replace(/\/+$/, "")}/`, n);
165
330
  }
166
- return s;
331
+ return r;
167
332
  }
168
333
  async validateRecords(t) {
169
- const o = Array.isArray(this.manifest.records) ? this.manifest.records : [], n = await this.validateProtocolList(this.manifest.protocols, t), s = /* @__PURE__ */ new Set();
170
- for (const [e, r] of o.entries()) {
171
- if (!r || typeof r != "object") {
172
- t.push(`Record manifest entry #${e + 1} must be an object.`);
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 a = r.path;
176
- if (!a) {
177
- t.push(`Record manifest entry #${e + 1} is missing a path.`);
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 h = v(a);
181
- if (h) {
182
- t.push(h);
345
+ const f = v(c);
346
+ if (f) {
347
+ t.push(f);
183
348
  continue;
184
349
  }
185
- if (!this.has(a)) {
186
- t.push(`Record file '${a}' is missing.`);
350
+ if (!this.has(c)) {
351
+ t.push(`Record file '${c}' is missing.`);
187
352
  continue;
188
353
  }
189
- s.add(a);
190
- let l;
354
+ r.add(c);
355
+ let h, l;
191
356
  try {
192
- l = await this.readBytes(a), JSON.parse(A.decode(l));
357
+ h = await this.readBytes(c), l = JSON.parse(E.decode(h));
193
358
  } catch {
194
- t.push(`Record file '${a}' is not valid UTF-8 JSON.`);
359
+ t.push(`Record file '${c}' is not valid UTF-8 JSON.`);
195
360
  continue;
196
361
  }
197
- if (r.sha256) {
198
- const f = await _(l);
199
- f !== r.sha256 && t.push(`Record file '${a}' sha256 mismatch: expected ${r.sha256}, got ${f}.`);
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
- r.embedded_protocol_root && !n.has(r.embedded_protocol_root) && t.push(`Record file '${a}' references missing embedded protocol root '${r.embedded_protocol_root}'.`);
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 c = await this.validateBlobs(t);
204
- this.validateFileReferences(t, c, s);
368
+ const s = await this.validateBlobs(t);
369
+ this.validateFileReferences(t, s, r);
205
370
  }
206
371
  async validateBlobs(t) {
207
- const o = /* @__PURE__ */ new Set(), n = this.manifest.blobs;
208
- if (n === void 0)
209
- return o;
210
- if (!Array.isArray(n))
211
- return t.push("Blobs manifest field must be a list."), o;
212
- const s = /* @__PURE__ */ new Set();
213
- for (const [c, e] of n.entries()) {
214
- if (!e || typeof e != "object") {
215
- t.push(`Blob manifest entry #${c + 1} must be an object.`);
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 r = e.blob_id, a = e.archive_path, h = e.sha256;
219
- if (!r) {
220
- t.push(`Blob manifest entry #${c + 1} is missing blob_id.`);
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 (o.has(r)) {
224
- t.push(`Blob manifest entry #${c + 1} uses duplicate blob_id '${r}'.`);
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 (o.add(r), !h || !R.test(h)) {
228
- t.push(`Blob '${r}' must include a valid sha256 hash.`);
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 l = `sha256:${h}`;
232
- if (r !== l && t.push(`Blob '${r}' does not match sha256-derived id '${l}'.`), !a) {
233
- t.push(`Blob '${r}' is missing archive_path.`);
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 f = v(a);
237
- if (f) {
238
- t.push(f);
401
+ const l = v(c);
402
+ if (l) {
403
+ t.push(l);
239
404
  continue;
240
405
  }
241
- if (a.startsWith("blobs/sha256/") || t.push(`Blob '${r}' archive_path must be under 'blobs/sha256/'.`), s.has(a)) {
242
- t.push(`Blob '${r}' uses duplicate archive_path '${a}'.`);
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 (s.add(a), !this.has(a)) {
246
- t.push(`Blob file '${a}' is missing.`);
410
+ if (r.add(c), !this.has(c)) {
411
+ t.push(`Blob file '${c}' is missing.`);
247
412
  continue;
248
413
  }
249
- const d = await this.readBytes(a), $ = await _(d);
250
- $ !== h && t.push(`Blob file '${a}' sha256 mismatch: expected ${h}, got ${$}.`), typeof e.size == "number" && e.size !== d.byteLength ? t.push(`Blob file '${a}' size mismatch: expected ${e.size}, got ${d.byteLength}.`) : e.size !== void 0 && typeof e.size != "number" && t.push(`Blob '${r}' size must be a number when present.`);
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 o;
417
+ return n;
253
418
  }
254
- validateFileReferences(t, o, n) {
255
- const s = this.manifest.files;
256
- if (s !== void 0) {
257
- if (!Array.isArray(s)) {
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 [c, e] of s.entries()) {
262
- if (!e || typeof e != "object") {
263
- t.push(`File manifest entry #${c + 1} must be an object.`);
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
- !e.file_id && !e.source_uri && !e.blob_id && t.push(`File manifest entry #${c + 1} must include file_id, source_uri, or blob_id.`), e.blob_id && !o.has(e.blob_id) && t.push(`File manifest entry #${c + 1} references missing blob_id '${e.blob_id}'.`), e.record_path && !n.has(e.record_path) && t.push(`File manifest entry #${c + 1} references missing record_path '${e.record_path}'.`), e.field_path !== void 0 && typeof e.field_path != "string" && t.push(`File manifest entry #${c + 1} field_path must be a string.`);
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 M(i) {
272
- return y.open(i);
436
+ async function et(e) {
437
+ return _.open(e);
273
438
  }
274
- async function O(i) {
275
- return (await y.open(i)).summary();
439
+ async function nt(e) {
440
+ return (await _.open(e)).summary();
276
441
  }
277
- function z(i) {
278
- return JSON.stringify(i, null, 2);
442
+ function rt(e) {
443
+ return JSON.stringify(e, null, 2);
279
444
  }
280
- function F(i) {
281
- return E.encode(i);
445
+ function it(e) {
446
+ return w.encode(e);
282
447
  }
283
448
  export {
284
- g as AIRA_ARCHIVE_FORMAT,
285
- b as AIRA_MANIFEST_PATH,
286
- y as AiraArchive,
287
- F as encodeUtf8,
288
- M as openAiraArchive,
289
- z as prettyPrintJson,
290
- O as readAiraArchiveSummary
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@airalogy/aira-core",
3
3
  "type": "module",
4
- "version": "0.1.0",
4
+ "version": "0.3.0",
5
5
  "description": "Core parser and validator for Airalogy .aira archives",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
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) {