attachment-external-ref-store.mjs
134 lines 3.8 KB
Raw
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor ⚠ breaking 10 days ago
1 /**
2 * Credential-free external media reference store (Phase 2F-b-d-kn-b).
3 *
4 * Apply target for media_external_link proposals; fourth read-model derivation source.
5 *
6 * @see docs/MEDIA-WRITE-SURFACES-CONTRACT-2F-b-d-kn.md §9.1
7 */
8
9 import fs from 'fs';
10 import path from 'path';
11 import { randomUUID } from 'crypto';
12
13 const STORE_FILENAME = 'hub_attachment_external_refs.json';
14
15 /**
16 * @typedef {'personal'|'project'|'org'} RefScope
17 */
18
19 /**
20 * @typedef {Object} ExternalRefRecord
21 * @property {string} connector_id
22 * @property {string} opaque_ref
23 * @property {RefScope} scope
24 * @property {string} display_label
25 * @property {string} consent_id
26 * @property {string} created
27 * @property {string} updated
28 */
29
30 /**
31 * @param {string} dataDir
32 * @returns {string}
33 */
34 export function getExternalRefStorePath(dataDir) {
35 return path.join(dataDir, STORE_FILENAME);
36 }
37
38 /**
39 * @param {string} dataDir
40 * @returns {{ schema: string, vaults: Record<string, { refs?: Record<string, ExternalRefRecord> }> }}
41 */
42 export function loadExternalRefStore(dataDir) {
43 const filePath = getExternalRefStorePath(dataDir);
44 if (!fs.existsSync(filePath)) {
45 return { schema: 'knowtation.attachment_external_ref/v0', vaults: {} };
46 }
47 try {
48 const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
49 if (!parsed || typeof parsed !== 'object' || !parsed.vaults || typeof parsed.vaults !== 'object') {
50 return { schema: 'knowtation.attachment_external_ref/v0', vaults: {} };
51 }
52 return {
53 schema: 'knowtation.attachment_external_ref/v0',
54 vaults: parsed.vaults,
55 };
56 } catch {
57 return { schema: 'knowtation.attachment_external_ref/v0', vaults: {} };
58 }
59 }
60
61 /**
62 * @param {string} dataDir
63 * @param {object} store
64 */
65 export function saveExternalRefStore(dataDir, store) {
66 const filePath = getExternalRefStorePath(dataDir);
67 const dir = path.dirname(filePath);
68 if (!fs.existsSync(dir)) {
69 fs.mkdirSync(dir, { recursive: true });
70 }
71 const payload = {
72 schema: 'knowtation.attachment_external_ref/v0',
73 vaults: store.vaults ?? {},
74 };
75 const tmp = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
76 fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
77 fs.renameSync(tmp, filePath);
78 }
79
80 /**
81 * @param {string} dataDir
82 * @param {string} vaultId
83 * @returns {Record<string, ExternalRefRecord>}
84 */
85 export function getVaultExternalRefs(dataDir, vaultId) {
86 const store = loadExternalRefStore(dataDir);
87 const vault = store.vaults?.[vaultId];
88 if (!vault?.refs || typeof vault.refs !== 'object') {
89 return {};
90 }
91 return vault.refs;
92 }
93
94 /**
95 * @param {string} dataDir
96 * @param {string} vaultId
97 * @param {string} attachmentId
98 * @returns {ExternalRefRecord|null}
99 */
100 export function getExternalRef(dataDir, vaultId, attachmentId) {
101 const refs = getVaultExternalRefs(dataDir, vaultId);
102 const row = refs[attachmentId];
103 return row && typeof row === 'object' ? row : null;
104 }
105
106 /**
107 * Idempotent upsert of an external reference record.
108 *
109 * @param {string} dataDir
110 * @param {string} vaultId
111 * @param {string} attachmentId
112 * @param {ExternalRefRecord} record
113 */
114 export function upsertExternalRef(dataDir, vaultId, attachmentId, record) {
115 const store = loadExternalRefStore(dataDir);
116 if (!store.vaults[vaultId]) {
117 store.vaults[vaultId] = { refs: {} };
118 }
119 if (!store.vaults[vaultId].refs) {
120 store.vaults[vaultId].refs = {};
121 }
122 const existing = store.vaults[vaultId].refs[attachmentId];
123 const now = new Date().toISOString();
124 store.vaults[vaultId].refs[attachmentId] = {
125 connector_id: record.connector_id,
126 opaque_ref: record.opaque_ref,
127 scope: record.scope,
128 display_label: record.display_label,
129 consent_id: record.consent_id,
130 created: existing?.created ?? record.created ?? now,
131 updated: now,
132 };
133 saveExternalRefStore(dataDir, store);
134 }
File History 1 commit
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor 10 days ago