holosphere 2.0.0-alpha21 → 2.0.0-alpha23
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 +1 -2
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +61 -58
- package/dist/{index-B6-8KAQm.js → index-BEkCLOwI.js} +2 -2
- package/dist/{index-B6-8KAQm.js.map → index-BEkCLOwI.js.map} +1 -1
- package/dist/{index-D2WstuZJ.js → index-BEvX6DxG.js} +2 -2
- package/dist/{index-D2WstuZJ.js.map → index-BEvX6DxG.js.map} +1 -1
- package/dist/{index--QsHG_gD.cjs → index-BGTOiJ2Y.cjs} +2 -2
- package/dist/{index--QsHG_gD.cjs.map → index-BGTOiJ2Y.cjs.map} +1 -1
- package/dist/{index-COpLk9gL.cjs → index-BH1woZXL.cjs} +2 -2
- package/dist/{index-COpLk9gL.cjs.map → index-BH1woZXL.cjs.map} +1 -1
- package/dist/{index-BHptWysv.js → index-Cvxov2jv.js} +2970 -7753
- package/dist/index-Cvxov2jv.js.map +1 -0
- package/dist/index-vTKI_BAX.cjs +29 -0
- package/dist/index-vTKI_BAX.cjs.map +1 -0
- package/dist/{indexeddb-storage-wKG4mICM.cjs → indexeddb-storage-BmnCNnSg.cjs} +2 -2
- package/dist/{indexeddb-storage-wKG4mICM.cjs.map → indexeddb-storage-BmnCNnSg.cjs.map} +1 -1
- package/dist/{indexeddb-storage-kQ53UHEE.js → indexeddb-storage-MIFisaPy.js} +2 -2
- package/dist/{indexeddb-storage-kQ53UHEE.js.map → indexeddb-storage-MIFisaPy.js.map} +1 -1
- package/dist/{memory-storage-CGC8xM2G.cjs → memory-storage-BJjK3F4r.cjs} +2 -2
- package/dist/{memory-storage-CGC8xM2G.cjs.map → memory-storage-BJjK3F4r.cjs.map} +1 -1
- package/dist/{memory-storage-DnXCSbBl.js → memory-storage-DhHXdKQ-.js} +2 -2
- package/dist/{memory-storage-DnXCSbBl.js.map → memory-storage-DhHXdKQ-.js.map} +1 -1
- package/examples/demo.html +2 -29
- package/package.json +3 -8
- package/src/content/social-protocols.js +3 -59
- package/src/core/holosphere.js +16 -554
- package/src/crypto/nostr-utils.js +98 -1
- package/src/crypto/secp256k1.js +4 -393
- package/src/federation/discovery.js +7 -75
- package/src/federation/handshake.js +69 -202
- package/src/federation/hologram.js +222 -298
- package/src/federation/index.js +2 -9
- package/src/federation/registry.js +67 -1257
- package/src/federation/request-card.js +21 -35
- package/src/hierarchical/upcast.js +4 -9
- package/src/index.js +145 -296
- package/src/lib/federation-methods.js +370 -909
- package/src/storage/global-tables.js +1 -1
- package/src/storage/nostr-wrapper.js +9 -5
- package/src/subscriptions/manager.js +1 -1
- package/types/index.d.ts +145 -37
- package/bin/holosphere-activitypub.js +0 -158
- package/dist/2019-BzVkRcax.js +0 -6680
- package/dist/2019-BzVkRcax.js.map +0 -1
- package/dist/2019-C1hPR_Os.cjs +0 -8
- package/dist/2019-C1hPR_Os.cjs.map +0 -1
- package/dist/browser-BcmACE3G.js +0 -3058
- package/dist/browser-BcmACE3G.js.map +0 -1
- package/dist/browser-DaqYUTcG.cjs +0 -2
- package/dist/browser-DaqYUTcG.cjs.map +0 -1
- package/dist/index-BHptWysv.js.map +0 -1
- package/dist/index-CDlhzxT2.cjs +0 -29
- package/dist/index-CDlhzxT2.cjs.map +0 -1
- package/src/federation/capabilities.js +0 -46
- package/src/storage/backend-factory.js +0 -130
- package/src/storage/backend-interface.js +0 -161
- package/src/storage/backends/activitypub/server.js +0 -675
- package/src/storage/backends/activitypub-backend.js +0 -295
- package/src/storage/backends/gundb-backend.js +0 -875
- package/src/storage/backends/nostr-backend.js +0 -251
- package/src/storage/gun-async.js +0 -341
- package/src/storage/gun-auth.js +0 -373
- package/src/storage/gun-federation.js +0 -785
- package/src/storage/gun-references.js +0 -209
- package/src/storage/gun-schema.js +0 -306
- package/src/storage/gun-wrapper.js +0 -642
- package/src/storage/migration.js +0 -351
- package/src/storage/unified-storage.js +0 -161
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @module crypto/nostr-utils
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { nip04, nip44, nip19, getPublicKey as nostrGetPublicKey, finalizeEvent, verifyEvent as nostrVerifyEvent } from 'nostr-tools';
|
|
12
|
+
import { nip04, nip44, nip19, getPublicKey as nostrGetPublicKey, finalizeEvent, verifyEvent as nostrVerifyEvent, generateSecretKey } from 'nostr-tools';
|
|
13
13
|
|
|
14
14
|
// Re-export NDK types for consumers who want to use NDK directly
|
|
15
15
|
export { default as NDK, NDKEvent, NDKPrivateKeySigner, NDKUser, NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk';
|
|
@@ -113,6 +113,103 @@ export function npubToHex(npub) {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Convert a hex private key to nsec format.
|
|
118
|
+
* @param {string} hexPrivKey - Hex private key (64 characters)
|
|
119
|
+
* @returns {string} nsec-encoded private key (nsec1...)
|
|
120
|
+
*/
|
|
121
|
+
export function hexToNsec(hexPrivKey) {
|
|
122
|
+
try {
|
|
123
|
+
return nip19.nsecEncode(hexToBytes(hexPrivKey));
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.error('Failed to encode hex to nsec:', e);
|
|
126
|
+
return hexPrivKey; // Return original on error
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Convert nsec to hex private key.
|
|
132
|
+
* @param {string} nsec - nsec-encoded private key string
|
|
133
|
+
* @returns {string|null} 64-character hex private key or null if invalid
|
|
134
|
+
*/
|
|
135
|
+
export function nsecToHex(nsec) {
|
|
136
|
+
try {
|
|
137
|
+
const decoded = nip19.decode(nsec);
|
|
138
|
+
if (decoded.type === 'nsec') {
|
|
139
|
+
return bytesToHex(decoded.data);
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
} catch (e) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Parse an nsec or hex private key string into hex format.
|
|
149
|
+
* Handles nostr: URI prefix and validates both nsec and hex formats.
|
|
150
|
+
* @param {string} input - nsec string (nsec1...) or 64-character hex private key
|
|
151
|
+
* @returns {Object} Validation result with valid, hexPrivKey (if valid), error (if invalid)
|
|
152
|
+
*/
|
|
153
|
+
export function parseNsecOrHex(input) {
|
|
154
|
+
const trimmed = input?.trim();
|
|
155
|
+
|
|
156
|
+
if (!trimmed) {
|
|
157
|
+
return { valid: false, error: 'Private key is required' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check if it's already hex (64 characters)
|
|
161
|
+
if (/^[0-9a-fA-F]{64}$/.test(trimmed)) {
|
|
162
|
+
return { valid: true, hexPrivKey: trimmed.toLowerCase() };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Handle nostr: URI prefix
|
|
166
|
+
let nsecString = trimmed;
|
|
167
|
+
if (nsecString.startsWith('nostr:')) {
|
|
168
|
+
nsecString = nsecString.slice(6);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Try to decode as nsec
|
|
172
|
+
if (nsecString.startsWith('nsec1')) {
|
|
173
|
+
try {
|
|
174
|
+
const decoded = nip19.decode(nsecString);
|
|
175
|
+
if (decoded.type === 'nsec') {
|
|
176
|
+
return { valid: true, hexPrivKey: bytesToHex(decoded.data) };
|
|
177
|
+
}
|
|
178
|
+
return { valid: false, error: 'Invalid nsec format' };
|
|
179
|
+
} catch (e) {
|
|
180
|
+
return { valid: false, error: 'Invalid nsec: unable to decode' };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { valid: false, error: 'Enter a valid nsec (nsec1...) or 64-character hex private key' };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check if a string is a valid nsec (starts with nsec1 and decodes correctly).
|
|
189
|
+
* @param {string} str - String to validate
|
|
190
|
+
* @returns {boolean} True if valid nsec format
|
|
191
|
+
*/
|
|
192
|
+
export function isValidNsec(str) {
|
|
193
|
+
if (typeof str !== 'string' || !str.startsWith('nsec1')) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const decoded = nip19.decode(str);
|
|
198
|
+
return decoded.type === 'nsec';
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Generate a new random private key.
|
|
206
|
+
* @returns {string} 64-character hex private key
|
|
207
|
+
*/
|
|
208
|
+
export function generatePrivateKey() {
|
|
209
|
+
const secretKey = generateSecretKey();
|
|
210
|
+
return bytesToHex(secretKey);
|
|
211
|
+
}
|
|
212
|
+
|
|
116
213
|
/**
|
|
117
214
|
* Shorten a public key for display (first 8 and last 8 chars).
|
|
118
215
|
* @param {string} pubKey - Public key in hex or npub format
|
package/src/crypto/secp256k1.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Cryptographic Operations using secp256k1.
|
|
3
3
|
*
|
|
4
|
-
* Provides signing
|
|
5
|
-
*
|
|
6
|
-
* Uses lazy loading for the crypto module to improve startup performance.
|
|
4
|
+
* Provides signing and verification using the secp256k1 elliptic curve
|
|
5
|
+
* (same curve used by Bitcoin and Nostr).
|
|
7
6
|
*
|
|
8
7
|
* @module crypto/secp256k1
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import { sha256 } from '@noble/hashes/sha256';
|
|
12
|
-
import { bytesToHex
|
|
13
|
-
import {
|
|
11
|
+
import { bytesToHex } from '@noble/hashes/utils';
|
|
12
|
+
import { schnorr } from '@noble/curves/secp256k1';
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* Check if a string is a 64-char hex public key (x-only schnorr format).
|
|
@@ -83,394 +82,6 @@ export async function verify(content, signature, publicKey) {
|
|
|
83
82
|
}
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
/**
|
|
87
|
-
* Normalize a capability token to a plain string.
|
|
88
|
-
* Handles: object wrappers ({ token: "..." }), Buffer serialization, comma-separated byte strings.
|
|
89
|
-
* @param {string|Object} token - Token in any supported format
|
|
90
|
-
* @returns {string|null} Normalized token string, or null if invalid
|
|
91
|
-
*/
|
|
92
|
-
export function normalizeTokenString(token) {
|
|
93
|
-
// Handle capability object wrapper { token, scope, permissions }
|
|
94
|
-
if (token && typeof token === 'object' && token.token) {
|
|
95
|
-
token = token.token;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Handle Buffer serialization format {"type":"Buffer","data":[...]}
|
|
99
|
-
if (token && typeof token === 'object' && token.type === 'Buffer' && Array.isArray(token.data)) {
|
|
100
|
-
try {
|
|
101
|
-
token = String.fromCharCode.apply(null, token.data);
|
|
102
|
-
} catch (e) {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Handle comma-separated byte string (e.g., "123,34,116,...")
|
|
108
|
-
if (typeof token === 'string' && /^\d+(,\d+)+$/.test(token.substring(0, 50))) {
|
|
109
|
-
try {
|
|
110
|
-
const bytes = token.split(',').map(Number);
|
|
111
|
-
token = String.fromCharCode.apply(null, bytes);
|
|
112
|
-
} catch (e) {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (typeof token === 'string') {
|
|
118
|
-
return token;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Parse a capability token string into its components.
|
|
126
|
-
* Handles base64-encoded, signed (payload.signature), and raw JSON formats.
|
|
127
|
-
* @private
|
|
128
|
-
* @param {string} token - Normalized token string
|
|
129
|
-
* @returns {{ tokenObj: Object, payload: string, signature: string|null }|null} Parsed token or null
|
|
130
|
-
*/
|
|
131
|
-
function _parseToken(token) {
|
|
132
|
-
try {
|
|
133
|
-
if (typeof token !== 'string') return null;
|
|
134
|
-
|
|
135
|
-
let payload;
|
|
136
|
-
let signature = null;
|
|
137
|
-
let tokenObj;
|
|
138
|
-
|
|
139
|
-
if (token.startsWith('ey') || (!token.includes('.') && !token.startsWith('{'))) {
|
|
140
|
-
// Base64 encoded (with or without signature)
|
|
141
|
-
if (token.includes('.')) {
|
|
142
|
-
const parts = token.split('.');
|
|
143
|
-
signature = parts[1];
|
|
144
|
-
const decoded = typeof atob === 'function'
|
|
145
|
-
? atob(parts[0])
|
|
146
|
-
: Buffer.from(parts[0], 'base64').toString('utf8');
|
|
147
|
-
payload = decoded;
|
|
148
|
-
} else {
|
|
149
|
-
const decoded = typeof atob === 'function'
|
|
150
|
-
? atob(token)
|
|
151
|
-
: Buffer.from(token, 'base64').toString('utf8');
|
|
152
|
-
payload = decoded;
|
|
153
|
-
}
|
|
154
|
-
tokenObj = JSON.parse(payload);
|
|
155
|
-
} else if (token.startsWith('{')) {
|
|
156
|
-
payload = token;
|
|
157
|
-
tokenObj = JSON.parse(token);
|
|
158
|
-
} else {
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return { tokenObj, payload, signature };
|
|
163
|
-
} catch (e) {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Hash a capability token using SHA-256.
|
|
170
|
-
* @param {string} token - Capability token string
|
|
171
|
-
* @returns {string} Hex-encoded SHA-256 hash
|
|
172
|
-
*/
|
|
173
|
-
export function hashToken(token) {
|
|
174
|
-
const encoder = new TextEncoder();
|
|
175
|
-
return bytesToHex(sha256(encoder.encode(token)));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Match token scope against requested scope with wildcard support
|
|
180
|
-
* Supports:
|
|
181
|
-
* - Exact match: { holonId: "abc", lensName: "quests" }
|
|
182
|
-
* - Item-level: { holonId: "abc", lensName: "quests", dataId: "quest-001" }
|
|
183
|
-
* - Wildcards: { holonId: "*", lensName: "*" } matches everything
|
|
184
|
-
* @param {Object} tokenScope - Scope from capability token
|
|
185
|
-
* @param {Object} requestedScope - Scope being requested
|
|
186
|
-
* @returns {boolean} True if token scope covers requested scope
|
|
187
|
-
*/
|
|
188
|
-
export function matchScope(tokenScope, requestedScope) {
|
|
189
|
-
// Handle string scopes (legacy support)
|
|
190
|
-
if (typeof tokenScope === 'string' || typeof requestedScope === 'string') {
|
|
191
|
-
const tokenStr = typeof tokenScope === 'string' ? tokenScope : JSON.stringify(tokenScope);
|
|
192
|
-
const reqStr = typeof requestedScope === 'string' ? requestedScope : JSON.stringify(requestedScope);
|
|
193
|
-
return tokenStr === reqStr;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Handle holonId
|
|
197
|
-
if (tokenScope.holonId !== '*' && tokenScope.holonId !== requestedScope.holonId) {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Handle lensName
|
|
202
|
-
if (tokenScope.lensName !== '*' && tokenScope.lensName !== requestedScope.lensName) {
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Handle optional dataId (item-level scope)
|
|
207
|
-
// If token has specific dataId (not wildcard), it must match requested dataId
|
|
208
|
-
if (tokenScope.dataId && tokenScope.dataId !== '*') {
|
|
209
|
-
if (requestedScope.dataId && tokenScope.dataId !== requestedScope.dataId) {
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return true;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Issue capability token - UNIFIED MODEL
|
|
219
|
-
*
|
|
220
|
-
* Supports both self-capabilities (issuer === recipient) and cross-capabilities.
|
|
221
|
-
* Self-capabilities are used for same-author federation, providing consistent
|
|
222
|
-
* security model across all federation types.
|
|
223
|
-
*
|
|
224
|
-
* @param {string[]} permissions - Permissions array (e.g., ['read', 'write', 'delete'])
|
|
225
|
-
* @param {Object|string} scope - Scope (holon/lens path or object). Supports wildcards: { holonId: "*", lensName: "*" }
|
|
226
|
-
* @param {string} recipient - Recipient public key (can be same as issuer for self-capability)
|
|
227
|
-
* @param {Object} options - Options
|
|
228
|
-
* @param {number} options.expiresIn - Expiration in milliseconds (default: 1 hour, longer for self-caps)
|
|
229
|
-
* @param {string} options.issuer - Issuer ID/public key
|
|
230
|
-
* @param {string} options.issuerKey - Issuer private key for signing
|
|
231
|
-
* @param {boolean} options.isSelfCapability - If true, marks as self-capability (auto-detected if issuer === recipient)
|
|
232
|
-
* @returns {Promise<string>} Capability token (base64-encoded JWT-like)
|
|
233
|
-
*/
|
|
234
|
-
export async function issueCapability(permissions, scope, recipient, options = {}) {
|
|
235
|
-
const { expiresIn = 3600000, issuer = 'holosphere', issuerKey, isSelfCapability } = options;
|
|
236
|
-
|
|
237
|
-
// Validate permissions
|
|
238
|
-
if (!Array.isArray(permissions) || permissions.length === 0) {
|
|
239
|
-
throw new Error('Permissions array cannot be empty');
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Validate scope - now supports wildcards
|
|
243
|
-
if (typeof scope === 'object') {
|
|
244
|
-
// holonId is required (can be '*' for wildcard)
|
|
245
|
-
if (scope.holonId === undefined || scope.holonId === '') {
|
|
246
|
-
throw new Error('Invalid scope: holonId is required (use "*" for wildcard)');
|
|
247
|
-
}
|
|
248
|
-
// lensName is required (can be '*' for wildcard)
|
|
249
|
-
if (scope.lensName === undefined || scope.lensName === '') {
|
|
250
|
-
throw new Error('Invalid scope: lensName is required (use "*" for wildcard)');
|
|
251
|
-
}
|
|
252
|
-
// dataId is optional (can be specific value, '*', or omitted)
|
|
253
|
-
} else if (typeof scope === 'string' && scope === '') {
|
|
254
|
-
throw new Error('Invalid scope: cannot be empty string');
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Validate issuerKey if provided
|
|
258
|
-
if (issuerKey && (typeof issuerKey !== 'string' || issuerKey.length < 32)) {
|
|
259
|
-
throw new Error('Invalid issuer key');
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Detect self-capability (issuer === recipient)
|
|
263
|
-
const selfCap = isSelfCapability !== undefined ? isSelfCapability : (issuer === recipient);
|
|
264
|
-
|
|
265
|
-
const token = {
|
|
266
|
-
type: 'capability',
|
|
267
|
-
permissions,
|
|
268
|
-
scope,
|
|
269
|
-
recipient,
|
|
270
|
-
issuer,
|
|
271
|
-
isSelfCapability: selfCap, // Mark self-capabilities for clarity
|
|
272
|
-
nonce: generateNonce(),
|
|
273
|
-
issued: Date.now(),
|
|
274
|
-
expires: expiresIn != null ? Date.now() + expiresIn : null,
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
// Encode token as base64
|
|
278
|
-
// Use browser-native btoa when available to avoid Buffer serialization issues
|
|
279
|
-
// (Buffer can serialize to {"type":"Buffer","data":[...]} in JSON)
|
|
280
|
-
const payload = JSON.stringify(token);
|
|
281
|
-
const encoded = typeof btoa === 'function'
|
|
282
|
-
? btoa(payload)
|
|
283
|
-
: Buffer.from(payload).toString('base64');
|
|
284
|
-
|
|
285
|
-
// If issuerKey provided, sign the token
|
|
286
|
-
if (issuerKey) {
|
|
287
|
-
const signature = await sign(payload, issuerKey);
|
|
288
|
-
return `${encoded}.${signature}`;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return encoded;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Issue a self-capability token - UNIFIED MODEL
|
|
296
|
-
*
|
|
297
|
-
* Convenience method for creating self-capabilities (same author federation).
|
|
298
|
-
* Self-capabilities have longer expiration by default (1 year).
|
|
299
|
-
*
|
|
300
|
-
* @param {string[]} permissions - Permissions array
|
|
301
|
-
* @param {Object} scope - Scope object { holonId, lensName, dataId? }
|
|
302
|
-
* @param {string} authorPubKey - Author's public key (both issuer and recipient)
|
|
303
|
-
* @param {Object} options - Options
|
|
304
|
-
* @param {string} options.privateKey - Author's private key for signing
|
|
305
|
-
* @param {number} [options.expiresIn=31536000000] - Expiration in ms (default: 1 year)
|
|
306
|
-
* @returns {Promise<string>} Self-capability token
|
|
307
|
-
*/
|
|
308
|
-
export async function issueSelfCapability(permissions, scope, authorPubKey, options = {}) {
|
|
309
|
-
const { privateKey, expiresIn = 365 * 24 * 60 * 60 * 1000 } = options; // 1 year default
|
|
310
|
-
|
|
311
|
-
return issueCapability(permissions, scope, authorPubKey, {
|
|
312
|
-
expiresIn,
|
|
313
|
-
issuer: authorPubKey,
|
|
314
|
-
issuerKey: privateKey,
|
|
315
|
-
isSelfCapability: true,
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Verify capability token
|
|
321
|
-
* @param {string|Object} token - Capability token (string or object)
|
|
322
|
-
* @param {string} requiredPermission - Required permission
|
|
323
|
-
* @param {Object|string} scope - Scope to check (supports wildcards in token scope)
|
|
324
|
-
* @returns {Promise<boolean>} True if valid
|
|
325
|
-
*/
|
|
326
|
-
export async function verifyCapability(token, requiredPermission, scope) {
|
|
327
|
-
try {
|
|
328
|
-
const normalized = normalizeTokenString(token);
|
|
329
|
-
if (!normalized) {
|
|
330
|
-
console.log('[verifyCapability] ❌ Failed to normalize token');
|
|
331
|
-
return false;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
let tokenObj;
|
|
335
|
-
|
|
336
|
-
// If it's a string, parse it
|
|
337
|
-
if (typeof normalized === 'string') {
|
|
338
|
-
const parsed = _parseToken(normalized);
|
|
339
|
-
if (!parsed) {
|
|
340
|
-
console.log('[verifyCapability] ❌ Failed to parse token');
|
|
341
|
-
return false;
|
|
342
|
-
}
|
|
343
|
-
tokenObj = parsed.tokenObj;
|
|
344
|
-
} else if (normalized && typeof normalized === 'object') {
|
|
345
|
-
tokenObj = normalized;
|
|
346
|
-
} else {
|
|
347
|
-
console.log('[verifyCapability] ❌ Invalid token:', typeof normalized);
|
|
348
|
-
return false;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (!tokenObj || tokenObj.type !== 'capability') {
|
|
352
|
-
console.log('[verifyCapability] ❌ Invalid token type:', { type: tokenObj?.type, tokenObj });
|
|
353
|
-
return false;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Check expiration (null/undefined expires = no expiration)
|
|
357
|
-
if (tokenObj.expires != null && Date.now() > tokenObj.expires) {
|
|
358
|
-
console.log('[verifyCapability] ❌ Token expired:', {
|
|
359
|
-
expires: tokenObj.expires,
|
|
360
|
-
now: Date.now(),
|
|
361
|
-
expiredAgo: `${(Date.now() - tokenObj.expires) / 1000}s ago`
|
|
362
|
-
});
|
|
363
|
-
return false;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Check scope using matchScope (supports wildcards)
|
|
367
|
-
if (!matchScope(tokenObj.scope, scope)) {
|
|
368
|
-
console.log('[verifyCapability] ❌ Scope mismatch:', {
|
|
369
|
-
tokenScope: tokenObj.scope,
|
|
370
|
-
requestedScope: scope
|
|
371
|
-
});
|
|
372
|
-
return false;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Check permission
|
|
376
|
-
if (!tokenObj.permissions.includes(requiredPermission)) {
|
|
377
|
-
console.log('[verifyCapability] ❌ Permission denied:', {
|
|
378
|
-
required: requiredPermission,
|
|
379
|
-
has: tokenObj.permissions
|
|
380
|
-
});
|
|
381
|
-
return false;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
console.log('[verifyCapability] ✅ Capability valid');
|
|
385
|
-
return true;
|
|
386
|
-
} catch (error) {
|
|
387
|
-
console.log('[verifyCapability] ❌ Error:', error.message);
|
|
388
|
-
return false;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Verify that a capability token was issued by the expected author.
|
|
394
|
-
* This is the core security check for the object-capability model.
|
|
395
|
-
* A hologram should only be resolvable if the capability issuer matches
|
|
396
|
-
* the claimed data author AND the signature was made by that author's key.
|
|
397
|
-
*
|
|
398
|
-
* @param {string} token - Capability token
|
|
399
|
-
* @param {string} expectedIssuer - Expected issuer public key (data author)
|
|
400
|
-
* @returns {Promise<boolean>} True if issuer matches expected AND signature is valid
|
|
401
|
-
*/
|
|
402
|
-
export async function verifyCapabilityIssuer(token, expectedIssuer) {
|
|
403
|
-
try {
|
|
404
|
-
const normalized = normalizeTokenString(token);
|
|
405
|
-
if (!normalized) {
|
|
406
|
-
console.log('[verifyCapabilityIssuer] ❌ Failed to normalize token');
|
|
407
|
-
return false;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const parsed = _parseToken(normalized);
|
|
411
|
-
if (!parsed) {
|
|
412
|
-
console.log('[verifyCapabilityIssuer] ❌ Failed to parse token');
|
|
413
|
-
return false;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const { tokenObj, payload, signature } = parsed;
|
|
417
|
-
|
|
418
|
-
// Verify issuer field matches expected
|
|
419
|
-
if (tokenObj.issuer !== expectedIssuer) {
|
|
420
|
-
console.log('[verifyCapabilityIssuer] ❌ Issuer mismatch:', {
|
|
421
|
-
tokenIssuer: tokenObj.issuer?.slice(0, 12) + '...',
|
|
422
|
-
expectedIssuer: expectedIssuer?.slice(0, 12) + '...',
|
|
423
|
-
});
|
|
424
|
-
return false;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// CRITICAL: Verify signature was made by the claimed issuer's private key
|
|
428
|
-
// This prevents forged capabilities where someone claims a different issuer
|
|
429
|
-
if (signature) {
|
|
430
|
-
const signatureValid = await verify(payload, signature, expectedIssuer);
|
|
431
|
-
if (!signatureValid) {
|
|
432
|
-
console.log('[verifyCapabilityIssuer] ❌ Signature verification failed - capability not signed by claimed issuer');
|
|
433
|
-
return false;
|
|
434
|
-
}
|
|
435
|
-
} else {
|
|
436
|
-
// No signature - cannot cryptographically verify issuer identity
|
|
437
|
-
console.log('[verifyCapabilityIssuer] ❌ Token has no signature - cannot verify issuer');
|
|
438
|
-
return false;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
console.log('[verifyCapabilityIssuer] ✅ Issuer verified');
|
|
442
|
-
return true;
|
|
443
|
-
} catch (error) {
|
|
444
|
-
console.log('[verifyCapabilityIssuer] ❌ Error:', error.message);
|
|
445
|
-
return false;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Decode a capability token to extract its payload.
|
|
451
|
-
* Useful for inspecting capability contents without full verification.
|
|
452
|
-
*
|
|
453
|
-
* @param {string} token - Capability token
|
|
454
|
-
* @returns {Object|null} Decoded token payload or null if invalid
|
|
455
|
-
*/
|
|
456
|
-
export function decodeCapability(token) {
|
|
457
|
-
const normalized = normalizeTokenString(token);
|
|
458
|
-
if (!normalized) return null;
|
|
459
|
-
|
|
460
|
-
const parsed = _parseToken(normalized);
|
|
461
|
-
return parsed ? parsed.tokenObj : null;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Generate unique nonce
|
|
466
|
-
* @private
|
|
467
|
-
*/
|
|
468
|
-
function generateNonce() {
|
|
469
|
-
return (
|
|
470
|
-
Date.now().toString(36) + Math.random().toString(36).substring(2, 15)
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
85
|
/**
|
|
475
86
|
* Hash content using SHA-256
|
|
476
87
|
* @private
|