@nibgate/sdk 0.1.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 +605 -0
- package/package.json +58 -0
- package/src/browser/env.js +3 -0
- package/src/browser/events.js +57 -0
- package/src/browser/gateway.js +89 -0
- package/src/browser/index.js +710 -0
- package/src/browser/json.js +8 -0
- package/src/browser/reputation.js +161 -0
- package/src/browser/storage.js +68 -0
- package/src/browser/tracking.js +42 -0
- package/src/browser/transfer.js +53 -0
- package/src/core/payment.js +8 -0
- package/src/core/rating.js +28 -0
- package/src/core/resource.js +143 -0
- package/src/core/settings.js +47 -0
- package/src/index.d.ts +423 -0
- package/src/index.js +1 -0
- package/src/server/access.js +217 -0
- package/src/server/actor.js +23 -0
- package/src/server/challenge.js +51 -0
- package/src/server/env.js +3 -0
- package/src/server/gateway.js +186 -0
- package/src/server/hub.js +36 -0
- package/src/server/index.js +15 -0
- package/src/server/manifest.js +19 -0
- package/src/server/presets.js +14 -0
- package/src/server/proof.js +73 -0
- package/src/server/response.js +9 -0
- package/src/server/runtime.js +39 -0
- package/src/server.d.ts +212 -0
- package/src/server.js +1 -0
- package/src/testing.d.ts +23 -0
- package/src/testing.js +56 -0
- package/src/tracking.d.ts +18 -0
- package/src/tracking.js +1 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { normalizeRating } from '../core/rating.js';
|
|
2
|
+
import { normalizeResource } from '../core/resource.js';
|
|
3
|
+
import { emit, payloadWithResource } from './events.js';
|
|
4
|
+
|
|
5
|
+
const RATE_CONTENT_SELECTOR = '0xc62fad09';
|
|
6
|
+
const ZERO_HASH = `0x${'0'.repeat(64)}`;
|
|
7
|
+
|
|
8
|
+
export const NIBGATE_CONTENT_HASH_NAMESPACE = 'nibgate:content:v1';
|
|
9
|
+
|
|
10
|
+
export const NIBGATE_REPUTATION_ABI = [
|
|
11
|
+
{
|
|
12
|
+
type: 'function',
|
|
13
|
+
name: 'rateContent',
|
|
14
|
+
stateMutability: 'nonpayable',
|
|
15
|
+
inputs: [
|
|
16
|
+
{ name: 'contentId', type: 'bytes32' },
|
|
17
|
+
{ name: 'rating', type: 'uint8' },
|
|
18
|
+
{ name: 'reviewHash', type: 'bytes32' },
|
|
19
|
+
{ name: 'unlockRef', type: 'string' }
|
|
20
|
+
],
|
|
21
|
+
outputs: []
|
|
22
|
+
}
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function stripHex(value = '') {
|
|
26
|
+
return String(value || '').replace(/^0x/i, '').toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function word(hex = '') {
|
|
30
|
+
const clean = stripHex(hex);
|
|
31
|
+
if (clean.length > 64) throw new Error('ABI word is too long.');
|
|
32
|
+
return clean.padStart(64, '0');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function wordRight(hex = '') {
|
|
36
|
+
const clean = stripHex(hex);
|
|
37
|
+
if (clean.length > 64) throw new Error('ABI word is too long.');
|
|
38
|
+
return clean.padEnd(64, '0');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function numberWord(value = 0) {
|
|
42
|
+
return Number(value || 0).toString(16).padStart(64, '0');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function utf8Hex(value = '') {
|
|
46
|
+
return Array.from(new TextEncoder().encode(String(value || '')))
|
|
47
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
48
|
+
.join('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function encodeString(value = '') {
|
|
52
|
+
const hex = utf8Hex(value);
|
|
53
|
+
const byteLength = hex.length / 2;
|
|
54
|
+
const paddedLength = Math.ceil(byteLength / 32) * 64;
|
|
55
|
+
return numberWord(byteLength) + hex.padEnd(paddedLength, '0');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function encodeRateContent({ contentId, ratingValue, reviewHash, unlockRef }) {
|
|
59
|
+
return RATE_CONTENT_SELECTOR
|
|
60
|
+
+ wordRight(contentId)
|
|
61
|
+
+ numberWord(ratingValue)
|
|
62
|
+
+ wordRight(reviewHash || ZERO_HASH)
|
|
63
|
+
+ numberWord(128)
|
|
64
|
+
+ encodeString(unlockRef || '');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function contentRatingHash(_resource, options = {}) {
|
|
68
|
+
const contentId = options.contentId || options.contentHash;
|
|
69
|
+
if (!contentId) {
|
|
70
|
+
throw new Error('contentId/contentHash is required. Use the Nibgate backend prepare endpoint or pass a known content hash.');
|
|
71
|
+
}
|
|
72
|
+
return contentId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function reviewTextHash(review = '') {
|
|
76
|
+
if (!review) return ZERO_HASH;
|
|
77
|
+
throw new Error('Text review hashing is not available in direct-browser mode. Pass reviewHash from your app/backend.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function prepareOnchainRating(resource, options = {}) {
|
|
81
|
+
if (options.contentId || options.contentHash) return { contentId: options.contentId || options.contentHash };
|
|
82
|
+
const prepareUrl = options.prepareUrl || options.indexUrl?.replace(/\/index$/, '/prepare');
|
|
83
|
+
if (!prepareUrl) throw new Error('contentId/contentHash or prepareUrl is required for onchain rating.');
|
|
84
|
+
const response = await fetch(prepareUrl, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'content-type': 'application/json', ...(options.indexHeaders || {}) },
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
siteId: options.siteId,
|
|
89
|
+
token: options.token,
|
|
90
|
+
resource,
|
|
91
|
+
url: resource.url,
|
|
92
|
+
path: resource.path
|
|
93
|
+
})
|
|
94
|
+
});
|
|
95
|
+
const payload = await response.json().catch(() => ({}));
|
|
96
|
+
if (!response.ok || !payload.contentHash) throw new Error(payload.error || 'Could not prepare Nibgate onchain rating.');
|
|
97
|
+
return payload;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function rateContentOnchain(resource, options = {}) {
|
|
101
|
+
const normalized = normalizeResource(resource);
|
|
102
|
+
const rating = normalizeRating(options.rating ?? options.stars ?? options);
|
|
103
|
+
if (!rating.ratingValue) throw new Error('Rating must be between 0.1 and 5 stars.');
|
|
104
|
+
|
|
105
|
+
const provider = options.provider || globalThis?.ethereum;
|
|
106
|
+
if (!provider?.request) throw new Error('Connect an EVM wallet to rate this content onchain.');
|
|
107
|
+
|
|
108
|
+
const contractAddress = options.contractAddress || options.reputationContract;
|
|
109
|
+
if (!contractAddress) throw new Error('Nibgate reputation contract address is not configured.');
|
|
110
|
+
|
|
111
|
+
const accounts = await provider.request({ method: 'eth_requestAccounts' });
|
|
112
|
+
const walletAddress = Array.isArray(accounts) ? accounts[0] || '' : '';
|
|
113
|
+
if (!walletAddress) throw new Error('No wallet account selected.');
|
|
114
|
+
|
|
115
|
+
const prepared = await prepareOnchainRating(normalized, options);
|
|
116
|
+
const contentId = prepared.contentHash || prepared.contentId || contentRatingHash(normalized, options);
|
|
117
|
+
const reviewHash = options.reviewHash || ZERO_HASH;
|
|
118
|
+
const unlockRef = String(options.unlockRef || options.paymentId || options.txHash || '');
|
|
119
|
+
const data = encodeRateContent({ contentId, ratingValue: rating.ratingValue, reviewHash, unlockRef });
|
|
120
|
+
|
|
121
|
+
const txHash = await provider.request({
|
|
122
|
+
method: 'eth_sendTransaction',
|
|
123
|
+
params: [{
|
|
124
|
+
from: walletAddress,
|
|
125
|
+
to: contractAddress,
|
|
126
|
+
data
|
|
127
|
+
}]
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const payload = payloadWithResource(normalized, {
|
|
131
|
+
rating: rating.rating,
|
|
132
|
+
ratingValue: rating.ratingValue,
|
|
133
|
+
walletAddress,
|
|
134
|
+
txHash,
|
|
135
|
+
contentHash: contentId,
|
|
136
|
+
reviewHash,
|
|
137
|
+
proofType: 'onchain_pending',
|
|
138
|
+
proof: unlockRef,
|
|
139
|
+
paymentId: options.paymentId,
|
|
140
|
+
actor: options.actor || 'human'
|
|
141
|
+
});
|
|
142
|
+
emit('content_rating', payload);
|
|
143
|
+
|
|
144
|
+
if (options.indexUrl) {
|
|
145
|
+
await fetch(options.indexUrl, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: { 'content-type': 'application/json', ...(options.indexHeaders || {}) },
|
|
148
|
+
body: JSON.stringify({
|
|
149
|
+
siteId: options.siteId,
|
|
150
|
+
token: options.token,
|
|
151
|
+
txHash,
|
|
152
|
+
resource: normalized,
|
|
153
|
+
url: normalized.url,
|
|
154
|
+
path: normalized.path,
|
|
155
|
+
actor: options.actor || 'human'
|
|
156
|
+
})
|
|
157
|
+
}).catch(() => null);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { txHash, walletAddress, contentId, ratingValue: rating.ratingValue, reviewHash };
|
|
161
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { browserWindow } from './env.js';
|
|
2
|
+
import { normalizeResource } from '../core/resource.js';
|
|
3
|
+
|
|
4
|
+
export function unlockStorageKey(resource) {
|
|
5
|
+
return `nibgate:unlock:${resource.id || resource.path || resource.url || 'content'}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function proofStorageKey(resource) {
|
|
9
|
+
return `nibgate:payment-proof:${resource.id || resource.path || resource.url || 'content'}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function markUnlocked(resource, payment = {}) {
|
|
13
|
+
const win = browserWindow();
|
|
14
|
+
if (!win) return false;
|
|
15
|
+
try {
|
|
16
|
+
win.localStorage.setItem(unlockStorageKey(resource), JSON.stringify({
|
|
17
|
+
unlockedAt: new Date().toISOString(),
|
|
18
|
+
payment
|
|
19
|
+
}));
|
|
20
|
+
return true;
|
|
21
|
+
} catch (_error) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function storePaymentProof(resource, proof) {
|
|
27
|
+
const win = browserWindow();
|
|
28
|
+
if (!win || !proof) return false;
|
|
29
|
+
try {
|
|
30
|
+
win.localStorage.setItem(proofStorageKey(resource), String(proof));
|
|
31
|
+
return true;
|
|
32
|
+
} catch (_error) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getPaymentProof(resource) {
|
|
38
|
+
const win = browserWindow();
|
|
39
|
+
if (!win) return '';
|
|
40
|
+
try {
|
|
41
|
+
return win.localStorage.getItem(proofStorageKey(resource)) || '';
|
|
42
|
+
} catch (_error) {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function clearPaymentProof(resource) {
|
|
48
|
+
const normalized = normalizeResource(resource);
|
|
49
|
+
const win = browserWindow();
|
|
50
|
+
if (!win) return false;
|
|
51
|
+
try {
|
|
52
|
+
win.localStorage.removeItem(proofStorageKey(normalized));
|
|
53
|
+
win.localStorage.removeItem(unlockStorageKey(normalized));
|
|
54
|
+
return true;
|
|
55
|
+
} catch (_error) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function hasUnlock(resource) {
|
|
61
|
+
const win = browserWindow();
|
|
62
|
+
if (!win) return false;
|
|
63
|
+
try {
|
|
64
|
+
return Boolean(win.localStorage.getItem(unlockStorageKey(resource)));
|
|
65
|
+
} catch (_error) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { normalizeResource, validateResourceMetadata } from '../core/resource.js';
|
|
2
|
+
import { browserWindow } from './env.js';
|
|
3
|
+
import { emit, payloadWithResource } from './events.js';
|
|
4
|
+
|
|
5
|
+
export function createTrackingGate(resource) {
|
|
6
|
+
const normalized = normalizeResource(resource);
|
|
7
|
+
return {
|
|
8
|
+
resource: normalized,
|
|
9
|
+
content(extra = {}) {
|
|
10
|
+
return emit('content_registered', payloadWithResource(normalized, extra));
|
|
11
|
+
},
|
|
12
|
+
view(extra = {}) {
|
|
13
|
+
return emit('resource_view', payloadWithResource(normalized, extra));
|
|
14
|
+
},
|
|
15
|
+
track(eventName, payload = {}) {
|
|
16
|
+
return emit(eventName, payloadWithResource(normalized, payload));
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function trackResourcePage(resource, options = {}) {
|
|
22
|
+
const item = createTrackingGate(resource);
|
|
23
|
+
const validation = validateResourceMetadata(item.resource, options.validation || {});
|
|
24
|
+
if ((validation.warnings.length || validation.errors.length) && options.warn !== false && browserWindow()?.console?.warn) {
|
|
25
|
+
browserWindow().console.warn('Nibgate content metadata needs attention', validation);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
item.content({
|
|
29
|
+
source: options.source,
|
|
30
|
+
metadataQuality: { score: validation.score, warnings: validation.warnings, errors: validation.errors },
|
|
31
|
+
...(options.content || {})
|
|
32
|
+
});
|
|
33
|
+
item.view({
|
|
34
|
+
source: options.source,
|
|
35
|
+
path: options.path || browserWindow()?.location?.pathname || item.resource.path,
|
|
36
|
+
referrer: options.referrer ?? browserWindow()?.document?.referrer ?? '',
|
|
37
|
+
...(options.view || {})
|
|
38
|
+
});
|
|
39
|
+
return item;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { normalizeResource, validateResourceMetadata };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { normalizeResource } from '../core/resource.js';
|
|
2
|
+
import { checkResourceAccess } from './index.js';
|
|
3
|
+
|
|
4
|
+
export function createTransferCheckout(resource, options = {}) {
|
|
5
|
+
const normalized = normalizeResource({ ...resource, paymentRail: 'transfer' });
|
|
6
|
+
const sendTransfer = options.sendTransfer || options.transfer;
|
|
7
|
+
if (typeof sendTransfer !== 'function') {
|
|
8
|
+
throw new Error('createTransferCheckout requires sendTransfer({ resource, recipient, amount, currency, network }) and a server verifyTransfer hook.');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
resource: normalized,
|
|
13
|
+
async pay(input = {}) {
|
|
14
|
+
const recipient = normalized.recipient || normalized.payTo;
|
|
15
|
+
const amount = String(normalized.price || normalized.amount || '0');
|
|
16
|
+
const currency = normalized.currency || 'USDC';
|
|
17
|
+
const network = options.network || input.challenge?.accepts?.[0]?.network || 'eip155:5042002';
|
|
18
|
+
const result = await sendTransfer({ resource: normalized, recipient, amount, currency, network, challenge: input.challenge });
|
|
19
|
+
const txHash = result?.txHash || result?.hash || result?.transactionHash || result?.paymentId || '';
|
|
20
|
+
if (!txHash) throw new Error('Transfer checkout did not return a txHash.');
|
|
21
|
+
return {
|
|
22
|
+
paymentSignature: txHash,
|
|
23
|
+
signature: txHash,
|
|
24
|
+
memo: result.memo || '',
|
|
25
|
+
metadata: {
|
|
26
|
+
paymentProvider: 'direct-transfer',
|
|
27
|
+
paymentId: txHash,
|
|
28
|
+
txHash,
|
|
29
|
+
recipient,
|
|
30
|
+
amount: Number(amount),
|
|
31
|
+
currency,
|
|
32
|
+
network,
|
|
33
|
+
...(result.metadata || result)
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function payWithTransfer(resource, options = {}) {
|
|
41
|
+
const checkout = options.checkout || createTransferCheckout(resource, options).pay;
|
|
42
|
+
const result = await checkout({ resource: normalizeResource(resource), challenge: options.challenge || null });
|
|
43
|
+
const txHash = result?.metadata?.txHash || result?.txHash || result?.paymentSignature || result?.signature || '';
|
|
44
|
+
if (!txHash) throw new Error('Transfer checkout did not return a txHash.');
|
|
45
|
+
return checkResourceAccess(resource, {
|
|
46
|
+
...options,
|
|
47
|
+
headers: {
|
|
48
|
+
...(options.headers || {}),
|
|
49
|
+
'x-nibgate-transfer-tx': txHash
|
|
50
|
+
},
|
|
51
|
+
payment: result.metadata || { paymentProvider: 'direct-transfer', txHash, paymentId: txHash }
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const PAYMENT_RAILS = ['gateway', 'transfer'];
|
|
2
|
+
|
|
3
|
+
export function normalizePaymentRail(value, fallback = 'gateway') {
|
|
4
|
+
const rail = String(value || '').trim().toLowerCase().replace(/[-\s]+/g, '_');
|
|
5
|
+
if (rail === 'circle_gateway' || rail === 'x402') return 'gateway';
|
|
6
|
+
if (rail === 'direct_transfer' || rail === 'wallet_transfer') return 'transfer';
|
|
7
|
+
return PAYMENT_RAILS.includes(rail) ? rail : fallback;
|
|
8
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { normalizeResource } from './resource.js';
|
|
2
|
+
|
|
3
|
+
export function normalizeRating(input = {}) {
|
|
4
|
+
const value = typeof input === 'number' ? input : (input.rating ?? input.stars ?? input.ratingValue ?? input.score);
|
|
5
|
+
const numeric = Number.parseFloat(value);
|
|
6
|
+
const ratingValue = Number.isFinite(numeric)
|
|
7
|
+
? Math.max(1, Math.min(50, numeric <= 5 ? Math.round(numeric * 10) : Math.round(numeric)))
|
|
8
|
+
: null;
|
|
9
|
+
return {
|
|
10
|
+
...input,
|
|
11
|
+
rating: ratingValue ? ratingValue / 10 : undefined,
|
|
12
|
+
ratingValue: ratingValue || undefined
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ratingMessage(resource, rating = {}, options = {}) {
|
|
17
|
+
const normalized = normalizeResource(resource);
|
|
18
|
+
const normalizedRating = normalizeRating(rating);
|
|
19
|
+
const value = normalizedRating.ratingValue || 0;
|
|
20
|
+
return [
|
|
21
|
+
'Nibgate content rating',
|
|
22
|
+
`site:${options.siteDomain || options.domain || normalized.siteDomain || normalized.domain || ''}`,
|
|
23
|
+
`content:${normalized.externalId || normalized.id}`,
|
|
24
|
+
`url:${normalized.url || options.url || ''}`,
|
|
25
|
+
`rating:${value}`,
|
|
26
|
+
'I confirm this rating is tied to my unlock/payment proof.'
|
|
27
|
+
].join('\n');
|
|
28
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { normalizePaymentRail } from './payment.js';
|
|
2
|
+
export const CONTENT_TYPES = ['music', 'video', 'article', 'image'];
|
|
3
|
+
export const TYPE_ALIASES = {
|
|
4
|
+
audio: 'music',
|
|
5
|
+
song: 'music',
|
|
6
|
+
track: 'music',
|
|
7
|
+
album: 'music',
|
|
8
|
+
playlist: 'music',
|
|
9
|
+
photo: 'image',
|
|
10
|
+
picture: 'image',
|
|
11
|
+
illustration: 'image',
|
|
12
|
+
art: 'image',
|
|
13
|
+
movie: 'video',
|
|
14
|
+
clip: 'video'
|
|
15
|
+
};
|
|
16
|
+
export const ACCESS_MODES = ['free', 'paid', 'blocked'];
|
|
17
|
+
export const UNLOCK_MODES = ['one_time', 'metered_stream', 'metered_read', 'time_pass', 'agent_quota'];
|
|
18
|
+
|
|
19
|
+
export function normalizeContentType(value) {
|
|
20
|
+
const type = String(value || '').trim().toLowerCase();
|
|
21
|
+
if (CONTENT_TYPES.includes(type)) return type;
|
|
22
|
+
return TYPE_ALIASES[type] || 'article';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizeAccessMode(value, fallback = 'paid') {
|
|
26
|
+
const mode = String(value || '').trim().toLowerCase();
|
|
27
|
+
return ACCESS_MODES.includes(mode) ? mode : fallback;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeAccessPolicy(value = {}) {
|
|
31
|
+
if (typeof value === 'string') {
|
|
32
|
+
const mode = normalizeAccessMode(value);
|
|
33
|
+
return { humans: mode, agents: mode };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
humans: normalizeAccessMode(value.humans || value.human || value.default, 'paid'),
|
|
38
|
+
agents: normalizeAccessMode(value.agents || value.agent || value.default, 'paid')
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function normalizeUnlockPolicy(value = {}) {
|
|
43
|
+
const input = typeof value === 'string' ? { mode: value } : (value || {});
|
|
44
|
+
const mode = String(input.mode || input.type || 'one_time').trim().toLowerCase().replace(/[-\s]+/g, '_');
|
|
45
|
+
return {
|
|
46
|
+
...input,
|
|
47
|
+
mode: UNLOCK_MODES.includes(mode) ? mode : 'one_time'
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function normalizeResource(resource = {}) {
|
|
52
|
+
const input = typeof resource === 'string' ? { id: resource } : (resource || {});
|
|
53
|
+
const {
|
|
54
|
+
publisher,
|
|
55
|
+
publisherId,
|
|
56
|
+
publisherWallet,
|
|
57
|
+
publisherHandle,
|
|
58
|
+
publisherName,
|
|
59
|
+
publisherProfileUrl,
|
|
60
|
+
publisherOrigin,
|
|
61
|
+
publisherVerification,
|
|
62
|
+
authorHandle,
|
|
63
|
+
...v1Input
|
|
64
|
+
} = input;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
...v1Input,
|
|
68
|
+
id: String(input.id || input.contentId || input.slug || '').trim(),
|
|
69
|
+
title: String(input.title || input.name || '').trim(),
|
|
70
|
+
type: normalizeContentType(input.type || input.contentType),
|
|
71
|
+
price: input.price ?? input.amount ?? '',
|
|
72
|
+
paymentRail: normalizePaymentRail(input.paymentRail || input.paymentMode || input.rail),
|
|
73
|
+
recipient: input.recipient || input.receiver || input.receiverAddress || input.payTo || input.creatorWallet || undefined,
|
|
74
|
+
payTo: input.payTo || input.recipient || input.receiver || input.receiverAddress || input.creatorWallet || undefined,
|
|
75
|
+
path: input.path || input.route || undefined,
|
|
76
|
+
url: input.url || undefined,
|
|
77
|
+
imageUrl: input.imageUrl || input.image || undefined,
|
|
78
|
+
tags: input.tags || undefined,
|
|
79
|
+
access: normalizeAccessPolicy(input.access),
|
|
80
|
+
unlock: normalizeUnlockPolicy(input.unlock)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hasValue(value) {
|
|
85
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
86
|
+
return value !== undefined && value !== null && String(value).trim() !== '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isPaidResource(resource = {}) {
|
|
90
|
+
const access = normalizeAccessPolicy(resource.access);
|
|
91
|
+
return access.humans === 'paid' || access.agents === 'paid' || Number.parseFloat(resource.price || resource.amount || '0') > 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function validateResourceMetadata(resource = {}, options = {}) {
|
|
95
|
+
const normalized = normalizeResource(resource);
|
|
96
|
+
const warnings = [];
|
|
97
|
+
const errors = [];
|
|
98
|
+
const required = options.required || ['id', 'title', 'url', 'type'];
|
|
99
|
+
const recommended = options.recommended || ['description', 'imageUrl', 'tags'];
|
|
100
|
+
|
|
101
|
+
for (const field of required) {
|
|
102
|
+
if (!hasValue(normalized[field])) errors.push(`Missing required content metadata: ${field}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const field of recommended) {
|
|
106
|
+
if (!hasValue(normalized[field])) warnings.push(`Missing recommended discovery metadata: ${field}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!CONTENT_TYPES.includes(normalized.type)) errors.push('Content type must be one of music, video, article, or image.');
|
|
110
|
+
|
|
111
|
+
if (normalized.url && !/^https?:\/\//i.test(String(normalized.url))) {
|
|
112
|
+
warnings.push('Use an absolute canonical url for stronger discovery identity.');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (normalized.imageUrl && !/^https?:\/\//i.test(String(normalized.imageUrl))) {
|
|
116
|
+
warnings.push('Use an absolute imageUrl for thumbnails in Explore and agent discovery.');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (isPaidResource(normalized)) {
|
|
120
|
+
if (!hasValue(normalized.price)) errors.push('Paid content requires price.');
|
|
121
|
+
if (!hasValue(normalized.recipient || normalized.payTo)) errors.push('Paid content requires recipient/payTo wallet.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const score = Math.max(0, 100 - errors.length * 20 - warnings.length * 8);
|
|
125
|
+
return {
|
|
126
|
+
ok: errors.length === 0,
|
|
127
|
+
score,
|
|
128
|
+
errors,
|
|
129
|
+
warnings,
|
|
130
|
+
resource: normalized
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function normalizeServerResource(resource = {}) {
|
|
135
|
+
const normalized = normalizeResource(resource);
|
|
136
|
+
return {
|
|
137
|
+
...normalized,
|
|
138
|
+
title: normalized.title || String((typeof resource === 'object' && (resource.name || resource.id)) || 'Untitled content').trim(),
|
|
139
|
+
price: normalized.price || '0',
|
|
140
|
+
path: normalized.path || '/',
|
|
141
|
+
currency: normalized.currency || 'USDC'
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { PAYMENT_RAILS, normalizePaymentRail } from './payment.js';
|
|
2
|
+
import { ACCESS_MODES, CONTENT_TYPES, UNLOCK_MODES, normalizeAccessPolicy, normalizeContentType, normalizeUnlockPolicy } from './resource.js';
|
|
3
|
+
|
|
4
|
+
export const NIBGATE_CONTENT_SETTING_FIELDS = [
|
|
5
|
+
{ name: 'publishToNibgate', label: 'Publish to Nibgate discovery', type: 'boolean', defaultValue: true },
|
|
6
|
+
{ name: 'type', label: 'Content type', type: 'select', options: CONTENT_TYPES, defaultValue: 'article' },
|
|
7
|
+
{ name: 'humanAccess', label: 'Human access', type: 'select', options: ACCESS_MODES, defaultValue: 'paid' },
|
|
8
|
+
{ name: 'agentAccess', label: 'Agent access', type: 'select', options: ACCESS_MODES, defaultValue: 'paid' },
|
|
9
|
+
{ name: 'unlockMode', label: 'Unlock mode', type: 'select', options: UNLOCK_MODES, defaultValue: 'one_time' },
|
|
10
|
+
{ name: 'paymentRail', label: 'Payment rail', type: 'select', options: PAYMENT_RAILS, defaultValue: 'gateway' },
|
|
11
|
+
{ name: 'price', label: 'Price', type: 'text', defaultValue: '0.005' },
|
|
12
|
+
{ name: 'currency', label: 'Currency', type: 'text', defaultValue: 'USDC' },
|
|
13
|
+
{ name: 'recipient', label: 'Recipient wallet', type: 'wallet', defaultValue: '' },
|
|
14
|
+
{ name: 'license', label: 'License / terms', type: 'textarea', defaultValue: '' }
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function createNibgateContentSettings(input = {}) {
|
|
18
|
+
const access = normalizeAccessPolicy(input.access || {
|
|
19
|
+
humans: input.humanAccess,
|
|
20
|
+
agents: input.agentAccess
|
|
21
|
+
});
|
|
22
|
+
const unlock = normalizeUnlockPolicy(input.unlock || input.unlockMode || 'one_time');
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
publishToNibgate: input.publishToNibgate ?? input.publishedToNibgate ?? true,
|
|
26
|
+
type: normalizeContentType(input.type || input.contentType || 'article'),
|
|
27
|
+
humanAccess: access.humans,
|
|
28
|
+
agentAccess: access.agents,
|
|
29
|
+
unlockMode: unlock.mode,
|
|
30
|
+
paymentRail: normalizePaymentRail(input.paymentRail || input.paymentMode || input.rail),
|
|
31
|
+
price: String(input.price ?? input.amount ?? '0.005'),
|
|
32
|
+
currency: input.currency || 'USDC',
|
|
33
|
+
recipient: input.recipient || input.payTo || input.receiverAddress || input.creatorWallet || '',
|
|
34
|
+
license: input.license || input.terms || ''
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function settingsToAccessPolicy(settings = {}) {
|
|
39
|
+
return normalizeAccessPolicy({
|
|
40
|
+
humans: settings.humanAccess,
|
|
41
|
+
agents: settings.agentAccess
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function settingsToUnlockPolicy(settings = {}) {
|
|
46
|
+
return normalizeUnlockPolicy(settings.unlockMode || settings.unlock || 'one_time');
|
|
47
|
+
}
|