@nibgate/sdk 0.2.1 → 0.2.2
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/SKILL.md +136 -10
- package/dist/nibgate.js +352 -362
- package/dist/nibgate.js.map +4 -4
- package/dist/nibgate.min.js +2 -2
- package/dist/nibgate.min.js.map +4 -4
- package/package.json +6 -3
- package/src/browser/access.js +168 -0
- package/src/browser/checkout.js +53 -0
- package/src/browser/client.js +97 -0
- package/src/browser/evm-gateway.js +180 -0
- package/src/browser/gate.js +63 -0
- package/src/browser/index.js +9 -770
- package/src/browser/rating-ui.js +161 -0
- package/src/browser/track.js +45 -0
- package/src/server/access.js +14 -29
- package/src/server/admin-page.html +168 -0
- package/src/server/admin.js +18 -180
- package/src/server/gateway.js +8 -3
- package/src/server/webhooks.js +2 -2
- package/src/server/runtime.js +0 -39
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { normalizeResource, normalizeContentType } from '../core/resource.js';
|
|
2
|
+
import { normalizeRating, ratingMessage } from '../core/rating.js';
|
|
3
|
+
import { emit } from './events.js';
|
|
4
|
+
import { browserWindow } from './env.js';
|
|
5
|
+
import { rateContentOnchain } from './reputation.js';
|
|
6
|
+
import { createGate } from './gate.js';
|
|
7
|
+
|
|
8
|
+
export function rateResource(resource, rating = {}, extra = {}) {
|
|
9
|
+
const normalized = normalizeResource(resource);
|
|
10
|
+
const normalizedRating = normalizeRating(rating);
|
|
11
|
+
const payload = {
|
|
12
|
+
...extra,
|
|
13
|
+
...normalizedRating,
|
|
14
|
+
ratingMessage: extra.ratingMessage || rating.message || rating.ratingMessage || ratingMessage(normalized, normalizedRating, extra),
|
|
15
|
+
ratingSignature: extra.ratingSignature || rating.signature || rating.ratingSignature || undefined,
|
|
16
|
+
resource: normalized
|
|
17
|
+
};
|
|
18
|
+
return emit('content_rating', payload);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createOnchainRating(resource, options = {}) {
|
|
22
|
+
const item = createGate(resource, options.gateOptions || {});
|
|
23
|
+
const win = browserWindow();
|
|
24
|
+
const statusTarget = typeof options.status === 'string' ? win?.document.querySelector(options.status) : options.status;
|
|
25
|
+
const ratingTarget = typeof options.ratingTarget === 'string' ? win?.document.querySelector(options.ratingTarget) : options.ratingTarget;
|
|
26
|
+
const buttonSelector = options.ratingButtons || options.buttons || '[data-nibgate-rating-value], [data-rating]';
|
|
27
|
+
const explicitButtons = Array.isArray(options.buttons)
|
|
28
|
+
? options.buttons
|
|
29
|
+
: typeof options.buttons === 'string'
|
|
30
|
+
? Array.from(win?.document.querySelectorAll(options.buttons) || [])
|
|
31
|
+
: options.buttons
|
|
32
|
+
? [options.buttons]
|
|
33
|
+
: null;
|
|
34
|
+
const buttons = explicitButtons || Array.from(win?.document.querySelectorAll(buttonSelector) || []);
|
|
35
|
+
const source = options.source || 'nibgate-onchain-rating';
|
|
36
|
+
|
|
37
|
+
let busy = false;
|
|
38
|
+
let payment = options.payment || null;
|
|
39
|
+
|
|
40
|
+
function setStatus(message) {
|
|
41
|
+
if (typeof options.onStatus === 'function') options.onStatus(message);
|
|
42
|
+
if (statusTarget) statusTarget.textContent = message || '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setBusy(value) {
|
|
46
|
+
busy = Boolean(value);
|
|
47
|
+
buttons.forEach((button) => {
|
|
48
|
+
if (button && 'disabled' in button) button.disabled = busy;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function setPayment(nextPayment = null) {
|
|
53
|
+
payment = nextPayment || null;
|
|
54
|
+
return payment;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function setVisible(isVisible) {
|
|
58
|
+
if (!ratingTarget) return Boolean(isVisible);
|
|
59
|
+
if ('hidden' in ratingTarget) ratingTarget.hidden = !isVisible;
|
|
60
|
+
ratingTarget.setAttribute('aria-hidden', isVisible ? 'false' : 'true');
|
|
61
|
+
return Boolean(isVisible);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function valueFromButton(button) {
|
|
65
|
+
const raw = button?.dataset?.nibgateRatingValue || button?.dataset?.rating || button?.value || button?.textContent;
|
|
66
|
+
const numeric = Number.parseFloat(String(raw || '').replace(/[^\d.]/g, ''));
|
|
67
|
+
return Number.isFinite(numeric) ? numeric : Number(options.rating || options.stars || 0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function rate(input = {}) {
|
|
71
|
+
setBusy(true);
|
|
72
|
+
try {
|
|
73
|
+
const rating = Number.parseFloat(input.rating ?? input.stars ?? input.value ?? options.rating ?? options.stars);
|
|
74
|
+
if (!Number.isFinite(rating)) throw new Error('Choose a rating before sending.');
|
|
75
|
+
const paymentId = input.paymentId || options.paymentId || (typeof options.getPaymentId === 'function' ? options.getPaymentId() : payment?.paymentId);
|
|
76
|
+
const unlockRef = input.unlockRef || options.unlockRef
|
|
77
|
+
|| (typeof options.getUnlockRef === 'function' ? options.getUnlockRef() : null)
|
|
78
|
+
|| paymentId || payment?.txHash || payment?.transactionHash || '';
|
|
79
|
+
|
|
80
|
+
setStatus(options.pendingMessage || 'Send the onchain rating transaction...');
|
|
81
|
+
const result = await rateContentOnchain(item.resource, { ...options, ...input, rating, paymentId, unlockRef, source });
|
|
82
|
+
setStatus(options.successMessage || 'Rating sent to Nibgate reputation.');
|
|
83
|
+
if (typeof options.onRated === 'function') options.onRated(result);
|
|
84
|
+
return result;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const message = error?.message || options.errorMessage || 'Rating failed.';
|
|
87
|
+
setStatus(message);
|
|
88
|
+
if (typeof options.onError === 'function') options.onError(error);
|
|
89
|
+
throw error;
|
|
90
|
+
} finally {
|
|
91
|
+
setBusy(false);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function mount() {
|
|
96
|
+
buttons.forEach((button) => {
|
|
97
|
+
button?.addEventListener?.('click', () => rate({ rating: valueFromButton(button) }).catch(() => null));
|
|
98
|
+
});
|
|
99
|
+
if (options.visible !== undefined) setVisible(Boolean(options.visible));
|
|
100
|
+
return controller;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const controller = { resource: item.resource, rate, mount, setPayment, setVisible };
|
|
104
|
+
if (options.autoMount !== false) mount();
|
|
105
|
+
return controller;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function mountRatingUI(resource, options = {}) {
|
|
109
|
+
const item = createGate(resource, options.gateOptions || {});
|
|
110
|
+
const win = browserWindow();
|
|
111
|
+
if (!win) return null;
|
|
112
|
+
const target = typeof options.target === 'string' ? win.document.querySelector(options.target) : options.target;
|
|
113
|
+
if (!target) return null;
|
|
114
|
+
|
|
115
|
+
const stars = [1, 2, 3, 4, 5];
|
|
116
|
+
let selectedRating = 0;
|
|
117
|
+
|
|
118
|
+
const container = win.document.createElement('div');
|
|
119
|
+
container.className = 'nibgate-rating-ui';
|
|
120
|
+
container.style.cssText = 'display:flex;align-items:center;gap:4px;padding:8px 0';
|
|
121
|
+
|
|
122
|
+
const starButtons = stars.map((value) => {
|
|
123
|
+
const btn = win.document.createElement('button');
|
|
124
|
+
btn.type = 'button';
|
|
125
|
+
btn.dataset.nibgateRatingValue = String(value);
|
|
126
|
+
btn.setAttribute('aria-label', `${value} star${value > 1 ? 's' : ''}`);
|
|
127
|
+
btn.innerHTML = '☆';
|
|
128
|
+
btn.style.cssText = 'background:none;border:none;font-size:24px;cursor:pointer;color:#ccc;transition:color 0.15s;padding:2px;line-height:1';
|
|
129
|
+
btn.addEventListener('mouseenter', () => {
|
|
130
|
+
starButtons.forEach((b, i) => b.style.color = i < value ? '#f5b342' : '#ccc');
|
|
131
|
+
});
|
|
132
|
+
btn.addEventListener('mouseleave', () => {
|
|
133
|
+
starButtons.forEach((b, i) => b.style.color = i < selectedRating ? '#f5b342' : '#ccc');
|
|
134
|
+
});
|
|
135
|
+
btn.addEventListener('click', () => {
|
|
136
|
+
selectedRating = value;
|
|
137
|
+
starButtons.forEach((b, i) => b.style.color = i < value ? '#f5b342' : '#ccc');
|
|
138
|
+
rateResource(item.resource, { rating: value }).catch(() => {});
|
|
139
|
+
});
|
|
140
|
+
container.appendChild(btn);
|
|
141
|
+
return btn;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const statusEl = win.document.createElement('span');
|
|
145
|
+
statusEl.style.cssText = 'font-size:13px;color:#888;margin-left:8px';
|
|
146
|
+
statusEl.textContent = options.label || 'Rate this content';
|
|
147
|
+
container.appendChild(statusEl);
|
|
148
|
+
|
|
149
|
+
target.appendChild(container);
|
|
150
|
+
|
|
151
|
+
function rate(r, input = {}) {
|
|
152
|
+
return item.rate({ ...input, rating: r });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function setRating(value) {
|
|
156
|
+
selectedRating = value;
|
|
157
|
+
starButtons.forEach((b, i) => b.style.color = i < value ? '#f5b342' : '#ccc');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { resource: item.resource, container, setRating, rate };
|
|
161
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { validateResourceMetadata } from '../core/resource.js';
|
|
2
|
+
import { browserWindow } from './env.js';
|
|
3
|
+
import { createGate } from './gate.js';
|
|
4
|
+
import { checkResourceAccess } from './access.js';
|
|
5
|
+
|
|
6
|
+
export function trackResourcePage(resource, options = {}) {
|
|
7
|
+
const item = createGate(resource, options.gateOptions || {});
|
|
8
|
+
const validation = validateResourceMetadata(item.resource, options.validation || {});
|
|
9
|
+
if ((validation.warnings.length || validation.errors.length) && options.warn !== false && browserWindow()?.console?.warn) {
|
|
10
|
+
browserWindow().console.warn('Nibgate content metadata needs attention', validation);
|
|
11
|
+
}
|
|
12
|
+
item.content({ source: options.source, metadataQuality: { score: validation.score, warnings: validation.warnings, errors: validation.errors }, ...(options.content || {}) });
|
|
13
|
+
item.view({
|
|
14
|
+
source: options.source,
|
|
15
|
+
path: options.path || browserWindow()?.location?.pathname || item.resource.path,
|
|
16
|
+
referrer: options.referrer ?? browserWindow()?.document?.referrer ?? '',
|
|
17
|
+
...(options.view || {})
|
|
18
|
+
});
|
|
19
|
+
return item;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function setupResourcePage(resource, options = {}) {
|
|
23
|
+
const item = trackResourcePage(resource, options);
|
|
24
|
+
const win = browserWindow();
|
|
25
|
+
if (!win) return item;
|
|
26
|
+
|
|
27
|
+
const button = typeof options.button === 'string' ? win.document.querySelector(options.button) : options.button;
|
|
28
|
+
const statusElement = typeof options.status === 'string' ? win.document.querySelector(options.status) : options.status;
|
|
29
|
+
const setStatus = options.onStatus || ((message) => {
|
|
30
|
+
if (statusElement) statusElement.textContent = message || '';
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (button) {
|
|
34
|
+
button.addEventListener('click', async () => {
|
|
35
|
+
button.disabled = true;
|
|
36
|
+
try {
|
|
37
|
+
await checkResourceAccess(resource, { ...options, onStatus: setStatus });
|
|
38
|
+
} finally {
|
|
39
|
+
button.disabled = false;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return item;
|
|
45
|
+
}
|
package/src/server/access.js
CHANGED
|
@@ -75,13 +75,8 @@ export function createNibgateServer(options = {}) {
|
|
|
75
75
|
|
|
76
76
|
if (access.blocked) {
|
|
77
77
|
return jsonResponse({
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
nibgate: {
|
|
81
|
-
contentId: resource.id,
|
|
82
|
-
actor: access.actor,
|
|
83
|
-
access: resource.access
|
|
84
|
-
}
|
|
78
|
+
ok: false, error: `${access.actor} access is blocked for this resource`,
|
|
79
|
+
nibgate: { contentId: resource.id, actor: access.actor, access: resource.access }
|
|
85
80
|
}, { status: 403 });
|
|
86
81
|
}
|
|
87
82
|
|
|
@@ -96,11 +91,7 @@ export function createNibgateServer(options = {}) {
|
|
|
96
91
|
|
|
97
92
|
if (!access.allowed) {
|
|
98
93
|
if (access.blocked) {
|
|
99
|
-
return jsonResponse({
|
|
100
|
-
status: 403,
|
|
101
|
-
error: `${access.actor} access is blocked for this resource`,
|
|
102
|
-
resource
|
|
103
|
-
}, { status: 403 });
|
|
94
|
+
return jsonResponse({ ok: false, error: `${access.actor} access is blocked for this resource`, resource }, { status: 403 });
|
|
104
95
|
}
|
|
105
96
|
|
|
106
97
|
const rail = normalizePaymentRail(resource.paymentRail || routeOptions.paymentRail || options.paymentRail || routeOptions.paymentMode || options.paymentMode);
|
|
@@ -109,7 +100,10 @@ export function createNibgateServer(options = {}) {
|
|
|
109
100
|
if (gateway.handled) return gateway.response;
|
|
110
101
|
const result = await unlock(resource, gateway.payment);
|
|
111
102
|
if (result.ok) {
|
|
112
|
-
|
|
103
|
+
const response = jsonResponse({ ok: true, resource, payment: gateway.payment, unlockProof: result.unlockProof, expiresInSeconds: result.expiresInSeconds });
|
|
104
|
+
emitHubEvent('payment_completed', resource, { ...options, ...routeOptions, payload: gateway.payment }).catch(() => {});
|
|
105
|
+
emitHubEvent('unlock_completed', resource, { ...options, ...routeOptions, payload: gateway.payment }).catch(() => {});
|
|
106
|
+
return response;
|
|
113
107
|
}
|
|
114
108
|
}
|
|
115
109
|
|
|
@@ -117,7 +111,7 @@ export function createNibgateServer(options = {}) {
|
|
|
117
111
|
const txHash = request.headers?.get?.('x-nibgate-transfer-tx') || request.headers?.get?.('x-transfer-tx') || '';
|
|
118
112
|
if (txHash) {
|
|
119
113
|
if (!verifyTransfer) {
|
|
120
|
-
return jsonResponse({ error: 'Transfer verification is not configured
|
|
114
|
+
return jsonResponse({ ok: false, error: 'Transfer verification is not configured. Pass verifyTransfer to createNibgateServer before using paymentRail: transfer.' }, { status: 501 });
|
|
121
115
|
}
|
|
122
116
|
const transferPayment = {
|
|
123
117
|
paymentProvider: 'direct-transfer',
|
|
@@ -130,7 +124,7 @@ export function createNibgateServer(options = {}) {
|
|
|
130
124
|
network: routeOptions.network || options.network || serverEnv('NIBGATE_PAYMENT_NETWORK') || 'eip155:5042002'
|
|
131
125
|
};
|
|
132
126
|
const verified = await verifyTransfer({ resource, txHash, payment: transferPayment, request });
|
|
133
|
-
if (!verified) return jsonResponse({ error: 'Transfer verification failed' }, { status: 402 });
|
|
127
|
+
if (!verified) return jsonResponse({ ok: false, error: 'Transfer verification failed' }, { status: 402 });
|
|
134
128
|
const result = await unlock(resource, { ...transferPayment, verified: true });
|
|
135
129
|
if (result.ok) return jsonResponse({ ok: true, resource, payment: { ...transferPayment, verified: true }, unlockProof: result.unlockProof, expiresInSeconds: result.expiresInSeconds });
|
|
136
130
|
}
|
|
@@ -157,18 +151,14 @@ export function createNibgateServer(options = {}) {
|
|
|
157
151
|
if (!gatewayResult.ok) return jsonResponse(gatewayResult, { status: gatewayResult.status || 500 });
|
|
158
152
|
payment = gatewayResult.payment;
|
|
159
153
|
} else {
|
|
160
|
-
return jsonResponse({
|
|
161
|
-
success: false,
|
|
162
|
-
error: 'Real payments are required',
|
|
163
|
-
detail: 'Set NIBGATE_PAYMENT_MODE=circle-gateway for real local payment tests.'
|
|
164
|
-
}, { status: 400 });
|
|
154
|
+
return jsonResponse({ ok: false, error: 'Real payments are required', detail: 'Set NIBGATE_PAYMENT_MODE=circle-gateway for real local payment tests.' }, { status: 400 });
|
|
165
155
|
}
|
|
166
156
|
|
|
167
157
|
const result = await unlock(resource, payment);
|
|
168
|
-
if (!result.ok) return jsonResponse({
|
|
158
|
+
if (!result.ok) return jsonResponse({ ok: false, ...result }, { status: result.status || 402 });
|
|
169
159
|
|
|
170
160
|
const response = jsonResponse({
|
|
171
|
-
|
|
161
|
+
ok: true,
|
|
172
162
|
unlockProof: result.unlockProof,
|
|
173
163
|
expiresInSeconds: result.expiresInSeconds,
|
|
174
164
|
payment,
|
|
@@ -234,13 +224,8 @@ export function verifyPayment(options = {}) {
|
|
|
234
224
|
const sigHeader = req.headers?.['payment-signature'] || '';
|
|
235
225
|
if (sigHeader) {
|
|
236
226
|
const resource = options.resource || { id: contentId, price: req.body?.price || '0' };
|
|
237
|
-
const gatewayHeaders = Object.assign(Object.create(null), req.headers || {});
|
|
238
|
-
gatewayHeaders.forEach = function (cb) {
|
|
239
|
-
var self = this;
|
|
240
|
-
Object.keys(self).forEach(function (k) { if (k !== 'forEach') cb(self[k], k); });
|
|
241
|
-
};
|
|
242
227
|
const gateway = await runCircleGatewayRequirement(
|
|
243
|
-
{ headers:
|
|
228
|
+
{ headers: req.headers, method: req.method || 'GET', url: req.originalUrl || req.url || '/' },
|
|
244
229
|
resource,
|
|
245
230
|
options
|
|
246
231
|
);
|
|
@@ -248,7 +233,7 @@ export function verifyPayment(options = {}) {
|
|
|
248
233
|
req.nibgate = { payment: gateway.payment, verified: true };
|
|
249
234
|
return next();
|
|
250
235
|
}
|
|
251
|
-
return res.status(402).json({ error: 'Payment verification failed', detail: 'The payment signature could not be verified.' });
|
|
236
|
+
return res.status(402).json({ ok: false, error: 'Payment verification failed', detail: 'The payment signature could not be verified.' });
|
|
252
237
|
}
|
|
253
238
|
}
|
|
254
239
|
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>${TITLE}</title>
|
|
7
|
+
<style>
|
|
8
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
9
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f5f5f0;color:#1a1a18;padding:24px;max-width:960px;margin:0 auto}
|
|
10
|
+
h1{font-size:28px;font-weight:700;margin-bottom:8px}
|
|
11
|
+
p.sub{color:#666;margin-bottom:24px}
|
|
12
|
+
.card{background:#fff;border-radius:12px;padding:20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,0.08)}
|
|
13
|
+
.card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
|
|
14
|
+
.card-header h3{font-size:18px;font-weight:600}
|
|
15
|
+
.status{font-size:12px;padding:3px 10px;border-radius:99px;font-weight:600}
|
|
16
|
+
.status-gated{background:#fef3c7;color:#92400e}
|
|
17
|
+
.status-open{background:#d1fae5;color:#065f46}
|
|
18
|
+
.status-hidden{background:#fce4ec;color:#c62828}
|
|
19
|
+
.field-row{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px}
|
|
20
|
+
.field{display:flex;flex-direction:column;gap:4px}
|
|
21
|
+
.field label{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#555}
|
|
22
|
+
.field input,.field select{font-size:14px;padding:8px 10px;border:1px solid #ddd;border-radius:6px;background:#fff}
|
|
23
|
+
.field input:focus,.field select:focus{outline:2px solid #7c9a6d;border-color:transparent}
|
|
24
|
+
.field .checkbox-row{display:flex;align-items:center;gap:8px;padding-top:6px}
|
|
25
|
+
.field .checkbox-row input[type="checkbox"]{width:18px;height:18px}
|
|
26
|
+
.empty{text-align:center;padding:48px;color:#888}
|
|
27
|
+
.toast{position:fixed;bottom:24px;right:24px;background:#1a1a18;color:#fff;padding:12px 20px;border-radius:8px;font-size:14px;opacity:0;transition:opacity 0.3s;z-index:100}
|
|
28
|
+
.toast.show{opacity:1}
|
|
29
|
+
.toast.error{background:#c62828}
|
|
30
|
+
.header-bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}
|
|
31
|
+
.header-bar a{color:#7c9a6d;text-decoration:none;font-size:14px;font-weight:600}
|
|
32
|
+
.header-bar a:hover{text-decoration:underline}
|
|
33
|
+
.help-text{font-size:12px;color:#888;margin-top:2px}
|
|
34
|
+
</style>
|
|
35
|
+
</head>
|
|
36
|
+
<body>
|
|
37
|
+
<div class="header-bar">
|
|
38
|
+
<div>
|
|
39
|
+
<h1>Nibgate Admin</h1>
|
|
40
|
+
<p class="sub">Manage gating, pricing, and settings for your content.</p>
|
|
41
|
+
</div>
|
|
42
|
+
<a href="/nibgate.json" target="_blank">View manifest →</a>
|
|
43
|
+
</div>
|
|
44
|
+
<div id="app"><div class="empty">Loading resources...</div></div>
|
|
45
|
+
<div id="toast" class="toast"></div>
|
|
46
|
+
<script>
|
|
47
|
+
const API = '${API_BASE}';
|
|
48
|
+
let fields = [];
|
|
49
|
+
let items = [];
|
|
50
|
+
|
|
51
|
+
async function load() {
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetch(API + '/resources');
|
|
54
|
+
const data = await res.json();
|
|
55
|
+
if (!data.ok) throw new Error(data.error || 'Failed to load');
|
|
56
|
+
fields = data.fields || [];
|
|
57
|
+
items = data.items || [];
|
|
58
|
+
render();
|
|
59
|
+
} catch (err) {
|
|
60
|
+
document.getElementById('app').innerHTML = '<div class="empty">Error loading: ' + err.message + '</div>';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function statusLabel(item) {
|
|
65
|
+
if (item.humanAccess === 'paid') return '<span class="status status-gated">Gated</span>';
|
|
66
|
+
if (item.humanAccess === 'blocked') return '<span class="status status-hidden">Blocked</span>';
|
|
67
|
+
return '<span class="status status-open">Open</span>';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function render() {
|
|
71
|
+
const app = document.getElementById('app');
|
|
72
|
+
if (!items.length) {
|
|
73
|
+
app.innerHTML = '<div class="empty">No resources configured yet. Add resources through the Nibgate SDK and they will appear here after the next manifest sync.</div>';
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
app.innerHTML = items.map(function(item) {
|
|
77
|
+
return '<div class="card" data-id="' + item.id + '">' +
|
|
78
|
+
'<div class="card-header">' +
|
|
79
|
+
'<h3>' + escapeHtml(item.title || item.id) + '</h3>' +
|
|
80
|
+
statusLabel(item) +
|
|
81
|
+
'</div>' +
|
|
82
|
+
'<div class="field-row">' +
|
|
83
|
+
selectField('humanAccess', 'Human access', ['free','paid','blocked'], item.humanAccess) +
|
|
84
|
+
selectField('agentAccess', 'Agent access', ['free','paid','blocked'], item.agentAccess) +
|
|
85
|
+
'</div>' +
|
|
86
|
+
'<div class="field-row">' +
|
|
87
|
+
selectField('unlockMode', 'Unlock mode', ['one_time'], item.unlockMode) +
|
|
88
|
+
selectField('paymentRail', 'Payment rail', ['gateway','transfer'], item.paymentRail || 'gateway') +
|
|
89
|
+
'</div>' +
|
|
90
|
+
'<div class="field-row">' +
|
|
91
|
+
textField('price', 'Price (' + (item.currency || 'USDC') + ')', item.price) +
|
|
92
|
+
textField('currency', 'Currency', item.currency || 'USDC') +
|
|
93
|
+
'</div>' +
|
|
94
|
+
'<div class="field-row">' +
|
|
95
|
+
textField('recipient', 'Recipient wallet', item.recipient || '') +
|
|
96
|
+
checkboxField('ratingsEnabled', 'Enable onchain ratings', item.ratingsEnabled !== false) +
|
|
97
|
+
'</div>' +
|
|
98
|
+
'<div class="field-row">' +
|
|
99
|
+
selectField('type', 'Content type', ['article','music','video','image'], item.type || 'article') +
|
|
100
|
+
checkboxField('publishToNibgate', 'Publish to discovery', item.publishToNibgate !== false) +
|
|
101
|
+
'</div>' +
|
|
102
|
+
'<div><button class="save-btn" onclick="save(this)">Save</button></div>' +
|
|
103
|
+
'</div>';
|
|
104
|
+
}).join('');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function textField(name, label, value) {
|
|
108
|
+
return '<div class="field"><label>' + label + '</label><input type="text" name="' + name + '" value="' + escapeAttr(value || '') + '" /></div>';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function selectField(name, label, options, value) {
|
|
112
|
+
const opts = (options || []).map(function(o) { return '<option value="' + o + '"' + (o === value ? ' selected' : '') + '>' + o + '</option>'; }).join('');
|
|
113
|
+
return '<div class="field"><label>' + label + '</label><select name="' + name + '">' + opts + '</select></div>';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function checkboxField(name, label, checked) {
|
|
117
|
+
return '<div class="field"><label>' + label + '</label><div class="checkbox-row"><input type="checkbox" name="' + name + '"' + (checked ? ' checked' : '') + ' /></div></div>';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function escapeHtml(str) {
|
|
121
|
+
var div = document.createElement('div');
|
|
122
|
+
div.textContent = str;
|
|
123
|
+
return div.innerHTML;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function escapeAttr(str) {
|
|
127
|
+
return String(str).replace(/"/g,'"').replace(/'/g,''');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function save(btn) {
|
|
131
|
+
var card = btn.closest('.card');
|
|
132
|
+
var id = card.dataset.id;
|
|
133
|
+
var inputs = card.querySelectorAll('[name]');
|
|
134
|
+
var body = {};
|
|
135
|
+
inputs.forEach(function(inp) {
|
|
136
|
+
if (inp.type === 'checkbox') body[inp.name] = inp.checked;
|
|
137
|
+
else body[inp.name] = inp.value;
|
|
138
|
+
});
|
|
139
|
+
btn.disabled = true;
|
|
140
|
+
btn.textContent = 'Saving...';
|
|
141
|
+
try {
|
|
142
|
+
var res = await fetch(API + '/resources/' + encodeURIComponent(id), {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: { 'content-type': 'application/json' },
|
|
145
|
+
body: JSON.stringify(body)
|
|
146
|
+
});
|
|
147
|
+
var data = await res.json();
|
|
148
|
+
if (!data.ok) throw new Error(data.error || 'Save failed');
|
|
149
|
+
toast('Saved ' + (data.item.title || id), 'success');
|
|
150
|
+
load();
|
|
151
|
+
} catch (err) {
|
|
152
|
+
toast(err.message, 'error');
|
|
153
|
+
btn.disabled = false;
|
|
154
|
+
btn.textContent = 'Save';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function toast(msg, type) {
|
|
159
|
+
var el = document.getElementById('toast');
|
|
160
|
+
el.textContent = msg;
|
|
161
|
+
el.className = 'toast ' + (type==='error'?'error':'') + ' show';
|
|
162
|
+
setTimeout(function() { el.classList.remove('show'); }, 3000);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
load();
|
|
166
|
+
</script>
|
|
167
|
+
</body>
|
|
168
|
+
</html>
|