@feralfile/cli 1.1.1
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 +21 -0
- package/README.md +96 -0
- package/config.json.example +96 -0
- package/dist/index.js +54 -0
- package/dist/src/ai-orchestrator/index.js +1019 -0
- package/dist/src/ai-orchestrator/registry.js +96 -0
- package/dist/src/commands/build.js +69 -0
- package/dist/src/commands/chat.js +189 -0
- package/dist/src/commands/config.js +68 -0
- package/dist/src/commands/device.js +278 -0
- package/dist/src/commands/helpers/config-files.js +62 -0
- package/dist/src/commands/helpers/device-discovery.js +111 -0
- package/dist/src/commands/helpers/playlist-display.js +161 -0
- package/dist/src/commands/helpers/prompt.js +65 -0
- package/dist/src/commands/helpers/ssh-helpers.js +44 -0
- package/dist/src/commands/play.js +110 -0
- package/dist/src/commands/publish.js +115 -0
- package/dist/src/commands/setup.js +225 -0
- package/dist/src/commands/sign.js +41 -0
- package/dist/src/commands/ssh.js +108 -0
- package/dist/src/commands/status.js +126 -0
- package/dist/src/commands/validate.js +18 -0
- package/dist/src/config.js +441 -0
- package/dist/src/intent-parser/index.js +1382 -0
- package/dist/src/intent-parser/utils.js +108 -0
- package/dist/src/logger.js +82 -0
- package/dist/src/main.js +459 -0
- package/dist/src/types.js +5 -0
- package/dist/src/utilities/address-validator.js +242 -0
- package/dist/src/utilities/device-default.js +36 -0
- package/dist/src/utilities/device-lookup.js +107 -0
- package/dist/src/utilities/device-normalize.js +62 -0
- package/dist/src/utilities/device-upsert.js +91 -0
- package/dist/src/utilities/domain-resolver.js +291 -0
- package/dist/src/utilities/ed25519-key-derive.js +155 -0
- package/dist/src/utilities/feed-fetcher.js +471 -0
- package/dist/src/utilities/ff1-compatibility.js +269 -0
- package/dist/src/utilities/ff1-device.js +250 -0
- package/dist/src/utilities/ff1-discovery.js +330 -0
- package/dist/src/utilities/functions.js +308 -0
- package/dist/src/utilities/index.js +469 -0
- package/dist/src/utilities/nft-indexer.js +1024 -0
- package/dist/src/utilities/playlist-builder.js +523 -0
- package/dist/src/utilities/playlist-publisher.js +131 -0
- package/dist/src/utilities/playlist-send.js +260 -0
- package/dist/src/utilities/playlist-signer.js +204 -0
- package/dist/src/utilities/playlist-signing-role.js +41 -0
- package/dist/src/utilities/playlist-source.js +128 -0
- package/dist/src/utilities/playlist-verifier.js +274 -0
- package/dist/src/utilities/ssh-access.js +145 -0
- package/dist/src/utils.js +48 -0
- package/docs/CONFIGURATION.md +206 -0
- package/docs/EXAMPLES.md +390 -0
- package/docs/FUNCTION_CALLING.md +96 -0
- package/docs/PROJECT_SPEC.md +228 -0
- package/docs/README.md +348 -0
- package/docs/RELEASING.md +73 -0
- package/package.json +76 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Domain Resolution Utilities
|
|
4
|
+
* Resolves blockchain domain names (ENS, TNS) to their corresponding addresses
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
40
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.resolveDomain = resolveDomain;
|
|
44
|
+
exports.resolveDomainsBatch = resolveDomainsBatch;
|
|
45
|
+
exports.displayResolutionResults = displayResolutionResults;
|
|
46
|
+
const viem_1 = require("viem");
|
|
47
|
+
const chains_1 = require("viem/chains");
|
|
48
|
+
const ens_1 = require("viem/ens");
|
|
49
|
+
const axios_1 = __importDefault(require("axios"));
|
|
50
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
51
|
+
const logger = __importStar(require("../logger"));
|
|
52
|
+
/**
|
|
53
|
+
* ENS resolver using viem
|
|
54
|
+
*/
|
|
55
|
+
class ENSResolver {
|
|
56
|
+
client;
|
|
57
|
+
constructor() {
|
|
58
|
+
this.client = (0, viem_1.createPublicClient)({
|
|
59
|
+
chain: chains_1.mainnet,
|
|
60
|
+
transport: (0, viem_1.http)(),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve an ENS domain to its Ethereum address
|
|
65
|
+
*
|
|
66
|
+
* @param {string} domain - ENS domain (e.g., 'vitalik.eth')
|
|
67
|
+
* @returns {Promise<string|null>} Resolved address or null
|
|
68
|
+
*/
|
|
69
|
+
async resolve(domain) {
|
|
70
|
+
try {
|
|
71
|
+
const address = await this.client.getEnsAddress({
|
|
72
|
+
name: (0, ens_1.normalize)(domain),
|
|
73
|
+
});
|
|
74
|
+
return address;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
logger.debug(`ENS resolution failed for ${domain}: ${error}`);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* TNS (Tezos Name Service) resolver using Tezos Domains GraphQL API
|
|
84
|
+
*
|
|
85
|
+
* Uses the official Tezos Domains API for reliable resolution
|
|
86
|
+
* API: https://api.tezos.domains/graphql
|
|
87
|
+
*/
|
|
88
|
+
class TNSResolver {
|
|
89
|
+
apiUrl;
|
|
90
|
+
constructor() {
|
|
91
|
+
this.apiUrl = 'https://api.tezos.domains/graphql';
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolve a TNS domain to its Tezos address using GraphQL API
|
|
95
|
+
*
|
|
96
|
+
* @param {string} domain - TNS domain (e.g., 'alice.tez', 'einstein-rosen.tez')
|
|
97
|
+
* @returns {Promise<string|null>} Resolved address or null
|
|
98
|
+
*/
|
|
99
|
+
async resolve(domain) {
|
|
100
|
+
try {
|
|
101
|
+
// GraphQL query to resolve domain (using GET request)
|
|
102
|
+
// Note: API only supports 'address' field, not 'expiry'
|
|
103
|
+
const query = `{ domain(name: "${domain}") { address } }`;
|
|
104
|
+
const response = await axios_1.default.get(this.apiUrl, {
|
|
105
|
+
params: { query },
|
|
106
|
+
timeout: 10000,
|
|
107
|
+
});
|
|
108
|
+
if (response.data?.errors) {
|
|
109
|
+
logger.debug(`TNS API returned errors for ${domain}: ${JSON.stringify(response.data.errors)}`);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const domainData = response.data?.data?.domain;
|
|
113
|
+
if (!domainData || !domainData.address) {
|
|
114
|
+
logger.debug(`TNS domain ${domain} not found - domainData: ${JSON.stringify(domainData)}`);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
logger.debug(`TNS resolved ${domain} → ${domainData.address}`);
|
|
118
|
+
return domainData.address;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
122
|
+
logger.debug(`TNS resolution failed for ${domain}: ${errorMessage}`);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Determine domain type based on TLD
|
|
129
|
+
*
|
|
130
|
+
* @param {string} domain - Domain name
|
|
131
|
+
* @returns {string|null} Domain type ('ens', 'tns') or null
|
|
132
|
+
*/
|
|
133
|
+
function getDomainType(domain) {
|
|
134
|
+
const normalizedDomain = domain.toLowerCase();
|
|
135
|
+
if (normalizedDomain.endsWith('.eth')) {
|
|
136
|
+
return 'ens';
|
|
137
|
+
}
|
|
138
|
+
else if (normalizedDomain.endsWith('.tez')) {
|
|
139
|
+
return 'tns';
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Validate domain name format
|
|
145
|
+
*
|
|
146
|
+
* @param {string} domain - Domain to validate
|
|
147
|
+
* @returns {boolean} Whether domain is valid
|
|
148
|
+
*/
|
|
149
|
+
function isValidDomain(domain) {
|
|
150
|
+
if (!domain || typeof domain !== 'string') {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const trimmedDomain = domain.trim();
|
|
154
|
+
if (trimmedDomain.length === 0) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const domainType = getDomainType(trimmedDomain);
|
|
158
|
+
return domainType !== null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Resolve a single domain to its blockchain address
|
|
162
|
+
*
|
|
163
|
+
* @param {string} domain - Domain name to resolve (e.g., 'vitalik.eth', 'alice.tez')
|
|
164
|
+
* @returns {Promise<DomainResolution>} Resolution result
|
|
165
|
+
* @example
|
|
166
|
+
* const result = await resolveDomain('vitalik.eth');
|
|
167
|
+
* if (result.resolved) {
|
|
168
|
+
* console.log(`${result.domain} -> ${result.address}`);
|
|
169
|
+
* }
|
|
170
|
+
*/
|
|
171
|
+
async function resolveDomain(domain) {
|
|
172
|
+
const trimmedDomain = domain.trim();
|
|
173
|
+
// Validate domain
|
|
174
|
+
if (!isValidDomain(trimmedDomain)) {
|
|
175
|
+
return {
|
|
176
|
+
domain: trimmedDomain,
|
|
177
|
+
address: null,
|
|
178
|
+
resolved: false,
|
|
179
|
+
error: `Invalid or unsupported domain: ${trimmedDomain}`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const domainType = getDomainType(trimmedDomain);
|
|
183
|
+
try {
|
|
184
|
+
let address = null;
|
|
185
|
+
if (domainType === 'ens') {
|
|
186
|
+
const ensResolver = new ENSResolver();
|
|
187
|
+
address = await ensResolver.resolve(trimmedDomain);
|
|
188
|
+
}
|
|
189
|
+
else if (domainType === 'tns') {
|
|
190
|
+
const tnsResolver = new TNSResolver();
|
|
191
|
+
address = await tnsResolver.resolve(trimmedDomain);
|
|
192
|
+
}
|
|
193
|
+
if (!address) {
|
|
194
|
+
return {
|
|
195
|
+
domain: trimmedDomain,
|
|
196
|
+
address: null,
|
|
197
|
+
resolved: false,
|
|
198
|
+
error: `Could not resolve ${trimmedDomain}`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
domain: trimmedDomain,
|
|
203
|
+
address,
|
|
204
|
+
resolved: true,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error during resolution';
|
|
209
|
+
logger.debug(`Domain resolution error for ${trimmedDomain}: ${errorMessage}`);
|
|
210
|
+
return {
|
|
211
|
+
domain: trimmedDomain,
|
|
212
|
+
address: null,
|
|
213
|
+
resolved: false,
|
|
214
|
+
error: errorMessage,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Resolve multiple domains in batch (concurrent processing)
|
|
220
|
+
*
|
|
221
|
+
* Supports ENS (.eth) and TNS (.tez) domains.
|
|
222
|
+
* Processes all domains concurrently for optimal performance.
|
|
223
|
+
*
|
|
224
|
+
* @param {string[]} domains - Array of domain names to resolve
|
|
225
|
+
* @returns {Promise<BatchResolutionResult>} Batch resolution result with domain->address map
|
|
226
|
+
* @example
|
|
227
|
+
* const result = await resolveDomainsBatch(['vitalik.eth', 'alice.tez']);
|
|
228
|
+
* if (result.success) {
|
|
229
|
+
* console.log(result.domainMap); // { 'vitalik.eth': '0x...', 'alice.tez': 'tz...' }
|
|
230
|
+
* }
|
|
231
|
+
*/
|
|
232
|
+
async function resolveDomainsBatch(domains) {
|
|
233
|
+
// Validate input
|
|
234
|
+
if (!Array.isArray(domains) || domains.length === 0) {
|
|
235
|
+
return {
|
|
236
|
+
success: false,
|
|
237
|
+
resolutions: [],
|
|
238
|
+
domainMap: {},
|
|
239
|
+
errors: ['No domains provided for resolution'],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
logger.debug(`Resolving ${domains.length} domains in batch...`);
|
|
243
|
+
// Resolve all domains concurrently
|
|
244
|
+
const resolutionPromises = domains.map((domain) => resolveDomain(domain));
|
|
245
|
+
const resolutions = await Promise.all(resolutionPromises);
|
|
246
|
+
// Build domain map and collect errors
|
|
247
|
+
const domainMap = {};
|
|
248
|
+
const errors = [];
|
|
249
|
+
for (const resolution of resolutions) {
|
|
250
|
+
if (resolution.resolved && resolution.address) {
|
|
251
|
+
domainMap[resolution.domain] = resolution.address;
|
|
252
|
+
}
|
|
253
|
+
else if (resolution.error) {
|
|
254
|
+
errors.push(`${resolution.domain}: ${resolution.error}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const successfulResolutions = resolutions.filter((r) => r.resolved).length;
|
|
258
|
+
logger.debug(`Batch resolution complete: ${successfulResolutions}/${domains.length} successful`);
|
|
259
|
+
return {
|
|
260
|
+
success: successfulResolutions > 0,
|
|
261
|
+
resolutions,
|
|
262
|
+
domainMap,
|
|
263
|
+
errors,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Display batch resolution results in a user-friendly format
|
|
268
|
+
*
|
|
269
|
+
* @param {BatchResolutionResult} result - Batch resolution result
|
|
270
|
+
*/
|
|
271
|
+
function displayResolutionResults(result) {
|
|
272
|
+
if (result.resolutions.length === 0) {
|
|
273
|
+
console.log(chalk_1.default.yellow('No names to resolve'));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// Display successful resolutions
|
|
277
|
+
const successful = result.resolutions.filter((r) => r.resolved);
|
|
278
|
+
if (successful.length > 0) {
|
|
279
|
+
successful.forEach((resolution) => {
|
|
280
|
+
console.log(chalk_1.default.dim(` ${resolution.domain} → ${resolution.address}`));
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
// Display failures (but don't make them too prominent)
|
|
284
|
+
const failed = result.resolutions.filter((r) => !r.resolved);
|
|
285
|
+
if (failed.length > 0) {
|
|
286
|
+
failed.forEach((resolution) => {
|
|
287
|
+
console.log(chalk_1.default.yellow(` ${resolution.domain}: ${resolution.error || 'Could not resolve'}`));
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
console.log();
|
|
291
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeVerifyPublicKeyToPem = normalizeVerifyPublicKeyToPem;
|
|
4
|
+
exports.parsePlaylistPrivateKeyToKeyObject = parsePlaylistPrivateKeyToKeyObject;
|
|
5
|
+
exports.deriveEd25519PublicKeyForVerify = deriveEd25519PublicKeyForVerify;
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
/**
|
|
8
|
+
* RFC 5480-style SubjectPublicKeyInfo prefix for Ed25519 public keys (raw 32 octets in BIT STRING).
|
|
9
|
+
*/
|
|
10
|
+
const ED25519_SPKI_RAW_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
11
|
+
function rawEd25519PublicBytesToPem(raw32) {
|
|
12
|
+
if (raw32.length !== 32) {
|
|
13
|
+
throw new Error('Ed25519 raw public key must be 32 bytes');
|
|
14
|
+
}
|
|
15
|
+
const der = Buffer.concat([ED25519_SPKI_RAW_PREFIX, raw32]);
|
|
16
|
+
const pub = (0, node_crypto_1.createPublicKey)({ key: der, format: 'der', type: 'spki' });
|
|
17
|
+
assertEd25519Public(pub);
|
|
18
|
+
return pub.export({ format: 'pem', type: 'spki' }).toString();
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* normalizeVerifyPublicKeyToPem interprets `--public-key` values documented for `verify`:
|
|
22
|
+
* PEM SPKI, 64-character hex (optional `0x`), standard base64 of exactly 32 raw bytes,
|
|
23
|
+
* or DER SPKI as base64 (typical single-line export).
|
|
24
|
+
*
|
|
25
|
+
* dp1-js expects PEM-friendly material in practice; this keeps CLI input aligned with docs.
|
|
26
|
+
*
|
|
27
|
+
* @param material - Non-empty key string from the CLI or config-derived PEM
|
|
28
|
+
* @returns PEM-encoded SPKI Ed25519 public key
|
|
29
|
+
* @throws Error if the material cannot be decoded as Ed25519 public key material
|
|
30
|
+
*/
|
|
31
|
+
function normalizeVerifyPublicKeyToPem(material) {
|
|
32
|
+
const trimmed = material.trim();
|
|
33
|
+
if (!trimmed) {
|
|
34
|
+
throw new Error('Public key material is empty');
|
|
35
|
+
}
|
|
36
|
+
if (trimmed.includes('BEGIN')) {
|
|
37
|
+
const pub = (0, node_crypto_1.createPublicKey)({ key: trimmed, format: 'pem' });
|
|
38
|
+
assertEd25519Public(pub);
|
|
39
|
+
return pub.export({ format: 'pem', type: 'spki' }).toString();
|
|
40
|
+
}
|
|
41
|
+
const hexCompact = trimmed.replace(/^0x/i, '').replace(/\s/g, '');
|
|
42
|
+
const hexRegex = /^[0-9a-fA-F]{64}$/;
|
|
43
|
+
if (hexRegex.test(hexCompact)) {
|
|
44
|
+
return rawEd25519PublicBytesToPem(Buffer.from(hexCompact, 'hex'));
|
|
45
|
+
}
|
|
46
|
+
const base64Regex = /^[A-Za-z0-9+/]+=*$/;
|
|
47
|
+
if (base64Regex.test(trimmed)) {
|
|
48
|
+
const buf = Buffer.from(trimmed, 'base64');
|
|
49
|
+
if (buf.length === 32) {
|
|
50
|
+
return rawEd25519PublicBytesToPem(buf);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const pub = (0, node_crypto_1.createPublicKey)({ key: buf, format: 'der', type: 'spki' });
|
|
54
|
+
assertEd25519Public(pub);
|
|
55
|
+
return pub.export({ format: 'pem', type: 'spki' }).toString();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// fall through
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw new Error('Unrecognized Ed25519 public key format for verify (expected PEM, 32-byte hex, 32-byte base64, or SPKI DER base64)');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* RFC 8410 PKCS#8 prefix for Ed25519: nested OCTET STRING holds the 32-byte seed.
|
|
65
|
+
* Used so 32-byte hex seeds work on Node versions that reject `type: 'raw'` imports.
|
|
66
|
+
*/
|
|
67
|
+
const ED25519_PKCS8_SEED_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');
|
|
68
|
+
function ed25519SeedBytesToPkcs8(seed32) {
|
|
69
|
+
if (seed32.length !== 32) {
|
|
70
|
+
throw new Error('Ed25519 seed must be 32 bytes');
|
|
71
|
+
}
|
|
72
|
+
return Buffer.concat([ED25519_PKCS8_SEED_PREFIX, seed32]);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* parsePlaylistPrivateKeyToKeyObject interprets playlist signing material the same way
|
|
76
|
+
* operators configure it: PKCS#8 DER as base64 (setup default), optional 32-byte raw
|
|
77
|
+
* seed as hex, PKCS#8 as hex, or PEM.
|
|
78
|
+
*
|
|
79
|
+
* @param material - Trimmed private key string from config or env
|
|
80
|
+
* @returns Node.js KeyObject for the Ed25519 private key
|
|
81
|
+
* @throws Error if the material cannot be parsed or is not Ed25519
|
|
82
|
+
*/
|
|
83
|
+
function parsePlaylistPrivateKeyToKeyObject(material) {
|
|
84
|
+
const trimmed = material.trim();
|
|
85
|
+
if (!trimmed) {
|
|
86
|
+
throw new Error('Private key material is empty');
|
|
87
|
+
}
|
|
88
|
+
if (trimmed.includes('BEGIN')) {
|
|
89
|
+
const key = (0, node_crypto_1.createPrivateKey)({ key: trimmed, format: 'pem' });
|
|
90
|
+
assertEd25519(key);
|
|
91
|
+
return key;
|
|
92
|
+
}
|
|
93
|
+
const base64Regex = /^[A-Za-z0-9+/]+=*$/;
|
|
94
|
+
const hexRegex = /^(0x)?[0-9a-fA-F]+$/;
|
|
95
|
+
if (base64Regex.test(trimmed)) {
|
|
96
|
+
const buf = Buffer.from(trimmed, 'base64');
|
|
97
|
+
if (buf.length === 0) {
|
|
98
|
+
throw new Error('Invalid base64 private key');
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const key = (0, node_crypto_1.createPrivateKey)({ key: buf, format: 'der', type: 'pkcs8' });
|
|
102
|
+
assertEd25519(key);
|
|
103
|
+
return key;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Continue to other strategies (e.g. hex path may apply for unusual configs)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (hexRegex.test(trimmed)) {
|
|
110
|
+
const raw = Buffer.from(trimmed.replace(/^0x/i, ''), 'hex');
|
|
111
|
+
if (raw.length === 32) {
|
|
112
|
+
try {
|
|
113
|
+
const der = ed25519SeedBytesToPkcs8(raw);
|
|
114
|
+
const key = (0, node_crypto_1.createPrivateKey)({ key: der, format: 'der', type: 'pkcs8' });
|
|
115
|
+
assertEd25519(key);
|
|
116
|
+
return key;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// fall through to full PKCS#8-in-hex
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const key = (0, node_crypto_1.createPrivateKey)({ key: raw, format: 'der', type: 'pkcs8' });
|
|
124
|
+
assertEd25519(key);
|
|
125
|
+
return key;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// fall through
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw new Error('Unrecognized Ed25519 private key format (expected PKCS#8 base64, 32-byte hex seed, PKCS#8 hex, or PEM)');
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* deriveEd25519PublicKeyForVerify exports the public half as PEM so legacy
|
|
135
|
+
* verification can hand Node a decoder-friendly public key string directly.
|
|
136
|
+
*
|
|
137
|
+
* @param privateKeyMaterial - Same encoding rules as `playlist.privateKey` / `PLAYLIST_PRIVATE_KEY`
|
|
138
|
+
* @returns PEM-encoded SPKI public key
|
|
139
|
+
*/
|
|
140
|
+
function deriveEd25519PublicKeyForVerify(privateKeyMaterial) {
|
|
141
|
+
const privateKey = parsePlaylistPrivateKeyToKeyObject(privateKeyMaterial);
|
|
142
|
+
const publicKey = (0, node_crypto_1.createPublicKey)(privateKey);
|
|
143
|
+
assertEd25519Public(publicKey);
|
|
144
|
+
return publicKey.export({ format: 'pem', type: 'spki' }).toString();
|
|
145
|
+
}
|
|
146
|
+
function assertEd25519(key) {
|
|
147
|
+
if (key.asymmetricKeyType !== 'ed25519') {
|
|
148
|
+
throw new Error('Configured private key must be an Ed25519 key');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function assertEd25519Public(key) {
|
|
152
|
+
if (key.asymmetricKeyType !== 'ed25519') {
|
|
153
|
+
throw new Error('Public key must be Ed25519');
|
|
154
|
+
}
|
|
155
|
+
}
|