@morit/cli 1.0.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 +26 -0
- package/assets/plugin_contract.json +136 -0
- package/bin/morit.js +11 -0
- package/package.json +41 -0
- package/src/archive.js +243 -0
- package/src/cli.js +414 -0
- package/src/cloud-client.js +89 -0
- package/src/config.js +82 -0
- package/src/secure-store.js +130 -0
- package/src/workspace.js +1630 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @morit/cli
|
|
2
|
+
|
|
3
|
+
Official Morit Developer CLI. It owns the shared Plugin SDK, local/Cloud project
|
|
4
|
+
linking, authenticated synchronization, builds, and deployments. The separate
|
|
5
|
+
`@morit/plugin-mcp` package exposes these capabilities to AI clients over MCP.
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
npx @morit/cli login
|
|
9
|
+
npx @morit/cli plugin setup . --id com.example.plugin --name Example --publisher example
|
|
10
|
+
npx @morit/cli plugin add .
|
|
11
|
+
npx @morit/cli plugin validate .
|
|
12
|
+
npx @morit/cli plugin build .
|
|
13
|
+
npx @morit/cli plugin deploy . --visibility private
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`morit login` uses a browser device flow. Windows credentials are protected with
|
|
17
|
+
DPAPI, macOS uses Keychain, and Linux uses Secret Service through `secret-tool`.
|
|
18
|
+
CI can supply `MORIT_ACCESS_TOKEN` without writing it to disk.
|
|
19
|
+
|
|
20
|
+
The local link is stored in `morit-plugin.json`; it contains project identifiers
|
|
21
|
+
and a revision only, never credentials. Project source and `.mplg` files also
|
|
22
|
+
contain secret identifiers only. Values belong in Morit Cloud Project Secrets.
|
|
23
|
+
|
|
24
|
+
Binary PNG/JPEG/WebP/GIF assets and direct `children/*.mplg` dependencies are
|
|
25
|
+
encoded only while crossing the JSON Cloud boundary and are restored byte for
|
|
26
|
+
byte on pull, source ZIP, and build. Other source remains strict UTF-8.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sdk_version": "1.7.5",
|
|
3
|
+
"manifest_schema_versions": [1, 2],
|
|
4
|
+
"recommended_schema_version": 2,
|
|
5
|
+
"package_content_type": "application/vnd.morit.plugin+zip",
|
|
6
|
+
"max_package_bytes": 2097152,
|
|
7
|
+
"max_entries": 64,
|
|
8
|
+
"manifest_fields": [
|
|
9
|
+
"schema_version",
|
|
10
|
+
"id",
|
|
11
|
+
"name",
|
|
12
|
+
"icon",
|
|
13
|
+
"short_description",
|
|
14
|
+
"description",
|
|
15
|
+
"category",
|
|
16
|
+
"keywords",
|
|
17
|
+
"developer",
|
|
18
|
+
"homepage_url",
|
|
19
|
+
"privacy_policy_url",
|
|
20
|
+
"publisher",
|
|
21
|
+
"version",
|
|
22
|
+
"cloud_project_id",
|
|
23
|
+
"required_secrets",
|
|
24
|
+
"min_morit_version",
|
|
25
|
+
"max_morit_version",
|
|
26
|
+
"permissions",
|
|
27
|
+
"capabilities",
|
|
28
|
+
"ui_extensions",
|
|
29
|
+
"credentials",
|
|
30
|
+
"slash_commands",
|
|
31
|
+
"connectors",
|
|
32
|
+
"dependencies",
|
|
33
|
+
"data_policy"
|
|
34
|
+
],
|
|
35
|
+
"fragment_directories": {
|
|
36
|
+
"capabilities": {"target": "capabilities", "kind": null},
|
|
37
|
+
"tools": {"target": "capabilities", "kind": "tool"},
|
|
38
|
+
"skills": {"target": "capabilities", "kind": "skill"},
|
|
39
|
+
"search": {"target": "capabilities", "kind": "provider"},
|
|
40
|
+
"notifications": {"target": "capabilities", "kind": "notification"},
|
|
41
|
+
"background": {"target": "capabilities", "kind": "background"},
|
|
42
|
+
"ui": {"target": "ui_extensions", "kind": null},
|
|
43
|
+
"credentials": {"target": "credentials", "kind": null},
|
|
44
|
+
"slash_commands": {"target": "slash_commands", "kind": null}
|
|
45
|
+
},
|
|
46
|
+
"object_fields": {
|
|
47
|
+
"capability": ["id", "kind", "title", "description", "permissions", "timeout_seconds", "runtime"],
|
|
48
|
+
"ui_extension": ["id", "point", "title", "order", "permissions", "config"],
|
|
49
|
+
"credential": ["id", "label", "description", "kind", "required", "oauth_provider", "allow_multiple"],
|
|
50
|
+
"slash_command": ["id", "command", "title", "description", "capability", "argument_template"],
|
|
51
|
+
"connector": ["id", "kind", "label", "description", "credential_id", "cloud_connection_id", "cloud_secret_id", "endpoint", "timeout_seconds", "retry", "rate_limit"],
|
|
52
|
+
"developer": ["name", "url"],
|
|
53
|
+
"required_secret": ["id", "label", "description", "required"],
|
|
54
|
+
"dependency": ["id", "plugin_id", "required", "min_version", "max_version", "package_path", "exposed_capabilities"]
|
|
55
|
+
},
|
|
56
|
+
"capability_runtime_shorthand": ["adapter", "entrypoint", "parameters"],
|
|
57
|
+
"capability_kinds": ["tool", "skill", "provider", "background", "notification"],
|
|
58
|
+
"connector_kinds": ["https", "oauth", "api_key", "mcp"],
|
|
59
|
+
"credential_kinds": ["api_token", "oauth_access_token"],
|
|
60
|
+
"permissions": [
|
|
61
|
+
"network",
|
|
62
|
+
"user_data_read",
|
|
63
|
+
"user_data_write",
|
|
64
|
+
"attachments_read",
|
|
65
|
+
"file_write",
|
|
66
|
+
"ai_tool_call",
|
|
67
|
+
"notifications",
|
|
68
|
+
"background",
|
|
69
|
+
"account_read",
|
|
70
|
+
"storage",
|
|
71
|
+
"credentials"
|
|
72
|
+
],
|
|
73
|
+
"runtime_adapters": [
|
|
74
|
+
"text_stats",
|
|
75
|
+
"text_template",
|
|
76
|
+
"settings_search",
|
|
77
|
+
"notification_template",
|
|
78
|
+
"calendar_store",
|
|
79
|
+
"http_json",
|
|
80
|
+
"mcp_http",
|
|
81
|
+
"child_plugin",
|
|
82
|
+
"neis_school",
|
|
83
|
+
"sandbox_python"
|
|
84
|
+
],
|
|
85
|
+
"ui_points": ["screen", "surface", "menu", "action", "card", "settings", "workspace", "response"],
|
|
86
|
+
"ui_nodes": [
|
|
87
|
+
"column",
|
|
88
|
+
"row",
|
|
89
|
+
"wrap",
|
|
90
|
+
"grid",
|
|
91
|
+
"card",
|
|
92
|
+
"section",
|
|
93
|
+
"text",
|
|
94
|
+
"icon",
|
|
95
|
+
"divider",
|
|
96
|
+
"spacer",
|
|
97
|
+
"metric",
|
|
98
|
+
"progress",
|
|
99
|
+
"button",
|
|
100
|
+
"chip",
|
|
101
|
+
"list",
|
|
102
|
+
"timeline",
|
|
103
|
+
"calendar",
|
|
104
|
+
"chart",
|
|
105
|
+
"form",
|
|
106
|
+
"dialog",
|
|
107
|
+
"sheet",
|
|
108
|
+
"field",
|
|
109
|
+
"select",
|
|
110
|
+
"switch",
|
|
111
|
+
"empty"
|
|
112
|
+
],
|
|
113
|
+
"signature": {
|
|
114
|
+
"algorithm": "ed25519",
|
|
115
|
+
"embedded_public_key": true,
|
|
116
|
+
"install_policy": "valid signatures are installable; trusted publisher registration is optional"
|
|
117
|
+
},
|
|
118
|
+
"workflow": [
|
|
119
|
+
"research",
|
|
120
|
+
"develop",
|
|
121
|
+
"create-files",
|
|
122
|
+
"validate",
|
|
123
|
+
"preview",
|
|
124
|
+
"build",
|
|
125
|
+
"verify",
|
|
126
|
+
"install",
|
|
127
|
+
"return-artifacts"
|
|
128
|
+
],
|
|
129
|
+
"completion_requirements": [
|
|
130
|
+
"source files exist",
|
|
131
|
+
"validation succeeds",
|
|
132
|
+
"requested artifacts exist and are non-empty",
|
|
133
|
+
"artifact format can be reopened",
|
|
134
|
+
"package signature and Host manifest verification succeed"
|
|
135
|
+
]
|
|
136
|
+
}
|
package/bin/morit.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runCli } from "../src/cli.js";
|
|
4
|
+
|
|
5
|
+
runCli(process.argv.slice(2)).then(
|
|
6
|
+
(code) => { process.exitCode = code; },
|
|
7
|
+
(error) => {
|
|
8
|
+
process.stderr.write(`Morit CLI failed: ${error instanceof Error ? error.message : "unknown error"}\n`);
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
},
|
|
11
|
+
);
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@morit/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official Morit Developer CLI for Cloud Projects, validation, builds, and deployments",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"morit": "bin/morit.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/cli.js",
|
|
11
|
+
"./sdk": "./src/workspace.js",
|
|
12
|
+
"./cloud": "./src/cloud-client.js",
|
|
13
|
+
"./auth": "./src/secure-store.js",
|
|
14
|
+
"./project-config": "./src/config.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"assets",
|
|
18
|
+
"bin",
|
|
19
|
+
"src",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "node scripts/sync-contract.mjs",
|
|
24
|
+
"test": "node --test",
|
|
25
|
+
"prepack": "npm run build && npm test"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20.12"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"morit",
|
|
35
|
+
"developer",
|
|
36
|
+
"cli",
|
|
37
|
+
"plugin",
|
|
38
|
+
"deployment"
|
|
39
|
+
],
|
|
40
|
+
"license": "UNLICENSED"
|
|
41
|
+
}
|
package/src/archive.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHash,
|
|
3
|
+
createPrivateKey,
|
|
4
|
+
createPublicKey,
|
|
5
|
+
generateKeyPairSync,
|
|
6
|
+
sign,
|
|
7
|
+
verify,
|
|
8
|
+
} from "node:crypto";
|
|
9
|
+
import { deflateRawSync, inflateRawSync } from "node:zlib";
|
|
10
|
+
|
|
11
|
+
const ZIP_LOCAL = 0x04034b50;
|
|
12
|
+
const ZIP_CENTRAL = 0x02014b50;
|
|
13
|
+
const ZIP_END = 0x06054b50;
|
|
14
|
+
const DOS_DATE = 33;
|
|
15
|
+
const CRC_TABLE = Array.from({ length: 256 }, (_, start) => {
|
|
16
|
+
let value = start;
|
|
17
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
18
|
+
value = (value & 1) ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
19
|
+
}
|
|
20
|
+
return value >>> 0;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export function canonicalJson(value) {
|
|
24
|
+
return Buffer.from(JSON.stringify(sortJson(value)), "utf8");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sortJson(value) {
|
|
28
|
+
if (Array.isArray(value)) return value.map(sortJson);
|
|
29
|
+
if (value && typeof value === "object") {
|
|
30
|
+
return Object.fromEntries(
|
|
31
|
+
Object.keys(value).sort().map((key) => [key, sortJson(value[key])]),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function crc32(content) {
|
|
38
|
+
let value = 0xffffffff;
|
|
39
|
+
for (const byte of content) value = CRC_TABLE[(value ^ byte) & 0xff] ^ (value >>> 8);
|
|
40
|
+
return (value ^ 0xffffffff) >>> 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createZip(entries) {
|
|
44
|
+
const local = [];
|
|
45
|
+
const central = [];
|
|
46
|
+
let offset = 0;
|
|
47
|
+
for (const [name, rawContent] of [...entries.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
48
|
+
const nameBytes = Buffer.from(name, "utf8");
|
|
49
|
+
const content = Buffer.from(rawContent);
|
|
50
|
+
const compressed = deflateRawSync(content, { level: 9 });
|
|
51
|
+
const checksum = crc32(content);
|
|
52
|
+
const localHeader = Buffer.alloc(30);
|
|
53
|
+
localHeader.writeUInt32LE(ZIP_LOCAL, 0);
|
|
54
|
+
localHeader.writeUInt16LE(20, 4);
|
|
55
|
+
localHeader.writeUInt16LE(0x0800, 6);
|
|
56
|
+
localHeader.writeUInt16LE(8, 8);
|
|
57
|
+
localHeader.writeUInt16LE(0, 10);
|
|
58
|
+
localHeader.writeUInt16LE(DOS_DATE, 12);
|
|
59
|
+
localHeader.writeUInt32LE(checksum, 14);
|
|
60
|
+
localHeader.writeUInt32LE(compressed.length, 18);
|
|
61
|
+
localHeader.writeUInt32LE(content.length, 22);
|
|
62
|
+
localHeader.writeUInt16LE(nameBytes.length, 26);
|
|
63
|
+
localHeader.writeUInt16LE(0, 28);
|
|
64
|
+
local.push(localHeader, nameBytes, compressed);
|
|
65
|
+
|
|
66
|
+
const centralHeader = Buffer.alloc(46);
|
|
67
|
+
centralHeader.writeUInt32LE(ZIP_CENTRAL, 0);
|
|
68
|
+
centralHeader.writeUInt16LE(0x0314, 4);
|
|
69
|
+
centralHeader.writeUInt16LE(20, 6);
|
|
70
|
+
centralHeader.writeUInt16LE(0x0800, 8);
|
|
71
|
+
centralHeader.writeUInt16LE(8, 10);
|
|
72
|
+
centralHeader.writeUInt16LE(0, 12);
|
|
73
|
+
centralHeader.writeUInt16LE(DOS_DATE, 14);
|
|
74
|
+
centralHeader.writeUInt32LE(checksum, 16);
|
|
75
|
+
centralHeader.writeUInt32LE(compressed.length, 20);
|
|
76
|
+
centralHeader.writeUInt32LE(content.length, 24);
|
|
77
|
+
centralHeader.writeUInt16LE(nameBytes.length, 28);
|
|
78
|
+
centralHeader.writeUInt16LE(0, 30);
|
|
79
|
+
centralHeader.writeUInt16LE(0, 32);
|
|
80
|
+
centralHeader.writeUInt16LE(0, 34);
|
|
81
|
+
centralHeader.writeUInt16LE(0, 36);
|
|
82
|
+
centralHeader.writeUInt32LE((0o100644 * 65536) >>> 0, 38);
|
|
83
|
+
centralHeader.writeUInt32LE(offset, 42);
|
|
84
|
+
central.push(centralHeader, nameBytes);
|
|
85
|
+
offset += localHeader.length + nameBytes.length + compressed.length;
|
|
86
|
+
}
|
|
87
|
+
const localBytes = Buffer.concat(local);
|
|
88
|
+
const centralBytes = Buffer.concat(central);
|
|
89
|
+
const end = Buffer.alloc(22);
|
|
90
|
+
end.writeUInt32LE(ZIP_END, 0);
|
|
91
|
+
end.writeUInt16LE(0, 4);
|
|
92
|
+
end.writeUInt16LE(0, 6);
|
|
93
|
+
end.writeUInt16LE(entries.size, 8);
|
|
94
|
+
end.writeUInt16LE(entries.size, 10);
|
|
95
|
+
end.writeUInt32LE(centralBytes.length, 12);
|
|
96
|
+
end.writeUInt32LE(localBytes.length, 16);
|
|
97
|
+
end.writeUInt16LE(0, 20);
|
|
98
|
+
return Buffer.concat([localBytes, centralBytes, end]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function readZip(content) {
|
|
102
|
+
const input = Buffer.from(content);
|
|
103
|
+
const entries = new Map();
|
|
104
|
+
let offset = 0;
|
|
105
|
+
while (offset + 4 <= input.length && input.readUInt32LE(offset) === ZIP_LOCAL) {
|
|
106
|
+
if (offset + 30 > input.length) throw new Error("ZIP local header is truncated");
|
|
107
|
+
const flags = input.readUInt16LE(offset + 6);
|
|
108
|
+
const method = input.readUInt16LE(offset + 8);
|
|
109
|
+
const expectedCrc = input.readUInt32LE(offset + 14);
|
|
110
|
+
const compressedSize = input.readUInt32LE(offset + 18);
|
|
111
|
+
const size = input.readUInt32LE(offset + 22);
|
|
112
|
+
const nameLength = input.readUInt16LE(offset + 26);
|
|
113
|
+
const extraLength = input.readUInt16LE(offset + 28);
|
|
114
|
+
const nameStart = offset + 30;
|
|
115
|
+
const dataStart = nameStart + nameLength + extraLength;
|
|
116
|
+
const dataEnd = dataStart + compressedSize;
|
|
117
|
+
if (flags & 0x1 || flags & 0x8) throw new Error("Encrypted or data-descriptor ZIP entries are not supported");
|
|
118
|
+
if (![0, 8].includes(method)) throw new Error("ZIP entry uses an unsupported compression method");
|
|
119
|
+
if (!nameLength || dataStart > input.length || dataEnd > input.length) {
|
|
120
|
+
throw new Error("ZIP entry exceeds archive bounds");
|
|
121
|
+
}
|
|
122
|
+
const name = input.subarray(nameStart, nameStart + nameLength).toString("utf8");
|
|
123
|
+
if (entries.has(name)) throw new Error(`ZIP contains a duplicate entry: ${name}`);
|
|
124
|
+
const compressed = input.subarray(dataStart, dataEnd);
|
|
125
|
+
const value = method === 8 ? inflateRawSync(compressed) : Buffer.from(compressed);
|
|
126
|
+
if (value.length !== size) throw new Error(`Invalid ZIP entry size: ${name}`);
|
|
127
|
+
if (crc32(value) !== expectedCrc) throw new Error(`Invalid ZIP entry checksum: ${name}`);
|
|
128
|
+
entries.set(name, value);
|
|
129
|
+
offset = dataEnd;
|
|
130
|
+
}
|
|
131
|
+
if (entries.size === 0) throw new Error("ZIP contains no files");
|
|
132
|
+
if (offset + 4 > input.length || ![ZIP_CENTRAL, ZIP_END].includes(input.readUInt32LE(offset))) {
|
|
133
|
+
throw new Error("ZIP central directory is missing or archive has trailing local data");
|
|
134
|
+
}
|
|
135
|
+
return entries;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function signaturePayload(entries) {
|
|
139
|
+
const digest = createHash("sha256");
|
|
140
|
+
for (const name of [...entries.keys()].filter((value) => value !== "signature.json").sort()) {
|
|
141
|
+
const encoded = Buffer.from(name, "utf8");
|
|
142
|
+
const length = Buffer.alloc(4);
|
|
143
|
+
length.writeUInt32BE(encoded.length);
|
|
144
|
+
digest.update(length);
|
|
145
|
+
digest.update(encoded);
|
|
146
|
+
digest.update(createHash("sha256").update(entries.get(name)).digest());
|
|
147
|
+
}
|
|
148
|
+
return Buffer.concat([Buffer.from("morit-plugin-v1\0"), digest.digest()]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function generateSigningKey() {
|
|
152
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
153
|
+
return {
|
|
154
|
+
privatePem: privateKey.export({ type: "pkcs8", format: "pem" }),
|
|
155
|
+
publicRaw: Buffer.from(publicKey.export({ format: "jwk" }).x, "base64url"),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function signEntries(entries, publisher, privatePem) {
|
|
160
|
+
const privateKey = createPrivateKey(privatePem);
|
|
161
|
+
const publicRaw = Buffer.from(privateKey.export({ format: "jwk" }).x, "base64url");
|
|
162
|
+
const result = new Map(entries);
|
|
163
|
+
result.set(
|
|
164
|
+
"signature.json",
|
|
165
|
+
canonicalJson({
|
|
166
|
+
algorithm: "ed25519",
|
|
167
|
+
key_id: publisher,
|
|
168
|
+
public_key: publicRaw.toString("base64"),
|
|
169
|
+
signature: sign(null, signaturePayload(result), privateKey).toString("base64"),
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function verifySignedEntries(entries) {
|
|
176
|
+
const manifest = parseJsonEntry(entries, "manifest.json");
|
|
177
|
+
const signature = parseJsonEntry(entries, "signature.json");
|
|
178
|
+
const fields = Object.keys(signature).sort();
|
|
179
|
+
if (
|
|
180
|
+
signature.algorithm !== "ed25519"
|
|
181
|
+
|| ![
|
|
182
|
+
["algorithm", "key_id", "public_key", "signature"],
|
|
183
|
+
].some((expected) => expected.every((name, index) => fields[index] === name) && fields.length === expected.length)
|
|
184
|
+
) {
|
|
185
|
+
throw new Error("signature.json must contain an embedded Ed25519 signature");
|
|
186
|
+
}
|
|
187
|
+
if (!manifest.publisher || signature.key_id !== manifest.publisher) {
|
|
188
|
+
throw new Error("package publisher does not match signature key_id");
|
|
189
|
+
}
|
|
190
|
+
const publicRaw = strictBase64(signature.public_key, 32, "signature public key");
|
|
191
|
+
const signatureRaw = strictBase64(signature.signature, 64, "package signature");
|
|
192
|
+
const publicKey = createPublicKey({
|
|
193
|
+
key: {
|
|
194
|
+
kty: "OKP",
|
|
195
|
+
crv: "Ed25519",
|
|
196
|
+
x: publicRaw.toString("base64url"),
|
|
197
|
+
},
|
|
198
|
+
format: "jwk",
|
|
199
|
+
});
|
|
200
|
+
if (!verify(null, signaturePayload(entries), publicKey, signatureRaw)) {
|
|
201
|
+
throw new Error("package signature is invalid or package content was modified");
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
manifest,
|
|
205
|
+
signature: {
|
|
206
|
+
algorithm: "ed25519",
|
|
207
|
+
key_id: signature.key_id,
|
|
208
|
+
public_key_sha256: sha256(publicRaw),
|
|
209
|
+
verified: true,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseJsonEntry(entries, name) {
|
|
215
|
+
const content = entries.get(name);
|
|
216
|
+
if (!content) throw new Error(`package requires ${name}`);
|
|
217
|
+
try {
|
|
218
|
+
const value = JSON.parse(content.toString("utf8"));
|
|
219
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error();
|
|
220
|
+
return value;
|
|
221
|
+
} catch {
|
|
222
|
+
throw new Error(`${name} must contain a JSON object`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function strictBase64(value, expectedLength, label) {
|
|
227
|
+
if (
|
|
228
|
+
typeof value !== "string"
|
|
229
|
+
|| value.length % 4 !== 0
|
|
230
|
+
|| !/^[A-Za-z0-9+/]+={0,2}$/.test(value)
|
|
231
|
+
) {
|
|
232
|
+
throw new Error(`${label} is not valid base64`);
|
|
233
|
+
}
|
|
234
|
+
const decoded = Buffer.from(value, "base64");
|
|
235
|
+
if (decoded.length !== expectedLength || decoded.toString("base64") !== value) {
|
|
236
|
+
throw new Error(`${label} has an invalid length or encoding`);
|
|
237
|
+
}
|
|
238
|
+
return decoded;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function sha256(content) {
|
|
242
|
+
return createHash("sha256").update(content).digest("hex");
|
|
243
|
+
}
|