@nibgate/sdk 0.2.0 → 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 +215 -18
- 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 +22 -32
- 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,
|
|
@@ -218,10 +208,12 @@ export function protect(resource, handler, options = {}) {
|
|
|
218
208
|
|
|
219
209
|
export function verifyPayment(options = {}) {
|
|
220
210
|
return async function verifyPaymentMiddleware(req, res, next) {
|
|
221
|
-
const
|
|
211
|
+
const contentId = req.params?.id || req.body?.resourceId || '';
|
|
212
|
+
|
|
213
|
+
const proofHeader = req.headers?.['x-nibgate-payment-proof'] || '';
|
|
222
214
|
if (proofHeader) {
|
|
223
215
|
const nibgate = createNibgateServer(options);
|
|
224
|
-
const payload = nibgate.verifyUnlockToken(proofHeader, { id:
|
|
216
|
+
const payload = nibgate.verifyUnlockToken(proofHeader, { id: contentId });
|
|
225
217
|
if (payload) {
|
|
226
218
|
req.nibgate = { unlock: payload, verified: true };
|
|
227
219
|
return next();
|
|
@@ -229,11 +221,11 @@ export function verifyPayment(options = {}) {
|
|
|
229
221
|
}
|
|
230
222
|
|
|
231
223
|
if ((options.paymentMode || process.env.NIBGATE_PAYMENT_MODE) === 'circle-gateway') {
|
|
232
|
-
const sigHeader = req.headers?.['payment-signature'] ||
|
|
224
|
+
const sigHeader = req.headers?.['payment-signature'] || '';
|
|
233
225
|
if (sigHeader) {
|
|
234
|
-
const resource = options.resource || { id:
|
|
226
|
+
const resource = options.resource || { id: contentId, price: req.body?.price || '0' };
|
|
235
227
|
const gateway = await runCircleGatewayRequirement(
|
|
236
|
-
{ headers:
|
|
228
|
+
{ headers: req.headers, method: req.method || 'GET', url: req.originalUrl || req.url || '/' },
|
|
237
229
|
resource,
|
|
238
230
|
options
|
|
239
231
|
);
|
|
@@ -241,13 +233,11 @@ export function verifyPayment(options = {}) {
|
|
|
241
233
|
req.nibgate = { payment: gateway.payment, verified: true };
|
|
242
234
|
return next();
|
|
243
235
|
}
|
|
244
|
-
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.' });
|
|
245
237
|
}
|
|
246
238
|
}
|
|
247
239
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
challenge: createPaymentChallenge(options.resource || { id: req.params?.id || '', price: req.body?.price || '0' }, options)
|
|
251
|
-
});
|
|
240
|
+
const errBody = createPaymentChallenge(options.resource || { id: contentId, price: req.body?.price || '0' }, options);
|
|
241
|
+
return res.status(402).json(errBody);
|
|
252
242
|
};
|
|
253
243
|
}
|
|
@@ -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>
|