media-import-consent.mjs
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf
mirror: GitHub Phase A durable MCP OAuth (#270)
Human
minor
⚠ breaking
10 days ago
| 1 | /** |
| 2 | * Per-vault media import consent store (Phase 2F-b-d-kn-b). |
| 3 | * |
| 4 | * Human-granted, revocable, expiry-aware — no connector credentials. |
| 5 | * |
| 6 | * @see docs/MEDIA-WRITE-SURFACES-CONTRACT-2F-b-d-kn.md §6 |
| 7 | */ |
| 8 | |
| 9 | import fs from 'fs'; |
| 10 | import path from 'path'; |
| 11 | import { randomUUID, randomBytes } from 'crypto'; |
| 12 | |
| 13 | const STORE_FILENAME = 'hub_media_import_consent.json'; |
| 14 | |
| 15 | export const CONSENT_ID_RE = /^mic_[a-f0-9]{16}$/; |
| 16 | |
| 17 | /** |
| 18 | * @typedef {'personal'|'project'|'org'} ConsentScope |
| 19 | * @typedef {'active'|'revoked'} ConsentStatus |
| 20 | */ |
| 21 | |
| 22 | /** |
| 23 | * @typedef {Object} MediaImportConsentRecord |
| 24 | * @property {string} connector_id |
| 25 | * @property {ConsentScope} scope |
| 26 | * @property {string} granted_by |
| 27 | * @property {string} granted_at |
| 28 | * @property {string|null} expires_at |
| 29 | * @property {ConsentStatus} status |
| 30 | */ |
| 31 | |
| 32 | /** |
| 33 | * @param {string} dataDir |
| 34 | * @returns {string} |
| 35 | */ |
| 36 | export function getMediaImportConsentPath(dataDir) { |
| 37 | return path.join(dataDir, STORE_FILENAME); |
| 38 | } |
| 39 | |
| 40 | /** |
| 41 | * @param {string} dataDir |
| 42 | * @returns {{ schema: string, vaults: Record<string, { consents?: Record<string, MediaImportConsentRecord> }> }} |
| 43 | */ |
| 44 | export function loadMediaImportConsentStore(dataDir) { |
| 45 | const filePath = getMediaImportConsentPath(dataDir); |
| 46 | if (!fs.existsSync(filePath)) { |
| 47 | return { schema: 'knowtation.media_import_consent/v0', vaults: {} }; |
| 48 | } |
| 49 | try { |
| 50 | const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')); |
| 51 | if (!parsed || typeof parsed !== 'object' || !parsed.vaults || typeof parsed.vaults !== 'object') { |
| 52 | return { schema: 'knowtation.media_import_consent/v0', vaults: {} }; |
| 53 | } |
| 54 | return { |
| 55 | schema: 'knowtation.media_import_consent/v0', |
| 56 | vaults: parsed.vaults, |
| 57 | }; |
| 58 | } catch { |
| 59 | return { schema: 'knowtation.media_import_consent/v0', vaults: {} }; |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | /** |
| 64 | * @param {string} dataDir |
| 65 | * @param {object} store |
| 66 | */ |
| 67 | export function saveMediaImportConsentStore(dataDir, store) { |
| 68 | const filePath = getMediaImportConsentPath(dataDir); |
| 69 | const dir = path.dirname(filePath); |
| 70 | if (!fs.existsSync(dir)) { |
| 71 | fs.mkdirSync(dir, { recursive: true }); |
| 72 | } |
| 73 | const payload = { |
| 74 | schema: 'knowtation.media_import_consent/v0', |
| 75 | vaults: store.vaults ?? {}, |
| 76 | }; |
| 77 | const tmp = `${filePath}.${process.pid}.${randomUUID()}.tmp`; |
| 78 | fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8'); |
| 79 | fs.renameSync(tmp, filePath); |
| 80 | } |
| 81 | |
| 82 | /** @returns {string} */ |
| 83 | export function mintConsentId() { |
| 84 | return `mic_${randomBytes(8).toString('hex')}`; |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * @param {string} dataDir |
| 89 | * @param {string} vaultId |
| 90 | * @param {string} connectorId |
| 91 | * @param {ConsentScope} scope |
| 92 | * @param {string|Date} [now] |
| 93 | * @returns {MediaImportConsentRecord|null} |
| 94 | */ |
| 95 | export function getActiveConsent(dataDir, vaultId, connectorId, scope, now = new Date()) { |
| 96 | const store = loadMediaImportConsentStore(dataDir); |
| 97 | const vault = store.vaults?.[vaultId]; |
| 98 | if (!vault?.consents || typeof vault.consents !== 'object') return null; |
| 99 | |
| 100 | const nowMs = now instanceof Date ? now.getTime() : new Date(now).getTime(); |
| 101 | |
| 102 | for (const record of Object.values(vault.consents)) { |
| 103 | if (!record || typeof record !== 'object') continue; |
| 104 | if (record.connector_id !== connectorId) continue; |
| 105 | if (record.scope !== scope) continue; |
| 106 | if (record.status !== 'active') continue; |
| 107 | if (record.expires_at != null) { |
| 108 | const exp = new Date(record.expires_at).getTime(); |
| 109 | if (!Number.isNaN(exp) && exp <= nowMs) continue; |
| 110 | } |
| 111 | return record; |
| 112 | } |
| 113 | return null; |
| 114 | } |
| 115 | |
| 116 | /** |
| 117 | * @param {string} dataDir |
| 118 | * @param {string} vaultId |
| 119 | * @param {ConsentScope} [scopeFilter] |
| 120 | * @returns {{ consent_id: string, record: MediaImportConsentRecord }[]} |
| 121 | */ |
| 122 | export function listVaultConsents(dataDir, vaultId, scopeFilter) { |
| 123 | const store = loadMediaImportConsentStore(dataDir); |
| 124 | const vault = store.vaults?.[vaultId]; |
| 125 | if (!vault?.consents) return []; |
| 126 | const out = []; |
| 127 | for (const [consentId, record] of Object.entries(vault.consents)) { |
| 128 | if (!record || typeof record !== 'object') continue; |
| 129 | if (scopeFilter && record.scope !== scopeFilter) continue; |
| 130 | out.push({ consent_id: consentId, record }); |
| 131 | } |
| 132 | out.sort((a, b) => a.consent_id.localeCompare(b.consent_id)); |
| 133 | return out; |
| 134 | } |
File History
1 commit
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf
mirror: GitHub Phase A durable MCP OAuth (#270)
Human
minor
⚠
10 days ago