@world-engines/scenario-author-source 0.1.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,46 @@
1
+ WorldEngine 官方作者工具许可证
2
+
3
+ 版权所有 (c) 2026 Nixdorfer。保留所有权利。
4
+
5
+ 本仓库的源代码、构建材料及附带资源(统称“本作品”)不是开源软件,
6
+ 不适用任何 OSI 认证的开源许可证。
7
+
8
+ 一、官方作者工具分发许可
9
+
10
+ 版权所有者可以通过其官方网站、npm registry、签名安装包或其他官方渠道,
11
+ 复制并分发由本作品构建的 WorldEngine 作者工具及其必要运行资源
12
+ (统称“官方作者工具”)。仅版权所有者或其书面指定的发布者享有此分发权。
13
+
14
+ 二、作者用户许可
15
+
16
+ 从官方渠道取得官方作者工具的作者用户,可以:
17
+
18
+ 1. 安装和运行官方作者工具;
19
+ 2. 使用官方作者工具创作、编辑、预览、导入、导出和提交其有权处理的内容;
20
+ 3. 为上述使用目的制作合理必要的本地备份副本。
21
+
22
+ 三、未授予的权利
23
+
24
+ 除第二条明确允许的行为外,本许可证不授予作者用户或其他第三方以下权利:
25
+
26
+ 1. 修改、改编、反编译、反汇编或制作官方作者工具的派生作品;
27
+ 2. 复制、镜像、转发、再上传、出售、出租、再分发或再许可官方作者工具;
28
+ 3. 使用本作品的源代码、构建材料或任何部分开发、提供或训练其他产品或服务;
29
+ 4. 删除或规避版权、许可、签名、访问控制或其他权利管理信息。
30
+
31
+ 法律强制允许且合同不得排除的权利不受上述限制影响。
32
+
33
+ 四、源代码查看
34
+
35
+ 第三方可以在已获合法访问权限的范围内查看本作品,用于审查、安全研究或学习参考;
36
+ 查看不授予运行、复制、修改、分发、再许可或用于其他产品与服务的权利。
37
+
38
+ 五、无担保与责任限制
39
+
40
+ 本作品与官方作者工具均按“现状”提供,不附带任何明示或暗示的担保。
41
+ 在适用法律允许的最大范围内,版权所有者不对因访问或使用本作品或官方作者工具
42
+ 造成的任何损失承担责任。
43
+
44
+ 六、终止
45
+
46
+ 违反本许可证将立即终止相应的访问与使用权;终止不影响版权所有者已经产生的权利和救济。
@@ -0,0 +1,12 @@
1
+ import type { NeutralScenarioSource, ScenarioSourceManifest, ScenarioSourceSection, ScenarioSourceSectionIdentity } from "@world-engines/protocol-ts/v1/neutral_scenario_source";
2
+ export declare const NEUTRAL_SCENARIO_SOURCE_SCHEMA_VERSION: 1;
3
+ export declare const SCENARIO_SOURCE_MANIFEST_SCHEMA_VERSION: 1;
4
+ export declare const REQUIRED_SCENARIO_SOURCE_SECTION_KEYS: readonly ["metadata", "logical_graph", "trigger_semantics", "layout", "view_project", "resource_refs", "prompt_retrieval"];
5
+ export type RequiredScenarioSourceSectionKey = typeof REQUIRED_SCENARIO_SOURCE_SECTION_KEYS[number];
6
+ export type { NeutralScenarioSource, ScenarioSourceManifest, ScenarioSourceSection, ScenarioSourceSectionIdentity, };
7
+ export type ScenarioSourceUnknownFields = Readonly<Record<number, readonly Uint8Array[]>>;
8
+ export declare class ScenarioSourceIntegrityError extends Error {
9
+ readonly code: string;
10
+ readonly path: string;
11
+ constructor(code: string, path: string);
12
+ }
@@ -0,0 +1,21 @@
1
+ export const NEUTRAL_SCENARIO_SOURCE_SCHEMA_VERSION = 1;
2
+ export const SCENARIO_SOURCE_MANIFEST_SCHEMA_VERSION = 1;
3
+ export const REQUIRED_SCENARIO_SOURCE_SECTION_KEYS = Object.freeze([
4
+ "metadata",
5
+ "logical_graph",
6
+ "trigger_semantics",
7
+ "layout",
8
+ "view_project",
9
+ "resource_refs",
10
+ "prompt_retrieval",
11
+ ]);
12
+ export class ScenarioSourceIntegrityError extends Error {
13
+ code;
14
+ path;
15
+ constructor(code, path) {
16
+ super(`${code}:${path}`);
17
+ this.name = "ScenarioSourceIntegrityError";
18
+ this.code = code;
19
+ this.path = path;
20
+ }
21
+ }
@@ -0,0 +1,8 @@
1
+ import type { NeutralScenarioSource, ScenarioSourceManifest, ScenarioSourceSection, ScenarioSourceSectionIdentity } from "./author-source.js";
2
+ export declare function canonicalizeScenarioSourceSection(section: ScenarioSourceSection): ScenarioSourceSection;
3
+ export declare function canonicalizeScenarioSourceSectionIdentity(identity: ScenarioSourceSectionIdentity): ScenarioSourceSectionIdentity;
4
+ export declare function canonicalizeScenarioSourceManifest(manifest: ScenarioSourceManifest, clearDigest?: boolean): ScenarioSourceManifest;
5
+ export declare function canonicalizeNeutralScenarioSource(source: NeutralScenarioSource): NeutralScenarioSource;
6
+ export declare function canonicalScenarioSourceSectionBytes(section: ScenarioSourceSection): Uint8Array;
7
+ export declare function canonicalScenarioSourceManifestDigestInputBytes(manifest: ScenarioSourceManifest): Uint8Array;
8
+ export declare function canonicalNeutralScenarioSourceBytes(source: NeutralScenarioSource): Uint8Array;
@@ -0,0 +1,76 @@
1
+ import { NeutralScenarioSource as NeutralScenarioSourceCodec, ScenarioSourceManifest as ScenarioSourceManifestCodec, ScenarioSourceSection as ScenarioSourceSectionCodec, } from "@world-engines/protocol-ts/v1/neutral_scenario_source";
2
+ function compareUtf8(left, right) {
3
+ const encoder = new TextEncoder();
4
+ const leftBytes = encoder.encode(left);
5
+ const rightBytes = encoder.encode(right);
6
+ const length = Math.min(leftBytes.length, rightBytes.length);
7
+ for (let index = 0; index < length; index += 1) {
8
+ const difference = (leftBytes[index] ?? 0) - (rightBytes[index] ?? 0);
9
+ if (difference !== 0)
10
+ return difference;
11
+ }
12
+ return leftBytes.length - rightBytes.length;
13
+ }
14
+ function cloneUnknownFields(fields) {
15
+ if (fields === undefined)
16
+ return undefined;
17
+ return Object.fromEntries(Object.entries(fields)
18
+ .sort(([left], [right]) => Number(left) - Number(right))
19
+ .map(([tag, values]) => [tag, values.map((value) => value.slice())]));
20
+ }
21
+ export function canonicalizeScenarioSourceSection(section) {
22
+ const unknownFields = cloneUnknownFields(section._unknownFields);
23
+ return Object.freeze({
24
+ section_key: section.section_key,
25
+ media_type: section.media_type,
26
+ canonical_encoding: section.canonical_encoding,
27
+ payload: section.payload.slice(),
28
+ ...(unknownFields === undefined ? {} : { _unknownFields: unknownFields }),
29
+ });
30
+ }
31
+ export function canonicalizeScenarioSourceSectionIdentity(identity) {
32
+ const unknownFields = cloneUnknownFields(identity._unknownFields);
33
+ return Object.freeze({
34
+ section_key: identity.section_key,
35
+ media_type: identity.media_type,
36
+ canonical_encoding: identity.canonical_encoding,
37
+ byte_length: identity.byte_length,
38
+ sha256: identity.sha256.slice(),
39
+ ...(unknownFields === undefined ? {} : { _unknownFields: unknownFields }),
40
+ });
41
+ }
42
+ export function canonicalizeScenarioSourceManifest(manifest, clearDigest = false) {
43
+ const unknownFields = cloneUnknownFields(manifest._unknownFields);
44
+ return Object.freeze({
45
+ manifest_schema_version: manifest.manifest_schema_version,
46
+ scenario_id: manifest.scenario_id,
47
+ sections: Object.freeze(manifest.sections
48
+ .map(canonicalizeScenarioSourceSectionIdentity)
49
+ .sort((left, right) => compareUtf8(left.section_key, right.section_key))),
50
+ manifest_sha256: clearDigest ? new Uint8Array(0) : manifest.manifest_sha256.slice(),
51
+ ...(unknownFields === undefined ? {} : { _unknownFields: unknownFields }),
52
+ });
53
+ }
54
+ export function canonicalizeNeutralScenarioSource(source) {
55
+ const unknownFields = cloneUnknownFields(source._unknownFields);
56
+ return Object.freeze({
57
+ source_schema_version: source.source_schema_version,
58
+ scenario_id: source.scenario_id,
59
+ manifest: source.manifest === undefined
60
+ ? undefined
61
+ : canonicalizeScenarioSourceManifest(source.manifest),
62
+ sections: Object.freeze(source.sections
63
+ .map(canonicalizeScenarioSourceSection)
64
+ .sort((left, right) => compareUtf8(left.section_key, right.section_key))),
65
+ ...(unknownFields === undefined ? {} : { _unknownFields: unknownFields }),
66
+ });
67
+ }
68
+ export function canonicalScenarioSourceSectionBytes(section) {
69
+ return ScenarioSourceSectionCodec.encode(canonicalizeScenarioSourceSection(section)).finish();
70
+ }
71
+ export function canonicalScenarioSourceManifestDigestInputBytes(manifest) {
72
+ return ScenarioSourceManifestCodec.encode(canonicalizeScenarioSourceManifest(manifest, true)).finish();
73
+ }
74
+ export function canonicalNeutralScenarioSourceBytes(source) {
75
+ return NeutralScenarioSourceCodec.encode(canonicalizeNeutralScenarioSource(source)).finish();
76
+ }
@@ -0,0 +1,35 @@
1
+ import type { NeutralScenarioSource, ScenarioSourceManifest, ScenarioSourceSection } from "./author-source.js";
2
+ export interface NeutralSourceCaptureWitness {
3
+ readonly owner_id: string;
4
+ readonly scenario_id: string;
5
+ readonly source_revision: number;
6
+ readonly manifest_digest: string;
7
+ readonly source_digest: string;
8
+ readonly revision_digest: string;
9
+ readonly previous_head_digest: string | null;
10
+ readonly head_digest: string;
11
+ }
12
+ /**
13
+ * Complete, immutable input captured from one neutral source head.
14
+ *
15
+ * Compiler stages may only consume this value plus a CompileTargetProfile.
16
+ * They must not retain a callback to the remote source or a Creator working
17
+ * projection.
18
+ */
19
+ export interface NeutralSourceCompileInput {
20
+ readonly witness: NeutralSourceCaptureWitness;
21
+ readonly source: NeutralScenarioSource;
22
+ readonly manifest: ScenarioSourceManifest;
23
+ readonly source_bytes: Uint8Array;
24
+ readonly sections: readonly ScenarioSourceSection[];
25
+ }
26
+ export interface CaptureNeutralSourceCompileInput {
27
+ readonly witness: NeutralSourceCaptureWitness;
28
+ readonly source_bytes: Uint8Array;
29
+ }
30
+ /**
31
+ * Parses and validates one exact readback. No callback or mutable store is
32
+ * retained, so all work after this boundary is deterministic from the returned
33
+ * value.
34
+ */
35
+ export declare const captureNeutralSourceCompileInput: (input: CaptureNeutralSourceCompileInput, subtle?: SubtleCrypto) => Promise<NeutralSourceCompileInput>;
@@ -0,0 +1,124 @@
1
+ import { NeutralScenarioSource as NeutralScenarioSourceCodec, } from "@world-engines/protocol-ts/v1/neutral_scenario_source";
2
+ import { bytesEqual, sha256Bytes, sha256Hex, } from "./integrity.js";
3
+ import { verifyNeutralScenarioSource, } from "./manifest.js";
4
+ const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/u;
5
+ const requireIdentity = (value, field) => {
6
+ const normalized = value.normalize("NFC").trim();
7
+ if (normalized.length === 0 || normalized !== value) {
8
+ throw new Error(`neutral_compile_capture_${field}_invalid`);
9
+ }
10
+ return normalized;
11
+ };
12
+ const requireDigest = (value, field) => {
13
+ if (!SHA256_HEX_PATTERN.test(value)) {
14
+ throw new Error(`neutral_compile_capture_${field}_invalid`);
15
+ }
16
+ return value;
17
+ };
18
+ const cloneUnknownFields = (value) => {
19
+ if (value === undefined)
20
+ return undefined;
21
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, entries]) => [
22
+ key,
23
+ Object.freeze(entries.map((entry) => entry.slice())),
24
+ ])));
25
+ };
26
+ const cloneSection = (section) => Object.freeze({
27
+ section_key: section.section_key,
28
+ media_type: section.media_type,
29
+ canonical_encoding: section.canonical_encoding,
30
+ payload: section.payload.slice(),
31
+ ...(section._unknownFields === undefined
32
+ ? {}
33
+ : { _unknownFields: cloneUnknownFields(section._unknownFields) }),
34
+ });
35
+ const cloneManifest = (manifest) => {
36
+ const sections = manifest.sections.map((section) => Object.freeze({
37
+ section_key: section.section_key,
38
+ media_type: section.media_type,
39
+ canonical_encoding: section.canonical_encoding,
40
+ byte_length: section.byte_length,
41
+ sha256: section.sha256.slice(),
42
+ ...(section._unknownFields === undefined
43
+ ? {}
44
+ : { _unknownFields: cloneUnknownFields(section._unknownFields) }),
45
+ }));
46
+ Object.freeze(sections);
47
+ return Object.freeze({
48
+ manifest_schema_version: manifest.manifest_schema_version,
49
+ scenario_id: manifest.scenario_id,
50
+ sections,
51
+ manifest_sha256: manifest.manifest_sha256.slice(),
52
+ ...(manifest._unknownFields === undefined
53
+ ? {}
54
+ : { _unknownFields: cloneUnknownFields(manifest._unknownFields) }),
55
+ });
56
+ };
57
+ const cloneSource = (source, manifest, sections) => {
58
+ const sourceSections = [...sections];
59
+ Object.freeze(sourceSections);
60
+ return Object.freeze({
61
+ source_schema_version: source.source_schema_version,
62
+ scenario_id: source.scenario_id,
63
+ manifest,
64
+ sections: sourceSections,
65
+ ...(source._unknownFields === undefined
66
+ ? {}
67
+ : { _unknownFields: cloneUnknownFields(source._unknownFields) }),
68
+ });
69
+ };
70
+ const copyWitness = (witness) => {
71
+ requireIdentity(witness.owner_id, "owner");
72
+ requireIdentity(witness.scenario_id, "scenario");
73
+ if (!Number.isSafeInteger(witness.source_revision)
74
+ || witness.source_revision < 1) {
75
+ throw new Error("neutral_compile_capture_revision_invalid");
76
+ }
77
+ requireDigest(witness.manifest_digest, "manifest_digest");
78
+ requireDigest(witness.source_digest, "source_digest");
79
+ requireDigest(witness.revision_digest, "revision_digest");
80
+ requireDigest(witness.head_digest, "head_digest");
81
+ if (witness.previous_head_digest !== null) {
82
+ requireDigest(witness.previous_head_digest, "previous_head_digest");
83
+ }
84
+ return Object.freeze({ ...witness });
85
+ };
86
+ /**
87
+ * Parses and validates one exact readback. No callback or mutable store is
88
+ * retained, so all work after this boundary is deterministic from the returned
89
+ * value.
90
+ */
91
+ export const captureNeutralSourceCompileInput = async (input, subtle) => {
92
+ if (!(input.source_bytes instanceof Uint8Array)) {
93
+ throw new Error("neutral_compile_capture_source_bytes_invalid");
94
+ }
95
+ const witness = copyWitness(input.witness);
96
+ const sourceBytes = input.source_bytes.slice();
97
+ const sourceDigest = sha256Hex(await sha256Bytes(sourceBytes, subtle));
98
+ if (sourceDigest !== witness.source_digest) {
99
+ throw new Error("neutral_compile_capture_source_digest_mismatch");
100
+ }
101
+ const parsed = await verifyNeutralScenarioSource(NeutralScenarioSourceCodec.decode(sourceBytes), subtle);
102
+ const parsedManifest = parsed.manifest;
103
+ if (parsedManifest === undefined) {
104
+ throw new Error("neutral_compile_capture_manifest_missing");
105
+ }
106
+ if (parsed.scenario_id !== witness.scenario_id
107
+ || parsedManifest.scenario_id !== witness.scenario_id) {
108
+ throw new Error("neutral_compile_capture_scenario_mismatch");
109
+ }
110
+ const manifestDigest = Uint8Array.from(witness.manifest_digest.match(/../gu) ?? [], (octet) => Number.parseInt(octet, 16));
111
+ if (!bytesEqual(parsedManifest.manifest_sha256, manifestDigest)) {
112
+ throw new Error("neutral_compile_capture_manifest_digest_mismatch");
113
+ }
114
+ const manifest = cloneManifest(parsedManifest);
115
+ const sections = Object.freeze(parsed.sections.map(cloneSection));
116
+ const source = cloneSource(parsed, manifest, sections);
117
+ return Object.freeze({
118
+ witness,
119
+ source,
120
+ manifest,
121
+ source_bytes: sourceBytes,
122
+ sections,
123
+ });
124
+ };
@@ -0,0 +1,24 @@
1
+ import { type ScenarioSourceSection } from "@world-engines/protocol-ts/v1/neutral_scenario_source";
2
+ import { REQUIRED_SCENARIO_SOURCE_SECTION_KEYS, type NeutralScenarioSource } from "./author-source.js";
3
+ import { canonicalNeutralScenarioSourceBytes, canonicalizeNeutralScenarioSource } from "./canonicalize.js";
4
+ export * from "./author-source.js";
5
+ export * from "./canonicalize.js";
6
+ export * from "./compile-input.js";
7
+ export * from "./integrity.js";
8
+ export * from "./manifest.js";
9
+ export interface EmptyScenarioSourceSectionOverride {
10
+ readonly media_type: string;
11
+ readonly canonical_encoding: string;
12
+ readonly payload: Uint8Array;
13
+ }
14
+ export interface CreateCanonicalEmptyNeutralScenarioSourceInput {
15
+ readonly scenario_id: string;
16
+ readonly section_overrides?: Partial<Readonly<Record<typeof REQUIRED_SCENARIO_SOURCE_SECTION_KEYS[number], EmptyScenarioSourceSectionOverride>>>;
17
+ }
18
+ export declare function createNeutralScenarioSource(scenarioId: string, sections: readonly ScenarioSourceSection[], subtle?: SubtleCrypto): Promise<NeutralScenarioSource>;
19
+ export declare function parseNeutralScenarioSource(bytes: Uint8Array, subtle?: SubtleCrypto): Promise<NeutralScenarioSource>;
20
+ export declare function decodeNeutralScenarioSource(bytes: Uint8Array, subtle?: SubtleCrypto): Promise<NeutralScenarioSource>;
21
+ export declare function encodeNeutralScenarioSource(source: NeutralScenarioSource, subtle?: SubtleCrypto): Promise<Uint8Array>;
22
+ export declare function createCanonicalEmptyNeutralScenarioSource(input: CreateCanonicalEmptyNeutralScenarioSourceInput, subtle?: SubtleCrypto): Promise<NeutralScenarioSource>;
23
+ export declare function hashNeutralScenarioSource(source: NeutralScenarioSource, subtle?: SubtleCrypto): Promise<string>;
24
+ export { canonicalNeutralScenarioSourceBytes, canonicalizeNeutralScenarioSource, };
package/dist/index.js ADDED
@@ -0,0 +1,48 @@
1
+ import { NeutralScenarioSource as NeutralScenarioSourceCodec, } from "@world-engines/protocol-ts/v1/neutral_scenario_source";
2
+ import { NEUTRAL_SCENARIO_SOURCE_SCHEMA_VERSION, REQUIRED_SCENARIO_SOURCE_SECTION_KEYS, } from "./author-source.js";
3
+ import { canonicalNeutralScenarioSourceBytes, canonicalizeNeutralScenarioSource, } from "./canonicalize.js";
4
+ import { sha256Bytes, sha256Hex, } from "./integrity.js";
5
+ import { buildScenarioSourceManifest, verifyNeutralScenarioSource, } from "./manifest.js";
6
+ export * from "./author-source.js";
7
+ export * from "./canonicalize.js";
8
+ export * from "./compile-input.js";
9
+ export * from "./integrity.js";
10
+ export * from "./manifest.js";
11
+ export async function createNeutralScenarioSource(scenarioId, sections, subtle) {
12
+ const source = {
13
+ source_schema_version: NEUTRAL_SCENARIO_SOURCE_SCHEMA_VERSION,
14
+ scenario_id: scenarioId,
15
+ manifest: await buildScenarioSourceManifest(scenarioId, sections, subtle),
16
+ sections: [...sections],
17
+ };
18
+ return verifyNeutralScenarioSource(source, subtle);
19
+ }
20
+ export async function parseNeutralScenarioSource(bytes, subtle) {
21
+ if (!(bytes instanceof Uint8Array))
22
+ throw new TypeError("neutral_source_bytes_required");
23
+ return verifyNeutralScenarioSource(NeutralScenarioSourceCodec.decode(bytes), subtle);
24
+ }
25
+ export async function decodeNeutralScenarioSource(bytes, subtle) {
26
+ return parseNeutralScenarioSource(bytes, subtle);
27
+ }
28
+ export async function encodeNeutralScenarioSource(source, subtle) {
29
+ const verified = await verifyNeutralScenarioSource(source, subtle);
30
+ return canonicalNeutralScenarioSourceBytes(verified);
31
+ }
32
+ export async function createCanonicalEmptyNeutralScenarioSource(input, subtle) {
33
+ const sections = REQUIRED_SCENARIO_SOURCE_SECTION_KEYS.map((sectionKey) => {
34
+ const override = input.section_overrides?.[sectionKey];
35
+ return {
36
+ section_key: sectionKey,
37
+ media_type: override?.media_type ?? "application/octet-stream",
38
+ canonical_encoding: override?.canonical_encoding ?? "opaque-bytes-v1",
39
+ payload: override?.payload.slice() ?? new Uint8Array(0),
40
+ };
41
+ });
42
+ return createNeutralScenarioSource(input.scenario_id, sections, subtle);
43
+ }
44
+ export async function hashNeutralScenarioSource(source, subtle) {
45
+ const verified = await verifyNeutralScenarioSource(source, subtle);
46
+ return sha256Hex(await sha256Bytes(canonicalNeutralScenarioSourceBytes(verified), subtle));
47
+ }
48
+ export { canonicalNeutralScenarioSourceBytes, canonicalizeNeutralScenarioSource, };
@@ -0,0 +1,13 @@
1
+ export declare function assertStableIdentifier(value: unknown, path: string): asserts value is string;
2
+ export declare function assertSectionKey(value: unknown, path: string): asserts value is string;
3
+ export declare function assertDescriptorText(value: unknown, path: string): asserts value is string;
4
+ export declare function parseCanonicalByteLength(value: unknown, path: string): bigint;
5
+ export declare function assertSha256Bytes(value: unknown, path: string): asserts value is Uint8Array;
6
+ export declare function bytesEqual(left: Uint8Array, right: Uint8Array): boolean;
7
+ export declare function sha256Bytes(bytes: Uint8Array, subtle?: SubtleCrypto): Promise<Uint8Array>;
8
+ export declare function sha256Hex(bytes: Uint8Array): string;
9
+ export declare function assertUnknownFields(value: {
10
+ readonly _unknownFields?: {
11
+ [key: number]: Uint8Array[];
12
+ } | undefined;
13
+ }, path: string): void;
@@ -0,0 +1,78 @@
1
+ import { ScenarioSourceIntegrityError } from "./author-source.js";
2
+ const SHA256_BYTE_LENGTH = 32;
3
+ const SECTION_KEY_PATTERN = /^[a-z][a-z0-9_.-]{0,127}$/u;
4
+ const DECIMAL_UINT_PATTERN = /^(?:0|[1-9][0-9]*)$/u;
5
+ const MAX_PROTO_FIELD_NUMBER = 536_870_911;
6
+ export function assertStableIdentifier(value, path) {
7
+ if (typeof value !== "string" || value.length === 0 || value !== value.trim().normalize("NFC")) {
8
+ throw new ScenarioSourceIntegrityError("identifier_invalid", path);
9
+ }
10
+ }
11
+ export function assertSectionKey(value, path) {
12
+ if (typeof value !== "string" || !SECTION_KEY_PATTERN.test(value) || value !== value.normalize("NFC")) {
13
+ throw new ScenarioSourceIntegrityError("section_key_invalid", path);
14
+ }
15
+ }
16
+ export function assertDescriptorText(value, path) {
17
+ if (typeof value !== "string"
18
+ || value.length === 0
19
+ || value.length > 255
20
+ || value !== value.trim().normalize("NFC")) {
21
+ throw new ScenarioSourceIntegrityError("section_descriptor_invalid", path);
22
+ }
23
+ }
24
+ export function parseCanonicalByteLength(value, path) {
25
+ if (typeof value !== "string" || !DECIMAL_UINT_PATTERN.test(value)) {
26
+ throw new ScenarioSourceIntegrityError("byte_length_invalid", path);
27
+ }
28
+ try {
29
+ return BigInt(value);
30
+ }
31
+ catch {
32
+ throw new ScenarioSourceIntegrityError("byte_length_invalid", path);
33
+ }
34
+ }
35
+ export function assertSha256Bytes(value, path) {
36
+ if (!(value instanceof Uint8Array) || value.byteLength !== SHA256_BYTE_LENGTH) {
37
+ throw new ScenarioSourceIntegrityError("sha256_invalid", path);
38
+ }
39
+ }
40
+ export function bytesEqual(left, right) {
41
+ if (left.byteLength !== right.byteLength)
42
+ return false;
43
+ let difference = 0;
44
+ for (let index = 0; index < left.byteLength; index += 1) {
45
+ difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
46
+ }
47
+ return difference === 0;
48
+ }
49
+ export async function sha256Bytes(bytes, subtle = globalThis.crypto.subtle) {
50
+ const input = bytes.slice();
51
+ return new Uint8Array(await subtle.digest("SHA-256", input));
52
+ }
53
+ export function sha256Hex(bytes) {
54
+ return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
55
+ }
56
+ export function assertUnknownFields(value, path) {
57
+ if (value._unknownFields === undefined)
58
+ return;
59
+ if (typeof value._unknownFields !== "object" || value._unknownFields === null) {
60
+ throw new ScenarioSourceIntegrityError("unknown_fields_invalid", path);
61
+ }
62
+ for (const [rawTag, buffers] of Object.entries(value._unknownFields)) {
63
+ if (!/^[1-9][0-9]*$/u.test(rawTag) || !Array.isArray(buffers) || buffers.length === 0) {
64
+ throw new ScenarioSourceIntegrityError("unknown_fields_invalid", `${path}/${rawTag}`);
65
+ }
66
+ const tag = Number(rawTag);
67
+ const fieldNumber = Math.floor(tag / 8);
68
+ const wireType = tag & 7;
69
+ if (!Number.isSafeInteger(tag)
70
+ || tag > 0xffff_ffff
71
+ || fieldNumber < 1
72
+ || fieldNumber > MAX_PROTO_FIELD_NUMBER
73
+ || ![0, 1, 2, 3, 5].includes(wireType)
74
+ || buffers.some((buffer) => !(buffer instanceof Uint8Array))) {
75
+ throw new ScenarioSourceIntegrityError("unknown_fields_invalid", `${path}/${rawTag}`);
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,4 @@
1
+ import type { NeutralScenarioSource, ScenarioSourceManifest, ScenarioSourceSection } from "./author-source.js";
2
+ export declare function buildScenarioSourceManifest(scenarioId: string, sections: readonly ScenarioSourceSection[], subtle?: SubtleCrypto): Promise<ScenarioSourceManifest>;
3
+ export declare function verifyScenarioSourceManifest(manifest: ScenarioSourceManifest, sections: readonly ScenarioSourceSection[], expectedScenarioId?: string, subtle?: SubtleCrypto): Promise<ScenarioSourceManifest>;
4
+ export declare function verifyNeutralScenarioSource(source: NeutralScenarioSource, subtle?: SubtleCrypto): Promise<NeutralScenarioSource>;
@@ -0,0 +1,131 @@
1
+ import { NeutralScenarioSource as NeutralScenarioSourceCodec, ScenarioSourceSection as ScenarioSourceSectionCodec, } from "@world-engines/protocol-ts/v1/neutral_scenario_source";
2
+ import { NEUTRAL_SCENARIO_SOURCE_SCHEMA_VERSION, REQUIRED_SCENARIO_SOURCE_SECTION_KEYS, SCENARIO_SOURCE_MANIFEST_SCHEMA_VERSION, ScenarioSourceIntegrityError, } from "./author-source.js";
3
+ import { canonicalizeNeutralScenarioSource, canonicalizeScenarioSourceManifest, canonicalNeutralScenarioSourceBytes, canonicalScenarioSourceManifestDigestInputBytes, canonicalScenarioSourceSectionBytes, } from "./canonicalize.js";
4
+ import { assertDescriptorText, assertSectionKey, assertSha256Bytes, assertStableIdentifier, assertUnknownFields, bytesEqual, parseCanonicalByteLength, sha256Bytes, } from "./integrity.js";
5
+ function assertUniqueCompleteSectionKeys(keys, path) {
6
+ const seen = new Set();
7
+ for (const [index, key] of keys.entries()) {
8
+ assertSectionKey(key, `${path}/${index}/section_key`);
9
+ if (seen.has(key)) {
10
+ throw new ScenarioSourceIntegrityError("section_key_duplicate", `${path}/${index}/section_key`);
11
+ }
12
+ seen.add(key);
13
+ }
14
+ for (const required of REQUIRED_SCENARIO_SOURCE_SECTION_KEYS) {
15
+ if (!seen.has(required)) {
16
+ throw new ScenarioSourceIntegrityError("required_section_missing", `${path}/${required}`);
17
+ }
18
+ }
19
+ }
20
+ function validateSection(section, index) {
21
+ const path = `$/sections/${index}`;
22
+ assertSectionKey(section.section_key, `${path}/section_key`);
23
+ assertDescriptorText(section.media_type, `${path}/media_type`);
24
+ assertDescriptorText(section.canonical_encoding, `${path}/canonical_encoding`);
25
+ if (!(section.payload instanceof Uint8Array)) {
26
+ throw new ScenarioSourceIntegrityError("section_payload_invalid", `${path}/payload`);
27
+ }
28
+ assertUnknownFields(section, `${path}/_unknownFields`);
29
+ try {
30
+ ScenarioSourceSectionCodec.decode(canonicalScenarioSourceSectionBytes(section));
31
+ }
32
+ catch {
33
+ throw new ScenarioSourceIntegrityError("section_wire_invalid", path);
34
+ }
35
+ }
36
+ async function buildSectionIdentity(section, subtle) {
37
+ const bytes = canonicalScenarioSourceSectionBytes(section);
38
+ return Object.freeze({
39
+ section_key: section.section_key,
40
+ media_type: section.media_type,
41
+ canonical_encoding: section.canonical_encoding,
42
+ byte_length: String(bytes.byteLength),
43
+ sha256: await sha256Bytes(bytes, subtle),
44
+ });
45
+ }
46
+ export async function buildScenarioSourceManifest(scenarioId, sections, subtle) {
47
+ assertStableIdentifier(scenarioId, "$/scenario_id");
48
+ sections.forEach(validateSection);
49
+ assertUniqueCompleteSectionKeys(sections.map((section) => section.section_key), "$/sections");
50
+ const identities = await Promise.all(sections.map((section) => buildSectionIdentity(section, subtle)));
51
+ const core = {
52
+ manifest_schema_version: SCENARIO_SOURCE_MANIFEST_SCHEMA_VERSION,
53
+ scenario_id: scenarioId,
54
+ sections: identities,
55
+ manifest_sha256: new Uint8Array(0),
56
+ };
57
+ const canonicalCore = canonicalizeScenarioSourceManifest(core, true);
58
+ return canonicalizeScenarioSourceManifest({
59
+ ...canonicalCore,
60
+ manifest_sha256: await sha256Bytes(canonicalScenarioSourceManifestDigestInputBytes(canonicalCore), subtle),
61
+ });
62
+ }
63
+ export async function verifyScenarioSourceManifest(manifest, sections, expectedScenarioId = manifest.scenario_id, subtle) {
64
+ assertUnknownFields(manifest, "$/manifest/_unknownFields");
65
+ if (manifest.manifest_schema_version !== SCENARIO_SOURCE_MANIFEST_SCHEMA_VERSION) {
66
+ throw new ScenarioSourceIntegrityError("manifest_schema_version_invalid", "$/manifest/manifest_schema_version");
67
+ }
68
+ assertStableIdentifier(manifest.scenario_id, "$/manifest/scenario_id");
69
+ assertStableIdentifier(expectedScenarioId, "$/scenario_id");
70
+ if (manifest.scenario_id !== expectedScenarioId) {
71
+ throw new ScenarioSourceIntegrityError("manifest_scenario_mismatch", "$/manifest/scenario_id");
72
+ }
73
+ assertSha256Bytes(manifest.manifest_sha256, "$/manifest/manifest_sha256");
74
+ sections.forEach(validateSection);
75
+ assertUniqueCompleteSectionKeys(sections.map((section) => section.section_key), "$/sections");
76
+ assertUniqueCompleteSectionKeys(manifest.sections.map((identity) => identity.section_key), "$/manifest/sections");
77
+ if (manifest.sections.length !== sections.length) {
78
+ throw new ScenarioSourceIntegrityError("manifest_section_count_mismatch", "$/manifest/sections");
79
+ }
80
+ const sectionByKey = new Map(sections.map((section) => [section.section_key, section]));
81
+ for (const [index, identity] of manifest.sections.entries()) {
82
+ const path = `$/manifest/sections/${index}`;
83
+ assertDescriptorText(identity.media_type, `${path}/media_type`);
84
+ assertDescriptorText(identity.canonical_encoding, `${path}/canonical_encoding`);
85
+ assertUnknownFields(identity, `${path}/_unknownFields`);
86
+ assertSha256Bytes(identity.sha256, `${path}/sha256`);
87
+ const byteLength = parseCanonicalByteLength(identity.byte_length, `${path}/byte_length`);
88
+ const section = sectionByKey.get(identity.section_key);
89
+ if (section === undefined) {
90
+ throw new ScenarioSourceIntegrityError("manifest_section_missing", `${path}/section_key`);
91
+ }
92
+ if (identity.media_type !== section.media_type
93
+ || identity.canonical_encoding !== section.canonical_encoding) {
94
+ throw new ScenarioSourceIntegrityError("manifest_section_descriptor_mismatch", path);
95
+ }
96
+ const sectionBytes = canonicalScenarioSourceSectionBytes(section);
97
+ const sectionSha256 = await sha256Bytes(sectionBytes, subtle);
98
+ if (byteLength !== BigInt(sectionBytes.byteLength)
99
+ || !bytesEqual(identity.sha256, sectionSha256)) {
100
+ throw new ScenarioSourceIntegrityError("manifest_section_identity_mismatch", path);
101
+ }
102
+ }
103
+ const expectedDigest = await sha256Bytes(canonicalScenarioSourceManifestDigestInputBytes(manifest), subtle);
104
+ if (!bytesEqual(manifest.manifest_sha256, expectedDigest)) {
105
+ throw new ScenarioSourceIntegrityError("manifest_digest_mismatch", "$/manifest/manifest_sha256");
106
+ }
107
+ return canonicalizeScenarioSourceManifest(manifest);
108
+ }
109
+ export async function verifyNeutralScenarioSource(source, subtle) {
110
+ assertUnknownFields(source, "$/_unknownFields");
111
+ if (source.source_schema_version !== NEUTRAL_SCENARIO_SOURCE_SCHEMA_VERSION) {
112
+ throw new ScenarioSourceIntegrityError("source_schema_version_invalid", "$/source_schema_version");
113
+ }
114
+ assertStableIdentifier(source.scenario_id, "$/scenario_id");
115
+ if (source.manifest === undefined) {
116
+ throw new ScenarioSourceIntegrityError("source_manifest_missing", "$/manifest");
117
+ }
118
+ await verifyScenarioSourceManifest(source.manifest, source.sections, source.scenario_id, subtle);
119
+ const canonical = canonicalizeNeutralScenarioSource(source);
120
+ const canonicalBytes = canonicalNeutralScenarioSourceBytes(canonical);
121
+ try {
122
+ const decoded = NeutralScenarioSourceCodec.decode(canonicalBytes);
123
+ if (!bytesEqual(canonicalBytes, canonicalNeutralScenarioSourceBytes(decoded))) {
124
+ throw new Error("non_exact");
125
+ }
126
+ }
127
+ catch {
128
+ throw new ScenarioSourceIntegrityError("source_wire_invalid", "$");
129
+ }
130
+ return canonical;
131
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@world-engines/scenario-author-source",
3
+ "private": false,
4
+ "version": "0.1.0-alpha.0",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "dependencies": {
17
+ "@world-engines/protocol-ts": "0.1.0-alpha.0"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "worldengine_source_sha256": "004d61441204b40d0272737cad6611562a9b4d3786cc9e1f0d516e5d452097bf",
23
+ "worldengine_source_identity_provenance": {
24
+ "schema": "worldengine-public-namespace-projection/v1",
25
+ "kind": "composite-source-identity; namespace projection; not-full-source-recompile",
26
+ "original_name": "@chat/scenario-author-source",
27
+ "original_archive_sha256": "3769ee18906b258e4028bbf5f1ad01b81055ca6a86cd9dee08ef921147e1f743",
28
+ "original_source_sha256": "13c4e329bd8dd47c90550b434f1aba0f8436afc32c7cdef2786ce12e394fd706",
29
+ "projected_name": "@world-engines/scenario-author-source",
30
+ "package_name_mapping": {
31
+ "@chat/blocks-editor": "@world-engines/blocks-editor",
32
+ "@chat/ladybug-bridge": "@world-engines/ladybug-bridge",
33
+ "@chat/monaco-host": "@world-engines/monaco-host",
34
+ "@chat/protocol-ts": "@world-engines/protocol-ts",
35
+ "@chat/scenario-author-source": "@world-engines/scenario-author-source",
36
+ "@chat/scenario-review-snapshot": "@world-engines/scenario-review-snapshot",
37
+ "@chat/tmw": "@world-engines/tmw",
38
+ "@chat/view-exposure": "@world-engines/view-exposure",
39
+ "@world-engines/chatplay-vite-plugin": "@world-engines/chatplay-vite-plugin",
40
+ "@worldengine/agent-kit": "@world-engines/agent-kit",
41
+ "@worldengine/authoring-bridge": "@world-engines/authoring-bridge",
42
+ "@worldengine/authoring-ui": "@world-engines/authoring-ui",
43
+ "@worldengine/create-project": "@world-engines/create-project",
44
+ "@worldengine/project-format": "@world-engines/project-format",
45
+ "@worldengine/project-host": "@world-engines/project-host",
46
+ "@worldengine/project-setup": "@world-engines/project-setup",
47
+ "@worldengine/spatial-authoring": "@world-engines/spatial-authoring",
48
+ "@worldengine/view-sdk": "@world-engines/view-sdk"
49
+ }
50
+ }
51
+ }