feat: implement permanent audio access grants with pay-to-unlock

This commit is contained in:
Codex
2026-02-18 12:32:15 +00:00
parent d90d9aa1f7
commit 5e5ee9f8e7
2 changed files with 145 additions and 0 deletions

78
src/lib/access.js Normal file
View File

@@ -0,0 +1,78 @@
"use strict";
class AudioAccessStore {
constructor() {
this.grants = new Map();
}
_key(audioId) {
return String(audioId);
}
hasGrant(audioId, userId) {
const key = this._key(audioId);
const set = this.grants.get(key);
return Boolean(set && set.has(userId));
}
grantAccess(audioId, userId) {
const key = this._key(audioId);
if (!this.grants.has(key)) {
this.grants.set(key, new Set());
}
this.grants.get(key).add(userId);
}
canAccess({ audio, userId }) {
if (!audio || !audio.id) {
throw new Error("audio_required");
}
if (!userId) {
return { allowed: false, reason: "auth_required" };
}
if (audio.ownerUserId === userId) {
return { allowed: true, reason: "owner" };
}
if (this.hasGrant(audio.id, userId)) {
return { allowed: true, reason: "grant" };
}
return {
allowed: false,
reason: "payment_required",
creditsRequired: audio.creditsCharged,
};
}
unlockWithCredits({ audio, userId, wallet }) {
if (!audio || !audio.id) {
throw new Error("audio_required");
}
if (!userId) {
throw new Error("auth_required");
}
if (audio.ownerUserId === userId || this.hasGrant(audio.id, userId)) {
return { unlocked: true, charged: 0 };
}
wallet.applyTransaction({
userId,
type: "debit",
amount: audio.creditsCharged,
reason: "audio_unlock",
idempotencyKey: `unlock:${audio.id}:${userId}`,
});
this.grantAccess(audio.id, userId);
return { unlocked: true, charged: audio.creditsCharged };
}
}
module.exports = {
AudioAccessStore,
};