@noy-db/in-pinia 0.1.0-pre.9 → 0.2.0-pre.10
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/dist/index.cjs +190 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +199 -2
- package/dist/index.d.ts +199 -2
- package/dist/index.js +186 -24
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -26,13 +26,16 @@ __export(index_exports, {
|
|
|
26
26
|
getActiveNoydb: () => getActiveNoydb,
|
|
27
27
|
resolveNoydb: () => resolveNoydb,
|
|
28
28
|
setActiveNoydb: () => setActiveNoydb,
|
|
29
|
-
useCapabilityGrant: () => useCapabilityGrant
|
|
29
|
+
useCapabilityGrant: () => useCapabilityGrant,
|
|
30
|
+
useDictLabel: () => useDictLabel,
|
|
31
|
+
useI18nField: () => useI18nField,
|
|
32
|
+
useNoydbI18n: () => useNoydbI18n
|
|
30
33
|
});
|
|
31
34
|
module.exports = __toCommonJS(index_exports);
|
|
32
35
|
|
|
33
36
|
// src/defineNoydbStore.ts
|
|
34
|
-
var
|
|
35
|
-
var
|
|
37
|
+
var import_pinia2 = require("pinia");
|
|
38
|
+
var import_vue2 = require("vue");
|
|
36
39
|
|
|
37
40
|
// src/context.ts
|
|
38
41
|
var activeInstance = null;
|
|
@@ -50,13 +53,57 @@ function resolveNoydb(explicit) {
|
|
|
50
53
|
);
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
// src/useNoydbI18n.ts
|
|
57
|
+
var import_pinia = require("pinia");
|
|
58
|
+
var import_vue = require("vue");
|
|
59
|
+
var useNoydbI18n = (0, import_pinia.defineStore)("noydb-i18n", () => {
|
|
60
|
+
const locale = (0, import_vue.ref)("en");
|
|
61
|
+
const fallback = (0, import_vue.ref)(["en", "any"]);
|
|
62
|
+
function setLocale(l, opts) {
|
|
63
|
+
locale.value = l;
|
|
64
|
+
const sync = opts?.syncVault;
|
|
65
|
+
if (sync) {
|
|
66
|
+
const vaults = Array.isArray(sync) ? sync : [sync];
|
|
67
|
+
for (const v of vaults) v.setLocale(l);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function setFallback(chain) {
|
|
71
|
+
fallback.value = chain;
|
|
72
|
+
}
|
|
73
|
+
function bindTo(source, opts) {
|
|
74
|
+
return (0, import_vue.watch)(
|
|
75
|
+
source,
|
|
76
|
+
(v) => {
|
|
77
|
+
locale.value = v;
|
|
78
|
+
},
|
|
79
|
+
// sync flush so the mirror propagates immediately (no tick lag) —
|
|
80
|
+
// a locale change should be observable synchronously by dependents.
|
|
81
|
+
{ immediate: opts?.immediate ?? true, flush: "sync" }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return { locale, fallback, setLocale, setFallback, bindTo };
|
|
85
|
+
});
|
|
86
|
+
|
|
53
87
|
// src/defineNoydbStore.ts
|
|
54
88
|
function defineNoydbStore(id, options) {
|
|
55
89
|
const collectionName = options.collection ?? id;
|
|
56
90
|
const prefetch = options.prefetch ?? true;
|
|
57
|
-
return (0,
|
|
58
|
-
const items = (0,
|
|
59
|
-
const count = (0,
|
|
91
|
+
return (0, import_pinia2.defineStore)(id, () => {
|
|
92
|
+
const items = (0, import_vue2.shallowRef)([]);
|
|
93
|
+
const count = (0, import_vue2.computed)(() => items.value.length);
|
|
94
|
+
const i18nMode = options.i18n ?? "raw";
|
|
95
|
+
const i18nStore = i18nMode === "follow" ? useNoydbI18n() : null;
|
|
96
|
+
function localeOpts() {
|
|
97
|
+
if (i18nMode === "raw") return { locale: "raw" };
|
|
98
|
+
if (i18nMode === "follow") {
|
|
99
|
+
return { locale: i18nStore.locale, fallback: i18nStore.fallback };
|
|
100
|
+
}
|
|
101
|
+
const l = i18nMode.locale;
|
|
102
|
+
return {
|
|
103
|
+
locale: typeof l === "string" ? l : l.value,
|
|
104
|
+
...i18nMode.fallback !== void 0 ? { fallback: i18nMode.fallback } : {}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
60
107
|
let cachedCompartment = null;
|
|
61
108
|
let cachedCollection = null;
|
|
62
109
|
async function getCollection() {
|
|
@@ -65,12 +112,17 @@ function defineNoydbStore(id, options) {
|
|
|
65
112
|
cachedCompartment = await noydb.openVault(options.vault);
|
|
66
113
|
const collOpts = {};
|
|
67
114
|
if (options.schema !== void 0) collOpts.schema = options.schema;
|
|
115
|
+
if (options.attestation !== void 0) collOpts.attestation = options.attestation;
|
|
116
|
+
if (options.persistJsonSchema !== void 0) collOpts.persistJsonSchema = options.persistJsonSchema;
|
|
117
|
+
if (options.schemaUpdate !== void 0) collOpts.schemaUpdate = options.schemaUpdate;
|
|
118
|
+
if (options.i18nFields !== void 0) collOpts.i18nFields = options.i18nFields;
|
|
119
|
+
if (options.dictKeyFields !== void 0) collOpts.dictKeyFields = options.dictKeyFields;
|
|
68
120
|
cachedCollection = cachedCompartment.collection(collectionName, collOpts);
|
|
69
121
|
return cachedCollection;
|
|
70
122
|
}
|
|
71
123
|
async function refresh() {
|
|
72
124
|
const c = await getCollection();
|
|
73
|
-
const list = await c.list();
|
|
125
|
+
const list = await c.list(localeOpts());
|
|
74
126
|
items.value = list;
|
|
75
127
|
}
|
|
76
128
|
function byId(id2) {
|
|
@@ -82,7 +134,7 @@ function defineNoydbStore(id, options) {
|
|
|
82
134
|
async function add(id2, record) {
|
|
83
135
|
const c = await getCollection();
|
|
84
136
|
await c.put(id2, record);
|
|
85
|
-
items.value = await c.list();
|
|
137
|
+
items.value = await c.list(localeOpts());
|
|
86
138
|
}
|
|
87
139
|
async function update(id2, record) {
|
|
88
140
|
await add(id2, record);
|
|
@@ -90,7 +142,7 @@ function defineNoydbStore(id, options) {
|
|
|
90
142
|
async function remove(id2) {
|
|
91
143
|
const c = await getCollection();
|
|
92
144
|
await c.delete(id2);
|
|
93
|
-
items.value = await c.list();
|
|
145
|
+
items.value = await c.list(localeOpts());
|
|
94
146
|
}
|
|
95
147
|
function query() {
|
|
96
148
|
if (!cachedCollection) {
|
|
@@ -108,8 +160,8 @@ function defineNoydbStore(id, options) {
|
|
|
108
160
|
}
|
|
109
161
|
const built = build(cachedCollection.query());
|
|
110
162
|
const live = built.live();
|
|
111
|
-
const items2 = (0,
|
|
112
|
-
const error = (0,
|
|
163
|
+
const items2 = (0, import_vue2.shallowRef)(live.value);
|
|
164
|
+
const error = (0, import_vue2.ref)(live.error);
|
|
113
165
|
const unsubscribe = live.subscribe(() => {
|
|
114
166
|
items2.value = live.value;
|
|
115
167
|
error.value = live.error;
|
|
@@ -121,10 +173,22 @@ function defineNoydbStore(id, options) {
|
|
|
121
173
|
unsubscribe();
|
|
122
174
|
live.stop();
|
|
123
175
|
};
|
|
124
|
-
if ((0,
|
|
176
|
+
if ((0, import_vue2.getCurrentScope)()) (0, import_vue2.onScopeDispose)(stop);
|
|
125
177
|
return { items: items2, error, stop };
|
|
126
178
|
}
|
|
127
179
|
const $ready = prefetch ? refresh() : Promise.resolve();
|
|
180
|
+
if (i18nMode === "follow") {
|
|
181
|
+
(0, import_vue2.watch)(
|
|
182
|
+
() => [i18nStore.locale, i18nStore.fallback],
|
|
183
|
+
() => {
|
|
184
|
+
void refresh();
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
} else if (typeof i18nMode === "object" && (0, import_vue2.isRef)(i18nMode.locale)) {
|
|
188
|
+
(0, import_vue2.watch)(i18nMode.locale, () => {
|
|
189
|
+
void refresh();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
128
192
|
return {
|
|
129
193
|
items,
|
|
130
194
|
count,
|
|
@@ -140,6 +204,102 @@ function defineNoydbStore(id, options) {
|
|
|
140
204
|
});
|
|
141
205
|
}
|
|
142
206
|
|
|
207
|
+
// src/useI18nField.ts
|
|
208
|
+
var import_vue3 = require("vue");
|
|
209
|
+
var import_i18n = require("@noy-db/hub/i18n");
|
|
210
|
+
function useI18nField(source, opts = {}) {
|
|
211
|
+
const i18n = useNoydbI18n();
|
|
212
|
+
return (0, import_vue3.computed)(() => {
|
|
213
|
+
const map = typeof source === "function" ? source() : source;
|
|
214
|
+
if (!map || typeof map !== "object") return null;
|
|
215
|
+
const locale = opts.locale !== void 0 ? (0, import_vue3.unref)(opts.locale) : i18n.locale;
|
|
216
|
+
const fallback = opts.fallback ?? i18n.fallback;
|
|
217
|
+
const out = (0, import_i18n.resolveI18nText)(
|
|
218
|
+
map,
|
|
219
|
+
locale,
|
|
220
|
+
fallback,
|
|
221
|
+
void 0,
|
|
222
|
+
{ policy: "null" }
|
|
223
|
+
);
|
|
224
|
+
return typeof out === "string" ? out : null;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/useDictLabel.ts
|
|
229
|
+
var import_vue4 = require("vue");
|
|
230
|
+
function useDictLabel(dictionaryName, options = {}) {
|
|
231
|
+
const vault = resolveVault(options.vault);
|
|
232
|
+
const handle = vault.dictionary(dictionaryName);
|
|
233
|
+
let storeLocale = null;
|
|
234
|
+
let storeFallback = null;
|
|
235
|
+
if (options.locale === void 0 || options.fallback === void 0) {
|
|
236
|
+
try {
|
|
237
|
+
const i18n = useNoydbI18n();
|
|
238
|
+
storeLocale = (0, import_vue4.toRef)(i18n, "locale");
|
|
239
|
+
storeFallback = i18n.fallback;
|
|
240
|
+
} catch {
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const localeRef = options.locale !== void 0 ? normaliseLocale(options.locale) : storeLocale ?? (0, import_vue4.ref)("en");
|
|
244
|
+
const fallback = options.fallback ?? storeFallback ?? ["en", "any"];
|
|
245
|
+
const onMissingMode = options.onMissing ?? "key";
|
|
246
|
+
const missing = (key) => onMissingMode === "empty" ? "" : onMissingMode === "placeholder" ? `\u27E8missing:${key}\u27E9` : key;
|
|
247
|
+
const cache = /* @__PURE__ */ new Map();
|
|
248
|
+
const db = resolveNoydb();
|
|
249
|
+
const dictCollection = `_dict_${dictionaryName}`;
|
|
250
|
+
const onChange = (event) => {
|
|
251
|
+
if (event.collection !== dictCollection) return;
|
|
252
|
+
for (const [key, r] of cache) {
|
|
253
|
+
void refresh(key, r);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
db.on("change", onChange);
|
|
257
|
+
(0, import_vue4.watch)(localeRef, () => {
|
|
258
|
+
for (const [key, r] of cache) {
|
|
259
|
+
void refresh(key, r);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
async function refresh(key, r) {
|
|
263
|
+
try {
|
|
264
|
+
const resolved = await handle.resolveLabel(
|
|
265
|
+
key,
|
|
266
|
+
localeRef.value,
|
|
267
|
+
fallback
|
|
268
|
+
);
|
|
269
|
+
r.value = resolved ?? missing(key);
|
|
270
|
+
} catch {
|
|
271
|
+
r.value = missing(key);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return (key) => {
|
|
275
|
+
let r = cache.get(key);
|
|
276
|
+
if (r) return r;
|
|
277
|
+
r = (0, import_vue4.shallowRef)(missing(key));
|
|
278
|
+
cache.set(key, r);
|
|
279
|
+
void refresh(key, r);
|
|
280
|
+
return r;
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function resolveVault(source) {
|
|
284
|
+
const db = resolveNoydb();
|
|
285
|
+
if (source && typeof source !== "string") return source;
|
|
286
|
+
if (typeof source === "string") {
|
|
287
|
+
return db.vault(source);
|
|
288
|
+
}
|
|
289
|
+
const anyDb = db;
|
|
290
|
+
if (anyDb.vaultCache && anyDb.vaultCache.size > 0) {
|
|
291
|
+
return [...anyDb.vaultCache.values()][0];
|
|
292
|
+
}
|
|
293
|
+
throw new Error(
|
|
294
|
+
'[@noy-db/in-pinia] useDictLabel: no open vault. Pass `{ vault: "name" }` or `await db.openVault(name)` first.'
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
function normaliseLocale(locale) {
|
|
298
|
+
if (locale === void 0) return (0, import_vue4.ref)("en");
|
|
299
|
+
if (typeof locale === "string") return (0, import_vue4.ref)(locale);
|
|
300
|
+
return locale;
|
|
301
|
+
}
|
|
302
|
+
|
|
143
303
|
// src/plugin.ts
|
|
144
304
|
var import_hub = require("@noy-db/hub");
|
|
145
305
|
var STATE_DOC_ID = "__state__";
|
|
@@ -233,18 +393,18 @@ function pickKeys(state, persist) {
|
|
|
233
393
|
}
|
|
234
394
|
|
|
235
395
|
// src/useCapabilityGrant.ts
|
|
236
|
-
var
|
|
396
|
+
var import_vue5 = require("vue");
|
|
237
397
|
var CAPABILITY_REQUESTS_COLLECTION = "_capability_requests";
|
|
238
398
|
function useCapabilityGrant(capability, options) {
|
|
239
|
-
const state = (0,
|
|
240
|
-
const error = (0,
|
|
241
|
-
const recordRef = (0,
|
|
399
|
+
const state = (0, import_vue5.ref)("idle");
|
|
400
|
+
const error = (0, import_vue5.ref)(null);
|
|
401
|
+
const recordRef = (0, import_vue5.shallowRef)(null);
|
|
242
402
|
const inBrowser = typeof window !== "undefined";
|
|
243
403
|
let expiryTimer = null;
|
|
244
404
|
let unsubscribeChangeStream = null;
|
|
245
405
|
let resolvedVault = null;
|
|
246
406
|
let stopped = false;
|
|
247
|
-
async function
|
|
407
|
+
async function resolveVault2() {
|
|
248
408
|
if (resolvedVault) return resolvedVault;
|
|
249
409
|
if (typeof options.vault === "string") {
|
|
250
410
|
const noydb = resolveNoydb(null);
|
|
@@ -291,21 +451,21 @@ function useCapabilityGrant(capability, options) {
|
|
|
291
451
|
state.value = "idle";
|
|
292
452
|
recordRef.value = null;
|
|
293
453
|
}
|
|
294
|
-
const now = (0,
|
|
454
|
+
const now = (0, import_vue5.ref)(Date.now());
|
|
295
455
|
const tickTimer = inBrowser ? setInterval(() => {
|
|
296
456
|
now.value = Date.now();
|
|
297
457
|
}, 1e3) : null;
|
|
298
|
-
const timeRemaining = (0,
|
|
458
|
+
const timeRemaining = (0, import_vue5.computed)(() => {
|
|
299
459
|
if (state.value !== "granted" || !recordRef.value?.expiresAt) return 0;
|
|
300
460
|
void now.value;
|
|
301
461
|
const ms = new Date(recordRef.value.expiresAt).getTime() - Date.now();
|
|
302
462
|
return ms > 0 ? ms : 0;
|
|
303
463
|
});
|
|
304
|
-
(0,
|
|
464
|
+
(0, import_vue5.watch)(
|
|
305
465
|
() => recordRef.value?.id,
|
|
306
466
|
async (id) => {
|
|
307
467
|
if (!inBrowser || !id || unsubscribeChangeStream) return;
|
|
308
|
-
const vault = await
|
|
468
|
+
const vault = await resolveVault2();
|
|
309
469
|
const coll = vault.collection(
|
|
310
470
|
CAPABILITY_REQUESTS_COLLECTION
|
|
311
471
|
);
|
|
@@ -337,7 +497,7 @@ function useCapabilityGrant(capability, options) {
|
|
|
337
497
|
error.value = null;
|
|
338
498
|
if (!inBrowser) return;
|
|
339
499
|
try {
|
|
340
|
-
const vault = await
|
|
500
|
+
const vault = await resolveVault2();
|
|
341
501
|
const coll = vault.collection(
|
|
342
502
|
CAPABILITY_REQUESTS_COLLECTION
|
|
343
503
|
);
|
|
@@ -370,7 +530,7 @@ function useCapabilityGrant(capability, options) {
|
|
|
370
530
|
}
|
|
371
531
|
error.value = null;
|
|
372
532
|
try {
|
|
373
|
-
const vault = await
|
|
533
|
+
const vault = await resolveVault2();
|
|
374
534
|
if (vault.role !== options.approver && vault.role !== "owner") {
|
|
375
535
|
throw new Error(
|
|
376
536
|
`useCapabilityGrant: caller role "${vault.role}" cannot approve a "${options.approver}"-tier grant`
|
|
@@ -406,7 +566,7 @@ function useCapabilityGrant(capability, options) {
|
|
|
406
566
|
error.value = null;
|
|
407
567
|
try {
|
|
408
568
|
clearExpiryTimer();
|
|
409
|
-
const vault = await
|
|
569
|
+
const vault = await resolveVault2();
|
|
410
570
|
const released = { ...record, status: "released" };
|
|
411
571
|
const coll = vault.collection(
|
|
412
572
|
CAPABILITY_REQUESTS_COLLECTION
|
|
@@ -420,8 +580,8 @@ function useCapabilityGrant(capability, options) {
|
|
|
420
580
|
throw error.value;
|
|
421
581
|
}
|
|
422
582
|
}
|
|
423
|
-
if ((0,
|
|
424
|
-
(0,
|
|
583
|
+
if ((0, import_vue5.getCurrentScope)()) {
|
|
584
|
+
(0, import_vue5.onScopeDispose)(() => {
|
|
425
585
|
stopped = true;
|
|
426
586
|
clearExpiryTimer();
|
|
427
587
|
if (tickTimer) clearInterval(tickTimer);
|
|
@@ -438,6 +598,9 @@ function useCapabilityGrant(capability, options) {
|
|
|
438
598
|
getActiveNoydb,
|
|
439
599
|
resolveNoydb,
|
|
440
600
|
setActiveNoydb,
|
|
441
|
-
useCapabilityGrant
|
|
601
|
+
useCapabilityGrant,
|
|
602
|
+
useDictLabel,
|
|
603
|
+
useI18nField,
|
|
604
|
+
useNoydbI18n
|
|
442
605
|
});
|
|
443
606
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/defineNoydbStore.ts","../src/context.ts","../src/plugin.ts","../src/useCapabilityGrant.ts"],"sourcesContent":["/**\n * @noy-db/pinia — Pinia integration for noy-db.\n *\n * Two adoption paths:\n *\n * 1. **Greenfield** — `defineNoydbStore<T>(id, options)` creates a new\n * Pinia store fully wired to a NOYDB collection.\n *\n * 2. **Augmentation** — `createNoydbPiniaPlugin(options)` lets existing\n * `defineStore()` stores opt into NOYDB persistence by adding one\n * `noydb:` option, with no component code changes.\n *\n * Plus a global instance binding for both paths:\n * - `setActiveNoydb(instance)` / `getActiveNoydb()` / `resolveNoydb()`\n */\n\nexport { defineNoydbStore } from './defineNoydbStore.js'\nexport type {\n NoydbStoreOptions,\n NoydbStore,\n NoydbLiveQuery,\n} from './defineNoydbStore.js'\nexport { setActiveNoydb, getActiveNoydb, resolveNoydb } from './context.js'\nexport { createNoydbPiniaPlugin } from './plugin.js'\nexport type { StoreNoydbOptions, NoydbPiniaPluginOptions } from './plugin.js'\nexport {\n useCapabilityGrant,\n CAPABILITY_REQUESTS_COLLECTION,\n} from './useCapabilityGrant.js'\nexport type {\n UseCapabilityGrantOptions,\n UseCapabilityGrantReturn,\n CapabilityGrantState,\n CapabilityGrantRecord,\n} from './useCapabilityGrant.js'\n","/**\n * `defineNoydbStore` — drop-in `defineStore` that wires a Pinia store to a\n * NOYDB vault + collection.\n *\n * Returned store exposes:\n * - `items` — reactive array of all records\n * - `byId(id)` — O(1) lookup\n * - `count` — reactive count getter\n * - `add(id, rec)` — encrypt + persist + update reactive state\n * - `update(id, rec)` — same as add (Collection.put is upsert)\n * - `remove(id)` — delete + update reactive state\n * - `refresh()` — re-hydrate from the adapter\n * - `query()` — chainable query DSL bound to the store\n * - `$ready` — Promise<void> resolved on first hydration\n *\n * Compatible with `storeToRefs`, Vue Devtools, SSR, and pinia plugins.\n */\n\nimport { defineStore } from 'pinia'\nimport {\n computed,\n getCurrentScope,\n onScopeDispose,\n ref,\n shallowRef,\n type Ref,\n type ShallowRef,\n type ComputedRef,\n} from 'vue'\nimport type {\n Noydb,\n Vault,\n Collection,\n Query,\n StandardSchemaV1,\n} from '@noy-db/hub'\nimport { resolveNoydb } from './context.js'\n\n/**\n * Reactive handle returned by `store.liveQuery(fn)`. Mirrors a hub\n * `LiveQuery<R>` into Vue refs; `items` updates on every left- or\n * joined-right-side mutation, `error` carries re-run errors as state\n * (a `DanglingReferenceError` in strict join mode is the common case),\n * `stop()` tears down upstream subscriptions. Auto-disposed on scope\n * teardown when called inside a Vue setup / Pinia store body.\n */\nexport interface NoydbLiveQuery<R> {\n items: ShallowRef<readonly R[]>\n error: Ref<Error | null>\n stop(): void\n}\n\n/**\n * Options accepted by `defineNoydbStore`.\n *\n * Generic `T` is the record shape — defaults to `unknown` if the caller\n * doesn't supply a type. Use `defineNoydbStore<Invoice>('invoices', {...})`\n * for full type safety.\n */\nexport interface NoydbStoreOptions<T> {\n /** Vault (tenant) name. */\n vault: string\n /** Collection name within the vault. Defaults to the store id. */\n collection?: string\n /**\n * Optional explicit Noydb instance. If omitted, the store resolves the\n * globally bound instance via `getActiveNoydb()`.\n */\n noydb?: Noydb | null\n /**\n * If true (default), hydration kicks off immediately when the store is\n * first instantiated. If false, hydration is deferred until the first\n * call to `refresh()` or any read accessor.\n */\n prefetch?: boolean\n /**\n * Optional schema validator.\n *\n * Accepts any [Standard Schema v1](https://standardschema.dev) validator\n * — Zod, Valibot, ArkType, Effect Schema, etc. The same validator is\n * installed on the underlying `Collection`, so every `put()` is\n * validated **before encryption** and every read is validated **after\n * decryption**. The store's `add`/`update` methods inherit this\n * validation automatically; no duplicate `.parse()` call is needed.\n *\n * Schema-less stores behave exactly as before (no validation, no\n * perf cost, backwards compatible with usage).\n */\n schema?: StandardSchemaV1<unknown, T>\n}\n\n/**\n * The runtime shape of the store returned by `defineNoydbStore`.\n * Exposed as a public type so consumers can write `useStore: ReturnType<typeof useInvoices>`.\n */\nexport interface NoydbStore<T> {\n items: Ref<T[]>\n count: ComputedRef<number>\n $ready: Promise<void>\n byId(id: string): T | undefined\n add(id: string, record: T): Promise<void>\n update(id: string, record: T): Promise<void>\n remove(id: string): Promise<void>\n refresh(): Promise<void>\n query(): Query<T>\n liveQuery<R = T>(build: (q: Query<T>) => Query<R>): NoydbLiveQuery<R>\n}\n\n/**\n * Define a Pinia store that's wired to a NOYDB collection.\n *\n * Generic T defaults to `unknown` — pass `<MyType>` for full type inference.\n *\n * @example\n * ```ts\n * import { defineNoydbStore } from '@noy-db/in-pinia';\n *\n * export const useInvoices = defineNoydbStore<Invoice>('invoices', {\n * vault: 'C101',\n * schema: InvoiceSchema, // optional\n * });\n * ```\n */\nexport function defineNoydbStore<T>(\n id: string,\n options: NoydbStoreOptions<T>,\n) {\n const collectionName = options.collection ?? id\n const prefetch = options.prefetch ?? true\n\n return defineStore(id, () => {\n // Reactive state. shallowRef on items because the array reference is what\n // changes — replacing it triggers reactivity without per-record proxying.\n const items: Ref<T[]> = shallowRef<T[]>([])\n const count = computed(() => items.value.length)\n\n // Lazy collection handle — created on first hydrate.\n let cachedCompartment: Vault | null = null\n let cachedCollection: Collection<T> | null = null\n\n async function getCollection(): Promise<Collection<T>> {\n if (cachedCollection) return cachedCollection\n const noydb = resolveNoydb(options.noydb ?? null)\n cachedCompartment = await noydb.openVault(options.vault)\n // Pass the schema down to the Collection so validation runs at\n // the encrypt/decrypt boundary instead of only at the store\n // layer. This catches drifted stored data on read (which the\n // old `options.schema.parse(record)` call in add() could not do).\n const collOpts: Parameters<typeof cachedCompartment.collection<T>>[1] = {}\n if (options.schema !== undefined) collOpts.schema = options.schema\n cachedCollection = cachedCompartment.collection<T>(collectionName, collOpts)\n return cachedCollection\n }\n\n async function refresh(): Promise<void> {\n const c = await getCollection()\n const list = await c.list()\n items.value = list\n }\n\n function byId(id: string): T | undefined {\n // Linear scan against the reactive cache. Index-aware lookups planned.\n // Optimization opportunity: maintain a Map<string, T> alongside items.\n for (const item of items.value) {\n if ((item as { id?: string }).id === id) return item\n }\n return undefined\n }\n\n async function add(id: string, record: T): Promise<void> {\n // No explicit validation here — the Collection's own schema hook\n // runs before encryption, which means we get validation AND\n // transforms applied consistently across every code path that\n // writes to the collection (add/update/remove, future batch\n // operations, raw Collection.put calls). Users who want to\n // pre-validate in the UI layer can still do so with their own\n // schema handle.\n const c = await getCollection()\n await c.put(id, record)\n // Re-list to pick up the new record. Cheaper alternative would be to\n // splice into items.value directly, but list() ensures consistency\n // with the underlying cache.\n items.value = await c.list()\n }\n\n async function update(id: string, record: T): Promise<void> {\n // Collection.put is upsert; this is just a more readable alias.\n await add(id, record)\n }\n\n async function remove(id: string): Promise<void> {\n const c = await getCollection()\n await c.delete(id)\n items.value = await c.list()\n }\n\n function query(): Query<T> {\n // Synchronous query() requires the collection to be hydrated.\n // The lazy refresh() in $ready handles that — but if the user calls\n // query() before $ready resolves, the collection still works because\n // Collection.query() reads from its own internal cache (which Noydb\n // hydrates lazily as well).\n if (!cachedCollection) {\n throw new Error(\n '@noy-db/pinia: query() called before the store was ready. ' +\n 'Await store.$ready first, or set prefetch: true (default).',\n )\n }\n return cachedCollection.query()\n }\n\n function liveQuery<R = T>(\n build: (q: Query<T>) => Query<R>,\n ): NoydbLiveQuery<R> {\n if (!cachedCollection) {\n throw new Error(\n '@noy-db/pinia: liveQuery() called before the store was ready. ' +\n 'Await store.$ready first, or set prefetch: true (default).',\n )\n }\n const built = build(cachedCollection.query())\n const live = built.live()\n\n const items = shallowRef<readonly R[]>(live.value)\n const error = ref<Error | null>(live.error)\n\n const unsubscribe = live.subscribe(() => {\n items.value = live.value\n error.value = live.error\n })\n\n let stopped = false\n const stop = (): void => {\n if (stopped) return\n stopped = true\n unsubscribe()\n live.stop()\n }\n\n // Auto-teardown when the calling scope (a Vue component's setup,\n // a Pinia store body, or any user-created effectScope) disposes.\n // Outside an active scope (raw test harness, SSR top-level), skip\n // registration silently — caller is responsible for stop().\n if (getCurrentScope()) onScopeDispose(stop)\n\n return { items, error, stop }\n }\n\n // Kick off hydration. The promise is exposed as $ready so components\n // can `await store.$ready` before rendering data-dependent UI.\n const $ready: Promise<void> = prefetch\n ? refresh()\n : Promise.resolve()\n\n return {\n items,\n count,\n $ready,\n byId,\n add,\n update,\n remove,\n refresh,\n query,\n liveQuery,\n }\n })\n}\n","/**\n * Active NOYDB instance binding.\n *\n * `defineNoydbStore` resolves the `Noydb` instance from one of three places,\n * in priority order:\n *\n * 1. The store options' explicit `noydb:` field (highest precedence — useful\n * for tests and multi-database apps).\n * 2. A globally bound instance set via `setActiveNoydb()` — this is what the\n * Nuxt module's runtime plugin and playground apps use.\n * 3. Throws a clear error if neither is set.\n *\n * Keeping the binding pluggable means tests can pass an instance directly\n * without polluting global state.\n */\n\nimport type { Noydb } from '@noy-db/hub'\n\nlet activeInstance: Noydb | null = null\n\n/** Bind a Noydb instance globally. Called by the Nuxt module / app plugin. */\nexport function setActiveNoydb(instance: Noydb | null): void {\n activeInstance = instance\n}\n\n/** Returns the globally bound Noydb instance, or null if none. */\nexport function getActiveNoydb(): Noydb | null {\n return activeInstance\n}\n\n/**\n * Resolve the Noydb instance to use for a store. Throws if no instance is\n * bound — the error message points the developer at the three options.\n */\nexport function resolveNoydb(explicit?: Noydb | null): Noydb {\n if (explicit) return explicit\n if (activeInstance) return activeInstance\n throw new Error(\n '@noy-db/pinia: no Noydb instance bound.\\n' +\n ' Option A — pass `noydb:` directly to defineNoydbStore({...})\\n' +\n ' Option B — call setActiveNoydb(instance) once at app startup\\n' +\n ' Option C — install the @noy-db/nuxt module (Nuxt 4+)',\n )\n}\n","/**\n * `createNoydbPiniaPlugin` — augmentation path for existing Pinia stores.\n *\n * Lets a developer take any existing `defineStore()` call and opt into NOYDB\n * persistence by adding a single `noydb:` option, without touching component\n * code. The plugin watches the chosen state key(s), encrypts on change, syncs\n * to a NOYDB collection, and rehydrates on store init.\n *\n * @example\n * ```ts\n * import { createPinia } from 'pinia';\n * import { createNoydbPiniaPlugin } from '@noy-db/in-pinia';\n * import { jsonFile } from '@noy-db/to-file';\n *\n * const pinia = createPinia();\n * pinia.use(createNoydbPiniaPlugin({\n * adapter: jsonFile({ dir: './data' }),\n * user: 'owner-01',\n * secret: () => promptPassphrase(),\n * }));\n *\n * // existing store — add one option, no component changes:\n * export const useClients = defineStore('clients', {\n * state: () => ({ list: [] as Client[] }),\n * noydb: { vault: 'C101', collection: 'clients', persist: 'list' },\n * });\n * ```\n *\n * Design notes\n * ------------\n * - Each augmented store persists a SINGLE document at id `__state__`\n * containing the picked keys. We don't try to map state arrays onto\n * per-element records — that's `defineNoydbStore`'s territory.\n * - The Noydb instance is constructed lazily on first store-with-noydb\n * instantiation, then memoized for the lifetime of the Pinia app.\n * This means apps that don't actually use any noydb-augmented stores\n * pay zero crypto cost.\n * - `secret` is a function so the passphrase can come from a prompt,\n * biometric unlock, or session token — never stored in config.\n * - The plugin sets `store.$noydbReady` (a `Promise<void>`) and\n * `store.$noydbError` (an `Error | null`) on every augmented store\n * so components can await hydration and surface failures.\n */\n\nimport type { PiniaPluginContext, PiniaPlugin, StateTree } from 'pinia'\nimport { createNoydb, type Noydb, type NoydbOptions, type NoydbStore, type Vault, type Collection } from '@noy-db/hub'\n\n/**\n * Per-store NOYDB configuration. Attached to a Pinia store via the `noydb`\n * option inside `defineStore({ ..., noydb: {...} })`.\n *\n * `persist` selects which top-level state keys to mirror into NOYDB.\n * Pass a single key, an array of keys, or `'*'` to mirror the entire state.\n */\nexport interface StoreNoydbOptions<S extends StateTree = StateTree> {\n /** Vault (tenant) name. */\n vault: string\n /** Collection name within the vault. */\n collection: string\n /**\n * Which state keys to persist. Defaults to `'*'` (the entire state object).\n * Pass a string or string[] to scope to specific keys.\n */\n persist?: keyof S | (keyof S)[] | '*'\n /**\n * Optional schema validator applied at the document level (the persisted\n * subset of state, not individual records). Throws if validation fails on\n * hydration — the store stays at its initial state and `$noydbError` is set.\n */\n schema?: { parse: (input: unknown) => unknown }\n}\n\n/**\n * Configuration for `createNoydbPiniaPlugin`. Mirrors `NoydbOptions` but\n * makes `secret` a function so the passphrase can come from a prompt\n * rather than being stored in config.\n */\nexport interface NoydbPiniaPluginOptions {\n /** The NOYDB store to use for persistence. */\n adapter: NoydbStore\n /** User identifier (matches the keyring file). */\n user: string\n /**\n * Passphrase provider. Called once on first noydb-augmented store\n * instantiation. Return a string or a Promise that resolves to one.\n */\n secret: () => string | Promise<string>\n /** Optional Noydb open-options forwarded to `createNoydb`. */\n noydbOptions?: Partial<Omit<NoydbOptions, 'store' | 'user' | 'secret'>>\n}\n\n// The fixed document id under which a store's persisted state lives. Using a\n// reserved prefix so it can't collide with any user-chosen record id.\nconst STATE_DOC_ID = '__state__'\n\n/**\n * Create a Pinia plugin that wires NOYDB persistence into any store\n * declaring a `noydb:` option.\n *\n * Returns a `PiniaPlugin` directly usable with `pinia.use(...)`.\n */\nexport function createNoydbPiniaPlugin(opts: NoydbPiniaPluginOptions): PiniaPlugin {\n // Single Noydb instance shared across all augmented stores in this Pinia\n // app. Created lazily on first use so apps that never instantiate a\n // noydb-augmented store pay zero crypto cost.\n let dbPromise: Promise<Noydb> | null = null\n function getDb(): Promise<Noydb> {\n if (!dbPromise) {\n dbPromise = (async (): Promise<Noydb> => {\n const secret = await opts.secret()\n return createNoydb({\n store: opts.adapter,\n user: opts.user,\n secret,\n ...opts.noydbOptions,\n })\n })()\n }\n return dbPromise\n }\n\n // Vault cache so opening a vault is a one-time cost per app.\n const vaultCache = new Map<string, Promise<Vault>>()\n function getCompartment(name: string): Promise<Vault> {\n let p = vaultCache.get(name)\n if (!p) {\n p = getDb().then((db) => db.openVault(name))\n vaultCache.set(name, p)\n }\n return p\n }\n\n return (context: PiniaPluginContext) => {\n // Pinia stores can declare arbitrary options on `defineStore`, but the\n // plugin context only exposes them via `context.options`. Pull our\n // `noydb` option out and bail early if it's not present — that's\n // the \"store is untouched\" path for non-augmented stores.\n const noydbOption = (context.options as { noydb?: StoreNoydbOptions }).noydb\n if (!noydbOption) {\n // Mark the store as opted-out so devtools / consumers can detect it.\n context.store.$noydbAugmented = false\n return\n }\n\n context.store.$noydbAugmented = true\n context.store.$noydbError = null as Error | null\n\n // Track in-flight persistence promises so tests (and consumers) can\n // await deterministic flushes via `$noydbFlush()`. Plain Set-of-Promises\n // — entries auto-remove on settle.\n const pending = new Set<Promise<void>>()\n\n // Hydrate-then-subscribe. Both happen inside an async closure so the\n // store can be awaited via `$noydbReady`.\n const ready = (async (): Promise<void> => {\n try {\n const vault = await getCompartment(noydbOption.vault)\n const collection: Collection<StateTree> = vault.collection<StateTree>(\n noydbOption.collection,\n )\n\n // 1. Hydration: read the persisted document (if any) and apply\n // the picked keys onto the store's current state. We use\n // `$patch` so reactivity fires correctly.\n const persisted = await collection.get(STATE_DOC_ID)\n if (persisted) {\n const validated = noydbOption.schema\n ? (noydbOption.schema.parse(persisted) as StateTree)\n : persisted\n const picked = pickKeys(validated, noydbOption.persist)\n context.store.$patch(picked)\n }\n\n // 2. Subscribe: every state mutation triggers an encrypted write\n // of the picked subset back to NOYDB. The subscription captures\n // `collection` so it doesn't re-resolve on every event.\n context.store.$subscribe(\n (_mutation, state) => {\n const subset = pickKeys(state, noydbOption.persist)\n const p = collection.put(STATE_DOC_ID, subset)\n .catch((err: unknown) => {\n context.store.$noydbError = err instanceof Error ? err : new Error(String(err))\n })\n .finally(() => {\n pending.delete(p)\n })\n pending.add(p)\n },\n { detached: true }, // outlive the component that triggered the mutation\n )\n } catch (err) {\n context.store.$noydbError = err instanceof Error ? err : new Error(String(err))\n }\n })()\n\n context.store.$noydbReady = ready\n /**\n * Wait for all in-flight persistence puts to settle. Use this in tests\n * to deterministically observe the encrypted state on the adapter, and\n * in app code before unmounting components that mutated the store.\n */\n context.store.$noydbFlush = async (): Promise<void> => {\n await ready\n // Snapshot the current pending set; new puts added during await\n // are picked up by the next $noydbFlush() call.\n while (pending.size > 0) {\n await Promise.all([...pending])\n }\n }\n }\n}\n\n/**\n * Pick the configured subset of keys from a state object.\n *\n * Behaviors:\n * - `undefined` or `'*'` → returns the entire state shallow-copied\n * - single key string → returns `{ [key]: state[key] }`\n * - key array → returns `{ [k1]: state[k1], [k2]: state[k2], ... }`\n *\n * The result is always a fresh object so callers can mutate it without\n * touching the store's reactive state.\n */\nfunction pickKeys(state: StateTree, persist: StoreNoydbOptions['persist']): StateTree {\n if (persist === undefined || persist === '*') {\n return { ...state }\n }\n if (typeof persist === 'string') {\n return { [persist]: state[persist] } as StateTree\n }\n if (Array.isArray(persist)) {\n const out: StateTree = {}\n for (const key of persist) {\n out[key as string] = state[key as string]\n }\n return out\n }\n // Should be unreachable thanks to the type, but defensive default.\n return { ...state }\n}\n\n// ─── Pinia module augmentation ─────────────────────────────────────\n//\n// Pinia exposes `DefineStoreOptionsBase` as the place where third-party\n// plugins are expected to attach their custom option types. Augmenting it\n// here means `defineStore('x', { ..., noydb: {...} })` autocompletes inside\n// the IDE and type-checks correctly without forcing users to import\n// anything from `@noy-db/pinia`.\n//\n// We also augment `PiniaCustomProperties` so the runtime fields we add to\n// every store (`$noydbReady`, `$noydbError`, `$noydbAugmented`) are typed.\n\ndeclare module 'pinia' {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n export interface DefineStoreOptionsBase<S extends StateTree, Store> {\n /**\n * Opt this store into NOYDB persistence via the\n * `createNoydbPiniaPlugin` augmentation plugin.\n *\n * The chosen state keys are encrypted and persisted to the configured\n * vault + collection on every mutation, and rehydrated on first\n * store access.\n */\n noydb?: StoreNoydbOptions<S>\n }\n\n export interface PiniaCustomProperties {\n /**\n * Resolves once this store has finished its initial hydration from\n * NOYDB. `undefined` for stores that don't declare a `noydb:` option.\n */\n $noydbReady?: Promise<void>\n /**\n * Set when hydration or persistence fails. `null` while healthy.\n * Plugins (and devtools) can poll this to surface storage errors.\n */\n $noydbError?: Error | null\n /**\n * `true` if this store opted into NOYDB persistence via the `noydb:`\n * option, `false` otherwise. Useful for debugging and devtools.\n */\n $noydbAugmented?: boolean\n /**\n * Wait for all in-flight encrypted persistence puts to complete.\n * Useful in tests for deterministic flushing, and in app code before\n * unmounting components that just mutated the store.\n */\n $noydbFlush?: () => Promise<void>\n }\n}\n","/**\n * `useCapabilityGrant` — Vue/Pinia composable for time-boxed\n * capability approval flows.\n *\n * The composable orchestrates a request → approve → expire / release\n * lifecycle for a session-scoped capability. It manages UI state,\n * persists the request to a reserved `_capability_requests` collection\n * so a separate approver session can see it, and tracks TTL with\n * auto-revert.\n *\n * **It does NOT itself flip capability bits.** Capability mechanisms\n * vary across adopters: tier-based deployments wire `onGrant` to\n * `vault.elevate(tier, opts)`; keyring-based deployments wire it to\n * `db.grant(...)`; custom deployments do their own thing. Keeping the\n * actual flip behind a callback avoids introducing a parallel\n * \"capability elevation\" primitive in hub when the existing\n * `vault.elevate()` already covers the time-boxed-grant pattern.\n *\n * ## State machine\n *\n * idle ─── .request() ───────► requested\n * requested ─── .approve() ──► granted\n * granted ─── TTL expires ─► idle\n * granted ─── .release() ──► idle\n *\n * @module\n */\n\nimport {\n computed,\n getCurrentScope,\n onScopeDispose,\n ref,\n shallowRef,\n watch,\n type ComputedRef,\n type Ref,\n} from 'vue'\nimport type { Vault, Role, CollectionChangeEvent } from '@noy-db/hub'\nimport { resolveNoydb } from './context.js'\n\n/** Reserved internal collection that holds capability-grant lifecycle records. */\nexport const CAPABILITY_REQUESTS_COLLECTION = '_capability_requests'\n\nexport type CapabilityGrantState = 'idle' | 'requested' | 'granted' | 'expired'\n\n/**\n * On-disk shape of a capability-grant lifecycle record. Persisted in\n * the reserved {@link CAPABILITY_REQUESTS_COLLECTION}. Encrypted with\n * that collection's DEK at the storage layer; the in-memory shape\n * here is plaintext.\n *\n * The audit trail invariant: this record carries metadata only —\n * capability name, roles, ttl, reason. Never plaintext payload.\n */\nexport interface CapabilityGrantRecord {\n readonly id: string\n readonly capability: string\n readonly requestedBy: string\n readonly approverRole: Role\n readonly reason: string\n readonly ttlMs: number\n readonly status: 'requested' | 'granted' | 'released' | 'expired'\n readonly requestedAt: string\n readonly approvedBy?: string\n readonly approvedAt?: string\n readonly expiresAt?: string\n}\n\nexport interface UseCapabilityGrantOptions {\n /** TTL in milliseconds for the granted window. */\n readonly ttlMs: number\n /** Role required to call `.approve()`. Mismatch throws on `.approve()`. */\n readonly approver: Role\n /** Audit-ledger string. Stamped on the request record; no plaintext payload. */\n readonly reason: string\n /**\n * Optional explicit vault. Either a `Vault` instance or its name.\n * When omitted, resolves the active Noydb instance via\n * `setActiveNoydb()` and opens the first vault the caller has\n * already loaded.\n */\n readonly vault: Vault | string\n /**\n * Called on the approver's session when `.approve()` succeeds. Wire\n * this to whatever capability flip your codebase uses —\n * `vault.elevate(tier, opts)` for tier-based deployments,\n * `db.grant(...)` for keyring-based, custom for custom.\n *\n * The composable does NOT enforce that the capability was actually\n * granted — it just tracks the lifecycle. The post-expiry \"gated\n * call throws\" contract comes from the underlying mechanism the\n * callback wires up (e.g., `ElevationExpiredError` from\n * `vault.elevate`'s lazy TTL check).\n */\n readonly onGrant?: (ctx: {\n record: CapabilityGrantRecord\n vault: Vault\n }) => void | Promise<void>\n /**\n * Called when the grant ends (TTL expiry OR voluntary release).\n * Mirror of `onGrant`. Idempotent — may be called twice if release\n * and expiry race; callers should no-op on the second invocation.\n */\n readonly onRelease?: (ctx: {\n record: CapabilityGrantRecord\n vault: Vault\n cause: 'released' | 'expired'\n }) => void | Promise<void>\n}\n\nexport interface UseCapabilityGrantReturn {\n readonly state: Ref<CapabilityGrantState>\n /** Milliseconds remaining on the granted window; 0 outside `granted`. */\n readonly timeRemaining: ComputedRef<number>\n /** Most recent error from request/approve/release (resets on next op). */\n readonly error: Ref<Error | null>\n /** Issue a request. State must be `idle`. */\n request(): Promise<void>\n /** Approve a pending request. State must be `requested`. */\n approve(): Promise<void>\n /** Voluntarily revoke an active grant. State must be `granted`. */\n release(): Promise<void>\n}\n\n/**\n * Build a reactive capability-grant lifecycle handle.\n *\n * @example Tier-based capability flip\n * ```ts\n * let elevated: ElevatedHandle | null = null\n * const grant = useCapabilityGrant('canExportPlaintext', {\n * vault: 'V1',\n * ttlMs: 15 * 60_000,\n * approver: 'admin',\n * reason: 'bulk export',\n * onGrant: async ({ vault, record }) => {\n * elevated = await vault.elevate(2, {\n * ttlMs: record.ttlMs,\n * reason: record.reason,\n * })\n * },\n * onRelease: async () => { await elevated?.release(); elevated = null },\n * })\n * ```\n */\nexport function useCapabilityGrant(\n capability: string,\n options: UseCapabilityGrantOptions,\n): UseCapabilityGrantReturn {\n const state = ref<CapabilityGrantState>('idle')\n const error = ref<Error | null>(null)\n const recordRef = shallowRef<CapabilityGrantRecord | null>(null)\n\n // SSR / non-browser host: composable is a no-op. Methods reject; the\n // refs stay at their initial values so server-rendered output shows\n // the idle state.\n const inBrowser = typeof window !== 'undefined'\n\n let expiryTimer: ReturnType<typeof setTimeout> | null = null\n let unsubscribeChangeStream: (() => void) | null = null\n let resolvedVault: Vault | null = null\n let stopped = false\n\n async function resolveVault(): Promise<Vault> {\n if (resolvedVault) return resolvedVault\n if (typeof options.vault === 'string') {\n const noydb = resolveNoydb(null)\n resolvedVault = await noydb.openVault(options.vault)\n } else {\n resolvedVault = options.vault\n }\n // Open the requests collection eagerly so the change stream\n // subscription below has a target. We don't typed-cast here —\n // the collection holds CapabilityGrantRecord shapes only.\n resolvedVault.collection<CapabilityGrantRecord>(\n CAPABILITY_REQUESTS_COLLECTION,\n )\n return resolvedVault\n }\n\n function clearExpiryTimer(): void {\n if (expiryTimer) {\n clearTimeout(expiryTimer)\n expiryTimer = null\n }\n }\n\n function scheduleExpiry(record: CapabilityGrantRecord): void {\n if (!record.expiresAt) return\n const remaining = new Date(record.expiresAt).getTime() - Date.now()\n if (remaining <= 0) {\n void handleExpiry(record)\n return\n }\n clearExpiryTimer()\n expiryTimer = setTimeout(() => { void handleExpiry(record) }, remaining)\n }\n\n async function handleExpiry(record: CapabilityGrantRecord): Promise<void> {\n if (stopped) return\n if (state.value !== 'granted') return\n state.value = 'expired'\n try {\n await options.onRelease?.({\n record,\n vault: resolvedVault!,\n cause: 'expired',\n })\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err))\n }\n // Auto-return to idle after the expiry handler — matches the spec's\n // \"state auto-returns to idle\" contract.\n state.value = 'idle'\n recordRef.value = null\n }\n\n // `now` ticks every second so timeRemaining stays reactive without\n // wiring a per-frame loop.\n const now = ref(Date.now())\n const tickTimer = inBrowser\n ? setInterval(() => { now.value = Date.now() }, 1000)\n : null\n\n const timeRemaining = computed(() => {\n if (state.value !== 'granted' || !recordRef.value?.expiresAt) return 0\n // Subscribe to `now` for reactivity, but read the live clock for\n // accuracy — `now` ticks every second so the computed stays\n // honest between ticks too.\n void now.value\n const ms = new Date(recordRef.value.expiresAt).getTime() - Date.now()\n return ms > 0 ? ms : 0\n })\n\n // Subscribe to the requests collection so an approver session sees\n // pending records appear in real time within the same Noydb session.\n // Cross-session visibility additionally requires the sync engine.\n watch(\n () => recordRef.value?.id,\n async (id) => {\n if (!inBrowser || !id || unsubscribeChangeStream) return\n const vault = await resolveVault()\n const coll = vault.collection<CapabilityGrantRecord>(\n CAPABILITY_REQUESTS_COLLECTION,\n )\n unsubscribeChangeStream = coll.subscribe(\n (evt: CollectionChangeEvent<CapabilityGrantRecord>) => {\n if (evt.type !== 'put' || evt.id !== id) return\n const updated = evt.record\n if (!updated || stopped) return\n recordRef.value = updated\n if (updated.status === 'granted') {\n state.value = 'granted'\n scheduleExpiry(updated)\n } else if (updated.status === 'released' || updated.status === 'expired') {\n state.value = 'idle'\n clearExpiryTimer()\n }\n },\n )\n },\n { immediate: false },\n )\n\n async function request(): Promise<void> {\n if (state.value !== 'idle') {\n error.value = new Error(\n `useCapabilityGrant: cannot request from state \"${state.value}\"`,\n )\n throw error.value\n }\n error.value = null\n if (!inBrowser) return\n try {\n const vault = await resolveVault()\n const coll = vault.collection<CapabilityGrantRecord>(\n CAPABILITY_REQUESTS_COLLECTION,\n )\n const id = `cap-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 10)}`\n const record: CapabilityGrantRecord = {\n id,\n capability,\n requestedBy: vault.userId,\n approverRole: options.approver,\n reason: options.reason,\n ttlMs: options.ttlMs,\n status: 'requested',\n requestedAt: new Date().toISOString(),\n }\n await coll.put(id, record)\n recordRef.value = record\n state.value = 'requested'\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err))\n throw error.value\n }\n }\n\n async function approve(): Promise<void> {\n const record = recordRef.value\n if (state.value !== 'requested' || !record) {\n error.value = new Error(\n `useCapabilityGrant: cannot approve from state \"${state.value}\"`,\n )\n throw error.value\n }\n error.value = null\n try {\n const vault = await resolveVault()\n if (vault.role !== options.approver && vault.role !== 'owner') {\n throw new Error(\n `useCapabilityGrant: caller role \"${vault.role}\" cannot approve a \"${options.approver}\"-tier grant`,\n )\n }\n const approvedAt = new Date()\n const expiresAt = new Date(approvedAt.getTime() + options.ttlMs)\n const granted: CapabilityGrantRecord = {\n ...record,\n status: 'granted',\n approvedBy: vault.userId,\n approvedAt: approvedAt.toISOString(),\n expiresAt: expiresAt.toISOString(),\n }\n const coll = vault.collection<CapabilityGrantRecord>(\n CAPABILITY_REQUESTS_COLLECTION,\n )\n await coll.put(record.id, granted)\n recordRef.value = granted\n state.value = 'granted'\n scheduleExpiry(granted)\n await options.onGrant?.({ record: granted, vault })\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err))\n throw error.value\n }\n }\n\n async function release(): Promise<void> {\n const record = recordRef.value\n if (state.value !== 'granted' || !record) {\n // Releasing from non-granted state is a no-op.\n return\n }\n error.value = null\n try {\n clearExpiryTimer()\n const vault = await resolveVault()\n const released: CapabilityGrantRecord = { ...record, status: 'released' }\n const coll = vault.collection<CapabilityGrantRecord>(\n CAPABILITY_REQUESTS_COLLECTION,\n )\n await coll.put(record.id, released)\n recordRef.value = null\n state.value = 'idle'\n await options.onRelease?.({ record, vault, cause: 'released' })\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err))\n throw error.value\n }\n }\n\n if (getCurrentScope()) {\n onScopeDispose(() => {\n stopped = true\n clearExpiryTimer()\n if (tickTimer) clearInterval(tickTimer)\n unsubscribeChangeStream?.()\n })\n }\n\n return { state, timeRemaining, error, request, approve, release }\n}\n"],"mappings":"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","names":["id","items","import_vue"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/defineNoydbStore.ts","../src/context.ts","../src/useNoydbI18n.ts","../src/useI18nField.ts","../src/useDictLabel.ts","../src/plugin.ts","../src/useCapabilityGrant.ts"],"sourcesContent":["/**\n * @noy-db/pinia — Pinia integration for noy-db.\n *\n * Two adoption paths:\n *\n * 1. **Greenfield** — `defineNoydbStore<T>(id, options)` creates a new\n * Pinia store fully wired to a NOYDB collection.\n *\n * 2. **Augmentation** — `createNoydbPiniaPlugin(options)` lets existing\n * `defineStore()` stores opt into NOYDB persistence by adding one\n * `noydb:` option, with no component code changes.\n *\n * Plus a global instance binding for both paths:\n * - `setActiveNoydb(instance)` / `getActiveNoydb()` / `resolveNoydb()`\n */\n\nexport { defineNoydbStore } from './defineNoydbStore.js'\nexport type {\n NoydbStoreOptions,\n NoydbStore,\n NoydbLiveQuery,\n} from './defineNoydbStore.js'\nexport { setActiveNoydb, getActiveNoydb, resolveNoydb } from './context.js'\nexport { useNoydbI18n } from './useNoydbI18n.js'\nexport type { LocaleSyncable, SetLocaleOptions } from './useNoydbI18n.js'\nexport { useI18nField } from './useI18nField.js'\nexport type { UseI18nFieldOptions } from './useI18nField.js'\nexport { useDictLabel } from './useDictLabel.js'\nexport type { UseDictLabelOptions } from './useDictLabel.js'\nexport { createNoydbPiniaPlugin } from './plugin.js'\nexport type { StoreNoydbOptions, NoydbPiniaPluginOptions } from './plugin.js'\nexport {\n useCapabilityGrant,\n CAPABILITY_REQUESTS_COLLECTION,\n} from './useCapabilityGrant.js'\nexport type {\n UseCapabilityGrantOptions,\n UseCapabilityGrantReturn,\n CapabilityGrantState,\n CapabilityGrantRecord,\n} from './useCapabilityGrant.js'\n","/**\n * `defineNoydbStore` — drop-in `defineStore` that wires a Pinia store to a\n * NOYDB vault + collection.\n *\n * Returned store exposes:\n * - `items` — reactive array of all records\n * - `byId(id)` — O(1) lookup\n * - `count` — reactive count getter\n * - `add(id, rec)` — encrypt + persist + update reactive state\n * - `update(id, rec)` — same as add (Collection.put is upsert)\n * - `remove(id)` — delete + update reactive state\n * - `refresh()` — re-hydrate from the adapter\n * - `query()` — chainable query DSL bound to the store\n * - `$ready` — Promise<void> resolved on first hydration\n *\n * Compatible with `storeToRefs`, Vue Devtools, SSR, and pinia plugins.\n */\n\nimport { defineStore } from 'pinia'\nimport {\n computed,\n getCurrentScope,\n onScopeDispose,\n isRef,\n ref,\n shallowRef,\n watch,\n type Ref,\n type ShallowRef,\n type ComputedRef,\n} from 'vue'\nimport type {\n Noydb,\n Vault,\n Collection,\n Query,\n StandardSchemaV1,\n I18nTextDescriptor,\n DictKeyDescriptor,\n} from '@noy-db/hub'\nimport { resolveNoydb } from './context.js'\nimport { useNoydbI18n } from './useNoydbI18n.js'\n\n/**\n * i18n resolution mode for a store's reads.\n * - `'raw'` (default) — items keep `{ [locale]: string }` maps (today's behavior).\n * - `'follow'` — resolve to the global `useNoydbI18n` locale; re-read on flip.\n * - `{ locale, fallback? }` — pin to a fixed locale or own ref.\n */\nexport type NoydbStoreI18nMode =\n | 'raw'\n | 'follow'\n | { locale: string | Ref<string>; fallback?: string | readonly string[] }\n\n/**\n * Reactive handle returned by `store.liveQuery(fn)`. Mirrors a hub\n * `LiveQuery<R>` into Vue refs; `items` updates on every left- or\n * joined-right-side mutation, `error` carries re-run errors as state\n * (a `DanglingReferenceError` in strict join mode is the common case),\n * `stop()` tears down upstream subscriptions. Auto-disposed on scope\n * teardown when called inside a Vue setup / Pinia store body.\n */\nexport interface NoydbLiveQuery<R> {\n items: ShallowRef<readonly R[]>\n error: Ref<Error | null>\n stop(): void\n}\n\n/**\n * Options accepted by `defineNoydbStore`.\n *\n * Generic `T` is the record shape — defaults to `unknown` if the caller\n * doesn't supply a type. Use `defineNoydbStore<Invoice>('invoices', {...})`\n * for full type safety.\n */\nexport interface NoydbStoreOptions<T> {\n /** Vault (tenant) name. */\n vault: string\n /** Collection name within the vault. Defaults to the store id. */\n collection?: string\n /**\n * Optional explicit Noydb instance. If omitted, the store resolves the\n * globally bound instance via `getActiveNoydb()`.\n */\n noydb?: Noydb | null\n /**\n * If true (default), hydration kicks off immediately when the store is\n * first instantiated. If false, hydration is deferred until the first\n * call to `refresh()` or any read accessor.\n */\n prefetch?: boolean\n /**\n * Optional schema validator.\n *\n * Accepts any [Standard Schema v1](https://standardschema.dev) validator\n * — Zod, Valibot, ArkType, Effect Schema, etc. The same validator is\n * installed on the underlying `Collection`, so every `put()` is\n * validated **before encryption** and every read is validated **after\n * decryption**. The store's `add`/`update` methods inherit this\n * validation automatically; no duplicate `.parse()` call is needed.\n *\n * Schema-less stores behave exactly as before (no validation, no\n * perf cost, backwards compatible with usage).\n */\n schema?: StandardSchemaV1<unknown, T>\n /**\n * Optional per-field attestation schema. When set, it's installed on the\n * underlying `Collection` (alongside `schema`) so `vault.issueAttestation(name, id)`\n * can commit the declared fields against the firm's signing key — see\n * `@noy-db/attestation` `AttestationFieldSchema`. Stores without it behave as before.\n */\n attestation?: NonNullable<Parameters<Vault['collection']>[1]>['attestation']\n /**\n * If true, the collection persists a JSON Schema baseline of `schema` so the\n * schema-update protocol can detect drift on later opens. Forwarded as-is to\n * the underlying `Collection`. Required for `schemaUpdate` to take effect.\n */\n persistJsonSchema?: NonNullable<Parameters<Vault['collection']>[1]>['persistJsonSchema']\n /**\n * Ordered schema-update strategies (e.g. `coordinatedCutover`, `additiveOnly`,\n * `lockSchema`) applied when a stored baseline differs from the current\n * `schema`. Forwarded as-is to the underlying `Collection`. Requires\n * `persistJsonSchema: true` (drift detection needs the persisted baseline).\n * Lets a `defineNoydbStore`-defined collection opt into migration tracking\n * declaratively, without a pre-registration `vault.collection(...)` call.\n */\n schemaUpdate?: NonNullable<Parameters<Vault['collection']>[1]>['schemaUpdate']\n /**\n * Per-field `i18nText()` descriptors. Forwarded to the underlying\n * `Collection` so locale resolution and required-translation validation\n * run declaratively without a separate `vault.collection(name, { i18nFields })`\n * pre-registration call.\n */\n i18nFields?: Record<string, I18nTextDescriptor>\n /**\n * Per-field `dictKey()` descriptors. Forwarded to the underlying\n * `Collection` so dictionary label resolution runs declaratively.\n */\n dictKeyFields?: Record<string, DictKeyDescriptor>\n /**\n * How the store resolves i18nText/dictKey fields on read.\n * Default `'raw'` — items keep `{ [locale]: string }` maps (today's\n * behavior; zero breaking change). `'follow'` resolves to the global\n * `useNoydbI18n` locale and re-reads when it changes. `{ locale }`\n * pins to a fixed locale or own ref. Set `'raw'` for stores whose maps\n * feed identity/export reads or a per-cell bilingual toggle.\n */\n i18n?: NoydbStoreI18nMode\n}\n\n/**\n * The runtime shape of the store returned by `defineNoydbStore`.\n * Exposed as a public type so consumers can write `useStore: ReturnType<typeof useInvoices>`.\n */\nexport interface NoydbStore<T> {\n items: Ref<T[]>\n count: ComputedRef<number>\n $ready: Promise<void>\n byId(id: string): T | undefined\n add(id: string, record: T): Promise<void>\n update(id: string, record: T): Promise<void>\n remove(id: string): Promise<void>\n refresh(): Promise<void>\n query(): Query<T>\n liveQuery<R = T>(build: (q: Query<T>) => Query<R>): NoydbLiveQuery<R>\n}\n\n/**\n * Define a Pinia store that's wired to a NOYDB collection.\n *\n * Generic T defaults to `unknown` — pass `<MyType>` for full type inference.\n *\n * @example\n * ```ts\n * import { defineNoydbStore } from '@noy-db/in-pinia';\n *\n * export const useInvoices = defineNoydbStore<Invoice>('invoices', {\n * vault: 'C101',\n * schema: InvoiceSchema, // optional\n * });\n * ```\n */\nexport function defineNoydbStore<T>(\n id: string,\n options: NoydbStoreOptions<T>,\n) {\n const collectionName = options.collection ?? id\n const prefetch = options.prefetch ?? true\n\n return defineStore(id, () => {\n // Reactive state. shallowRef on items because the array reference is what\n // changes — replacing it triggers reactivity without per-record proxying.\n const items: Ref<T[]> = shallowRef<T[]>([])\n const count = computed(() => items.value.length)\n\n // i18n resolution mode. Default 'raw' (today's behavior): reads pass\n // { locale: 'raw' }, items keep their maps. 'follow' resolves to the\n // global useNoydbI18n locale and re-reads on flip; { locale } pins.\n const i18nMode: NoydbStoreI18nMode = options.i18n ?? 'raw'\n const i18nStore = i18nMode === 'follow' ? useNoydbI18n() : null\n function localeOpts(): { locale: string; fallback?: string | readonly string[] } {\n if (i18nMode === 'raw') return { locale: 'raw' }\n if (i18nMode === 'follow') {\n return { locale: i18nStore!.locale, fallback: i18nStore!.fallback }\n }\n const l = i18nMode.locale\n return {\n locale: typeof l === 'string' ? l : l.value,\n ...(i18nMode.fallback !== undefined ? { fallback: i18nMode.fallback } : {}),\n }\n }\n\n // Lazy collection handle — created on first hydrate.\n let cachedCompartment: Vault | null = null\n let cachedCollection: Collection<T> | null = null\n\n async function getCollection(): Promise<Collection<T>> {\n if (cachedCollection) return cachedCollection\n const noydb = resolveNoydb(options.noydb ?? null)\n cachedCompartment = await noydb.openVault(options.vault)\n // Pass the schema down to the Collection so validation runs at\n // the encrypt/decrypt boundary instead of only at the store\n // layer. This catches drifted stored data on read (which the\n // old `options.schema.parse(record)` call in add() could not do).\n const collOpts: Parameters<typeof cachedCompartment.collection<T>>[1] = {}\n if (options.schema !== undefined) collOpts.schema = options.schema\n if (options.attestation !== undefined) collOpts.attestation = options.attestation\n if (options.persistJsonSchema !== undefined) collOpts.persistJsonSchema = options.persistJsonSchema\n if (options.schemaUpdate !== undefined) collOpts.schemaUpdate = options.schemaUpdate\n if (options.i18nFields !== undefined) collOpts.i18nFields = options.i18nFields\n if (options.dictKeyFields !== undefined) collOpts.dictKeyFields = options.dictKeyFields\n cachedCollection = cachedCompartment.collection<T>(collectionName, collOpts)\n return cachedCollection\n }\n\n async function refresh(): Promise<void> {\n const c = await getCollection()\n const list = await c.list(localeOpts())\n items.value = list\n }\n\n function byId(id: string): T | undefined {\n // Linear scan against the reactive cache. Index-aware lookups planned.\n // Optimization opportunity: maintain a Map<string, T> alongside items.\n for (const item of items.value) {\n if ((item as { id?: string }).id === id) return item\n }\n return undefined\n }\n\n async function add(id: string, record: T): Promise<void> {\n // No explicit validation here — the Collection's own schema hook\n // runs before encryption, which means we get validation AND\n // transforms applied consistently across every code path that\n // writes to the collection (add/update/remove, future batch\n // operations, raw Collection.put calls). Users who want to\n // pre-validate in the UI layer can still do so with their own\n // schema handle.\n const c = await getCollection()\n await c.put(id, record)\n // Re-list to pick up the new record. Cheaper alternative would be to\n // splice into items.value directly, but list() ensures consistency\n // with the underlying cache.\n items.value = await c.list(localeOpts())\n }\n\n async function update(id: string, record: T): Promise<void> {\n // Collection.put is upsert; this is just a more readable alias.\n await add(id, record)\n }\n\n async function remove(id: string): Promise<void> {\n const c = await getCollection()\n await c.delete(id)\n items.value = await c.list(localeOpts())\n }\n\n function query(): Query<T> {\n // Synchronous query() requires the collection to be hydrated.\n // The lazy refresh() in $ready handles that — but if the user calls\n // query() before $ready resolves, the collection still works because\n // Collection.query() reads from its own internal cache (which Noydb\n // hydrates lazily as well).\n if (!cachedCollection) {\n throw new Error(\n '@noy-db/pinia: query() called before the store was ready. ' +\n 'Await store.$ready first, or set prefetch: true (default).',\n )\n }\n return cachedCollection.query()\n }\n\n function liveQuery<R = T>(\n build: (q: Query<T>) => Query<R>,\n ): NoydbLiveQuery<R> {\n if (!cachedCollection) {\n throw new Error(\n '@noy-db/pinia: liveQuery() called before the store was ready. ' +\n 'Await store.$ready first, or set prefetch: true (default).',\n )\n }\n const built = build(cachedCollection.query())\n const live = built.live()\n\n const items = shallowRef<readonly R[]>(live.value)\n const error = ref<Error | null>(live.error)\n\n const unsubscribe = live.subscribe(() => {\n items.value = live.value\n error.value = live.error\n })\n\n let stopped = false\n const stop = (): void => {\n if (stopped) return\n stopped = true\n unsubscribe()\n live.stop()\n }\n\n // Auto-teardown when the calling scope (a Vue component's setup,\n // a Pinia store body, or any user-created effectScope) disposes.\n // Outside an active scope (raw test harness, SSR top-level), skip\n // registration silently — caller is responsible for stop().\n if (getCurrentScope()) onScopeDispose(stop)\n\n return { items, error, stop }\n }\n\n // Kick off hydration. The promise is exposed as $ready so components\n // can `await store.$ready` before rendering data-dependent UI.\n const $ready: Promise<void> = prefetch\n ? refresh()\n : Promise.resolve()\n\n // Re-read with the new locale when it changes. 'follow' tracks the\n // global store; a { locale: ref } pin tracks its own ref. 'raw' and\n // a fixed-string locale never change, so no watch.\n if (i18nMode === 'follow') {\n watch(\n () => [i18nStore!.locale, i18nStore!.fallback] as const,\n () => { void refresh() },\n )\n } else if (typeof i18nMode === 'object' && isRef(i18nMode.locale)) {\n watch(i18nMode.locale, () => { void refresh() })\n }\n\n return {\n items,\n count,\n $ready,\n byId,\n add,\n update,\n remove,\n refresh,\n query,\n liveQuery,\n }\n })\n}\n","/**\n * Active NOYDB instance binding.\n *\n * `defineNoydbStore` resolves the `Noydb` instance from one of three places,\n * in priority order:\n *\n * 1. The store options' explicit `noydb:` field (highest precedence — useful\n * for tests and multi-database apps).\n * 2. A globally bound instance set via `setActiveNoydb()` — this is what the\n * Nuxt module's runtime plugin and playground apps use.\n * 3. Throws a clear error if neither is set.\n *\n * Keeping the binding pluggable means tests can pass an instance directly\n * without polluting global state.\n */\n\nimport type { Noydb } from '@noy-db/hub'\n\nlet activeInstance: Noydb | null = null\n\n/** Bind a Noydb instance globally. Called by the Nuxt module / app plugin. */\nexport function setActiveNoydb(instance: Noydb | null): void {\n activeInstance = instance\n}\n\n/** Returns the globally bound Noydb instance, or null if none. */\nexport function getActiveNoydb(): Noydb | null {\n return activeInstance\n}\n\n/**\n * Resolve the Noydb instance to use for a store. Throws if no instance is\n * bound — the error message points the developer at the three options.\n */\nexport function resolveNoydb(explicit?: Noydb | null): Noydb {\n if (explicit) return explicit\n if (activeInstance) return activeInstance\n throw new Error(\n '@noy-db/pinia: no Noydb instance bound.\\n' +\n ' Option A — pass `noydb:` directly to defineNoydbStore({...})\\n' +\n ' Option B — call setActiveNoydb(instance) once at app startup\\n' +\n ' Option C — install the @noy-db/nuxt module (Nuxt 4+)',\n )\n}\n","/**\n * `useNoydbI18n` — the reactive active-locale for a Pinia/Vue app.\n *\n * A single source of truth that drives opt-in store resolution\n * (`defineNoydbStore({ i18n: 'follow' })`) and the reactive\n * `useI18nField` / `useDictLabel` selectors.\n *\n * **State-only by default.** Every in-pinia reader passes `{ locale }`\n * explicitly on each read, so the reactive `locale` state alone drives\n * all in-pinia resolution — the vault's ambient locale is never needed.\n * `setLocale(l, { syncVault })` additionally calls `vault.setLocale` on\n * the vault(s) you pass, for apps that ALSO do imperative (non-in-pinia)\n * hub reads and want those to follow. Locale-less-vault consumers leave\n * `syncVault` off so the vault stays locale-less.\n *\n * @public\n */\nimport { defineStore } from 'pinia'\nimport { ref, watch, type Ref } from 'vue'\n\n/** Minimal vault shape needed to sync an ambient locale. */\nexport interface LocaleSyncable {\n setLocale(locale: string | undefined): void\n}\n\nexport interface SetLocaleOptions {\n /**\n * Vault(s) to ALSO update via `vault.setLocale` (opt-in). Omit to keep\n * the operation state-only — the safe default for locale-less vaults.\n */\n readonly syncVault?: LocaleSyncable | readonly LocaleSyncable[]\n}\n\nexport const useNoydbI18n = defineStore('noydb-i18n', () => {\n const locale = ref<string>('en')\n const fallback = ref<string[]>(['en', 'any'])\n\n function setLocale(l: string, opts?: SetLocaleOptions): void {\n locale.value = l\n const sync = opts?.syncVault\n if (sync) {\n const vaults = Array.isArray(sync) ? sync : [sync as LocaleSyncable]\n for (const v of vaults) v.setLocale(l)\n }\n }\n\n function setFallback(chain: string[]): void {\n fallback.value = chain\n }\n\n /**\n * One-way, state-only follow of an external locale ref (e.g. vue-i18n's\n * `locale`). Never touches a vault. Returns the watch stop handle.\n */\n function bindTo(source: Ref<string>, opts?: { immediate?: boolean }): () => void {\n return watch(\n source,\n (v) => { locale.value = v },\n // sync flush so the mirror propagates immediately (no tick lag) —\n // a locale change should be observable synchronously by dependents.\n { immediate: opts?.immediate ?? true, flush: 'sync' },\n )\n }\n\n return { locale, fallback, setLocale, setFallback, bindTo }\n})\n","/**\n * `useI18nField` — a reactive `pickLang` for a single i18nText map.\n *\n * Resolves a `{ [locale]: string }` map to the active locale, recomputing\n * when the locale (or a reactive source) changes. Uses hub's\n * `resolveI18nText` with `policy: 'null'`, so it never throws and never\n * yields `undefined` — `null` when no locale (and no fallback) resolves.\n *\n * Follows the global `useNoydbI18n` locale/fallback unless overridden.\n * Ideal for resolving one field at the edge while siblings stay raw\n * (e.g. a bilingual section reading raw maps).\n *\n * @public\n */\nimport { computed, unref, type Ref } from 'vue'\nimport { resolveI18nText } from '@noy-db/hub/i18n'\nimport { useNoydbI18n } from './useNoydbI18n.js'\n\ntype MapSource =\n | Record<string, string>\n | (() => Record<string, string> | undefined | null)\n\nexport interface UseI18nFieldOptions {\n /** Override the active locale (string or ref). Defaults to the global. */\n readonly locale?: string | Ref<string>\n /** Override the fallback chain. Defaults to the global. */\n readonly fallback?: string | readonly string[]\n}\n\nexport function useI18nField(\n source: MapSource,\n opts: UseI18nFieldOptions = {},\n): Ref<string | null> {\n const i18n = useNoydbI18n()\n return computed<string | null>(() => {\n const map = typeof source === 'function' ? source() : source\n if (!map || typeof map !== 'object') return null\n const locale = opts.locale !== undefined ? unref(opts.locale) : i18n.locale\n const fallback = opts.fallback ?? i18n.fallback\n const out = resolveI18nText(\n map,\n locale,\n fallback,\n undefined,\n { policy: 'null' },\n )\n // With a concrete locale (never 'raw'), the result is string | null.\n return typeof out === 'string' ? out : null\n })\n}\n","/**\n * `useDictLabel(name, options)` — Vue composable for rendering\n * `dictKey` fields at template time.\n *\n * The i18n boundary pattern has records store a **stable key**; the\n * label lives in a reserved `_dict_<name>` collection and is resolved\n * at render time. Every Vue / Nuxt consumer ends up writing the same\n * wrapper — this composable replaces that boilerplate.\n *\n * ```vue\n * <script setup lang=\"ts\">\n * import { useDictLabel } from '@noy-db/in-pinia'\n * const label = useDictLabel('invoiceStatus')\n * </script>\n *\n * <template>\n * <td v-for=\"inv in invoices\" :key=\"inv.id\">\n * {{ label(inv.status).value }}\n * </td>\n * </template>\n * ```\n *\n * ## Reactivity\n *\n * `label(key)` returns a `Ref<string>` that updates when the locale\n * changes (via the passed-in `locale` ref).\n *\n * **Known limitation:** mutations via `vault.dictionary(name).put()`\n * bypass the Collection emitter — the hub's `DictionaryHandle` writes\n * through the adapter directly, so labels cached by this composable\n * won't refresh for those writes until either (a) the locale changes\n * or (b) the caller re-creates the composable. Tracked as a\n * hub follow-up (route dict writes through the Collection emitter).\n *\n * The underlying fetch is async, so a newly-constructed label starts\n * at its \"missing\" sentinel (the key itself by default) and updates\n * to the resolved string within one tick.\n *\n * @module\n */\n\nimport { ref, shallowRef, toRef, watch, type Ref } from 'vue'\nimport type { Vault } from '@noy-db/hub'\nimport { resolveNoydb } from './context.js'\nimport { useNoydbI18n } from './useNoydbI18n.js'\n\nexport interface UseDictLabelOptions {\n /**\n * Explicit vault. Either a `Vault` instance or its name. When a\n * name is provided the composable calls `db.vault(name)` — which\n * requires the vault to already be open via\n * `await db.openVault(name)` elsewhere.\n *\n * When omitted, uses the currently-active vault on the global\n * Noydb instance set via `setActiveNoydb()`. If no vault is open\n * and none is provided, setup throws.\n */\n readonly vault?: Vault | string\n\n /**\n * Active locale. Pass a `Ref<string>` for live reactivity\n * (e.g. `useI18n().locale` from vue-i18n, or `useLocale()` from a\n * Nuxt module). A bare string is wrapped in a static ref.\n * Defaults to `'en'`.\n */\n readonly locale?: Ref<string> | string\n\n /**\n * Fallback locale chain — evaluated in order when the primary\n * locale has no translation. Use `'any'` as the final entry to\n * accept any available locale. Defaults to `['en', 'any']`.\n */\n readonly fallback?: string | readonly string[]\n\n /**\n * What to render when the key is absent from the dictionary.\n *\n * - `'key'` (default) — return the key itself. Matches the hub's\n * stable-key invariant and surfaces typos during development.\n * - `'empty'` — return `''`. Best for cells where a missing\n * value should render blank.\n * - `'placeholder'` — return `⟨missing:{key}⟩` for visible\n * audit during QA.\n */\n readonly onMissing?: 'key' | 'empty' | 'placeholder'\n}\n\n/**\n * Build a reactive label lookup. Returns a factory `(key) => Ref<string>`.\n */\nexport function useDictLabel(\n dictionaryName: string,\n options: UseDictLabelOptions = {},\n): (key: string) => Ref<string> {\n const vault = resolveVault(options.vault)\n const handle = vault.dictionary(dictionaryName)\n\n // When the caller omits locale/fallback, follow the shared useNoydbI18n\n // store (so a global setLocale re-resolves all labels). Guarded: if no\n // Pinia is active (e.g. standalone use), fall back to literal defaults\n // — keeps the composable usable outside an app context.\n let storeLocale: Ref<string> | null = null\n let storeFallback: readonly string[] | null = null\n if (options.locale === undefined || options.fallback === undefined) {\n try {\n const i18n = useNoydbI18n()\n storeLocale = toRef(i18n, 'locale')\n storeFallback = i18n.fallback\n } catch {\n /* no active Pinia — use literal defaults below */\n }\n }\n const localeRef =\n options.locale !== undefined\n ? normaliseLocale(options.locale)\n : (storeLocale ?? ref('en'))\n const fallback = options.fallback ?? storeFallback ?? (['en', 'any'] as const)\n const onMissingMode = options.onMissing ?? 'key'\n const missing = (key: string): string =>\n onMissingMode === 'empty'\n ? ''\n : onMissingMode === 'placeholder'\n ? `⟨missing:${key}⟩`\n : key\n\n const cache = new Map<string, Ref<string>>()\n\n // Refresh every cached label whenever the dict changes via sync /\n // local mutation. The emitter is per-Noydb; listen at the\n // collection granularity so unrelated mutations don't flush.\n const db = resolveNoydb()\n const dictCollection = `_dict_${dictionaryName}`\n const onChange = (event: { collection: string }): void => {\n if (event.collection !== dictCollection) return\n for (const [key, r] of cache) {\n void refresh(key, r)\n }\n }\n db.on('change', onChange)\n\n // Refresh every cached label when the locale flips.\n watch(localeRef, () => {\n for (const [key, r] of cache) {\n void refresh(key, r)\n }\n })\n\n async function refresh(key: string, r: Ref<string>): Promise<void> {\n try {\n const resolved = await handle.resolveLabel(\n key,\n localeRef.value,\n fallback as readonly string[],\n )\n r.value = resolved ?? missing(key)\n } catch {\n r.value = missing(key)\n }\n }\n\n return (key: string): Ref<string> => {\n let r = cache.get(key)\n if (r) return r\n r = shallowRef(missing(key))\n cache.set(key, r)\n void refresh(key, r)\n return r\n }\n}\n\nfunction resolveVault(source: Vault | string | undefined): Vault {\n const db = resolveNoydb()\n if (source && typeof source !== 'string') return source\n if (typeof source === 'string') {\n return (db as unknown as { vault(name: string): Vault }).vault(source)\n }\n // No vault specified — try the first one the instance has open.\n const anyDb = db as unknown as { vaultCache?: Map<string, Vault> }\n if (anyDb.vaultCache && anyDb.vaultCache.size > 0) {\n return [...anyDb.vaultCache.values()][0]!\n }\n throw new Error(\n '[@noy-db/in-pinia] useDictLabel: no open vault. Pass `{ vault: \"name\" }` or `await db.openVault(name)` first.',\n )\n}\n\nfunction normaliseLocale(locale: UseDictLabelOptions['locale']): Ref<string> {\n if (locale === undefined) return ref('en')\n if (typeof locale === 'string') return ref(locale)\n return locale\n}\n","/**\n * `createNoydbPiniaPlugin` — augmentation path for existing Pinia stores.\n *\n * Lets a developer take any existing `defineStore()` call and opt into NOYDB\n * persistence by adding a single `noydb:` option, without touching component\n * code. The plugin watches the chosen state key(s), encrypts on change, syncs\n * to a NOYDB collection, and rehydrates on store init.\n *\n * @example\n * ```ts\n * import { createPinia } from 'pinia';\n * import { createNoydbPiniaPlugin } from '@noy-db/in-pinia';\n * import { jsonFile } from '@noy-db/to-file';\n *\n * const pinia = createPinia();\n * pinia.use(createNoydbPiniaPlugin({\n * adapter: jsonFile({ dir: './data' }),\n * user: 'owner-01',\n * secret: () => promptPassphrase(),\n * }));\n *\n * // existing store — add one option, no component changes:\n * export const useClients = defineStore('clients', {\n * state: () => ({ list: [] as Client[] }),\n * noydb: { vault: 'C101', collection: 'clients', persist: 'list' },\n * });\n * ```\n *\n * Design notes\n * ------------\n * - Each augmented store persists a SINGLE document at id `__state__`\n * containing the picked keys. We don't try to map state arrays onto\n * per-element records — that's `defineNoydbStore`'s territory.\n * - The Noydb instance is constructed lazily on first store-with-noydb\n * instantiation, then memoized for the lifetime of the Pinia app.\n * This means apps that don't actually use any noydb-augmented stores\n * pay zero crypto cost.\n * - `secret` is a function so the passphrase can come from a prompt,\n * biometric unlock, or session token — never stored in config.\n * - The plugin sets `store.$noydbReady` (a `Promise<void>`) and\n * `store.$noydbError` (an `Error | null`) on every augmented store\n * so components can await hydration and surface failures.\n */\n\nimport type { PiniaPluginContext, PiniaPlugin, StateTree } from 'pinia'\nimport { createNoydb, type Noydb, type NoydbOptions, type NoydbStore, type Vault, type Collection } from '@noy-db/hub'\n\n/**\n * Per-store NOYDB configuration. Attached to a Pinia store via the `noydb`\n * option inside `defineStore({ ..., noydb: {...} })`.\n *\n * `persist` selects which top-level state keys to mirror into NOYDB.\n * Pass a single key, an array of keys, or `'*'` to mirror the entire state.\n */\nexport interface StoreNoydbOptions<S extends StateTree = StateTree> {\n /** Vault (tenant) name. */\n vault: string\n /** Collection name within the vault. */\n collection: string\n /**\n * Which state keys to persist. Defaults to `'*'` (the entire state object).\n * Pass a string or string[] to scope to specific keys.\n */\n persist?: keyof S | (keyof S)[] | '*'\n /**\n * Optional schema validator applied at the document level (the persisted\n * subset of state, not individual records). Throws if validation fails on\n * hydration — the store stays at its initial state and `$noydbError` is set.\n */\n schema?: { parse: (input: unknown) => unknown }\n}\n\n/**\n * Configuration for `createNoydbPiniaPlugin`. Mirrors `NoydbOptions` but\n * makes `secret` a function so the passphrase can come from a prompt\n * rather than being stored in config.\n */\nexport interface NoydbPiniaPluginOptions {\n /** The NOYDB store to use for persistence. */\n adapter: NoydbStore\n /** User identifier (matches the keyring file). */\n user: string\n /**\n * Passphrase provider. Called once on first noydb-augmented store\n * instantiation. Return a string or a Promise that resolves to one.\n */\n secret: () => string | Promise<string>\n /** Optional Noydb open-options forwarded to `createNoydb`. */\n noydbOptions?: Partial<Omit<NoydbOptions, 'store' | 'user' | 'secret'>>\n}\n\n// The fixed document id under which a store's persisted state lives. Using a\n// reserved prefix so it can't collide with any user-chosen record id.\nconst STATE_DOC_ID = '__state__'\n\n/**\n * Create a Pinia plugin that wires NOYDB persistence into any store\n * declaring a `noydb:` option.\n *\n * Returns a `PiniaPlugin` directly usable with `pinia.use(...)`.\n */\nexport function createNoydbPiniaPlugin(opts: NoydbPiniaPluginOptions): PiniaPlugin {\n // Single Noydb instance shared across all augmented stores in this Pinia\n // app. Created lazily on first use so apps that never instantiate a\n // noydb-augmented store pay zero crypto cost.\n let dbPromise: Promise<Noydb> | null = null\n function getDb(): Promise<Noydb> {\n if (!dbPromise) {\n dbPromise = (async (): Promise<Noydb> => {\n const secret = await opts.secret()\n return createNoydb({\n store: opts.adapter,\n user: opts.user,\n secret,\n ...opts.noydbOptions,\n })\n })()\n }\n return dbPromise\n }\n\n // Vault cache so opening a vault is a one-time cost per app.\n const vaultCache = new Map<string, Promise<Vault>>()\n function getCompartment(name: string): Promise<Vault> {\n let p = vaultCache.get(name)\n if (!p) {\n p = getDb().then((db) => db.openVault(name))\n vaultCache.set(name, p)\n }\n return p\n }\n\n return (context: PiniaPluginContext) => {\n // Pinia stores can declare arbitrary options on `defineStore`, but the\n // plugin context only exposes them via `context.options`. Pull our\n // `noydb` option out and bail early if it's not present — that's\n // the \"store is untouched\" path for non-augmented stores.\n const noydbOption = (context.options as { noydb?: StoreNoydbOptions }).noydb\n if (!noydbOption) {\n // Mark the store as opted-out so devtools / consumers can detect it.\n context.store.$noydbAugmented = false\n return\n }\n\n context.store.$noydbAugmented = true\n context.store.$noydbError = null as Error | null\n\n // Track in-flight persistence promises so tests (and consumers) can\n // await deterministic flushes via `$noydbFlush()`. Plain Set-of-Promises\n // — entries auto-remove on settle.\n const pending = new Set<Promise<void>>()\n\n // Hydrate-then-subscribe. Both happen inside an async closure so the\n // store can be awaited via `$noydbReady`.\n const ready = (async (): Promise<void> => {\n try {\n const vault = await getCompartment(noydbOption.vault)\n const collection: Collection<StateTree> = vault.collection<StateTree>(\n noydbOption.collection,\n )\n\n // 1. Hydration: read the persisted document (if any) and apply\n // the picked keys onto the store's current state. We use\n // `$patch` so reactivity fires correctly.\n const persisted = await collection.get(STATE_DOC_ID)\n if (persisted) {\n const validated = noydbOption.schema\n ? (noydbOption.schema.parse(persisted) as StateTree)\n : persisted\n const picked = pickKeys(validated, noydbOption.persist)\n context.store.$patch(picked)\n }\n\n // 2. Subscribe: every state mutation triggers an encrypted write\n // of the picked subset back to NOYDB. The subscription captures\n // `collection` so it doesn't re-resolve on every event.\n context.store.$subscribe(\n (_mutation, state) => {\n const subset = pickKeys(state, noydbOption.persist)\n const p = collection.put(STATE_DOC_ID, subset)\n .catch((err: unknown) => {\n context.store.$noydbError = err instanceof Error ? err : new Error(String(err))\n })\n .finally(() => {\n pending.delete(p)\n })\n pending.add(p)\n },\n { detached: true }, // outlive the component that triggered the mutation\n )\n } catch (err) {\n context.store.$noydbError = err instanceof Error ? err : new Error(String(err))\n }\n })()\n\n context.store.$noydbReady = ready\n /**\n * Wait for all in-flight persistence puts to settle. Use this in tests\n * to deterministically observe the encrypted state on the adapter, and\n * in app code before unmounting components that mutated the store.\n */\n context.store.$noydbFlush = async (): Promise<void> => {\n await ready\n // Snapshot the current pending set; new puts added during await\n // are picked up by the next $noydbFlush() call.\n while (pending.size > 0) {\n await Promise.all([...pending])\n }\n }\n }\n}\n\n/**\n * Pick the configured subset of keys from a state object.\n *\n * Behaviors:\n * - `undefined` or `'*'` → returns the entire state shallow-copied\n * - single key string → returns `{ [key]: state[key] }`\n * - key array → returns `{ [k1]: state[k1], [k2]: state[k2], ... }`\n *\n * The result is always a fresh object so callers can mutate it without\n * touching the store's reactive state.\n */\nfunction pickKeys(state: StateTree, persist: StoreNoydbOptions['persist']): StateTree {\n if (persist === undefined || persist === '*') {\n return { ...state }\n }\n if (typeof persist === 'string') {\n return { [persist]: state[persist] } as StateTree\n }\n if (Array.isArray(persist)) {\n const out: StateTree = {}\n for (const key of persist) {\n out[key as string] = state[key as string]\n }\n return out\n }\n // Should be unreachable thanks to the type, but defensive default.\n return { ...state }\n}\n\n// ─── Pinia module augmentation ─────────────────────────────────────\n//\n// Pinia exposes `DefineStoreOptionsBase` as the place where third-party\n// plugins are expected to attach their custom option types. Augmenting it\n// here means `defineStore('x', { ..., noydb: {...} })` autocompletes inside\n// the IDE and type-checks correctly without forcing users to import\n// anything from `@noy-db/pinia`.\n//\n// We also augment `PiniaCustomProperties` so the runtime fields we add to\n// every store (`$noydbReady`, `$noydbError`, `$noydbAugmented`) are typed.\n\ndeclare module 'pinia' {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n export interface DefineStoreOptionsBase<S extends StateTree, Store> {\n /**\n * Opt this store into NOYDB persistence via the\n * `createNoydbPiniaPlugin` augmentation plugin.\n *\n * The chosen state keys are encrypted and persisted to the configured\n * vault + collection on every mutation, and rehydrated on first\n * store access.\n */\n noydb?: StoreNoydbOptions<S>\n }\n\n export interface PiniaCustomProperties {\n /**\n * Resolves once this store has finished its initial hydration from\n * NOYDB. `undefined` for stores that don't declare a `noydb:` option.\n */\n $noydbReady?: Promise<void>\n /**\n * Set when hydration or persistence fails. `null` while healthy.\n * Plugins (and devtools) can poll this to surface storage errors.\n */\n $noydbError?: Error | null\n /**\n * `true` if this store opted into NOYDB persistence via the `noydb:`\n * option, `false` otherwise. Useful for debugging and devtools.\n */\n $noydbAugmented?: boolean\n /**\n * Wait for all in-flight encrypted persistence puts to complete.\n * Useful in tests for deterministic flushing, and in app code before\n * unmounting components that just mutated the store.\n */\n $noydbFlush?: () => Promise<void>\n }\n}\n","/**\n * `useCapabilityGrant` — Vue/Pinia composable for time-boxed\n * capability approval flows.\n *\n * The composable orchestrates a request → approve → expire / release\n * lifecycle for a session-scoped capability. It manages UI state,\n * persists the request to a reserved `_capability_requests` collection\n * so a separate approver session can see it, and tracks TTL with\n * auto-revert.\n *\n * **It does NOT itself flip capability bits.** Capability mechanisms\n * vary across adopters: tier-based deployments wire `onGrant` to\n * `vault.elevate(tier, opts)`; keyring-based deployments wire it to\n * `db.grant(...)`; custom deployments do their own thing. Keeping the\n * actual flip behind a callback avoids introducing a parallel\n * \"capability elevation\" primitive in hub when the existing\n * `vault.elevate()` already covers the time-boxed-grant pattern.\n *\n * ## State machine\n *\n * idle ─── .request() ───────► requested\n * requested ─── .approve() ──► granted\n * granted ─── TTL expires ─► idle\n * granted ─── .release() ──► idle\n *\n * @module\n */\n\nimport {\n computed,\n getCurrentScope,\n onScopeDispose,\n ref,\n shallowRef,\n watch,\n type ComputedRef,\n type Ref,\n} from 'vue'\nimport type { Vault, Role, CollectionChangeEvent } from '@noy-db/hub'\nimport { resolveNoydb } from './context.js'\n\n/** Reserved internal collection that holds capability-grant lifecycle records. */\nexport const CAPABILITY_REQUESTS_COLLECTION = '_capability_requests'\n\nexport type CapabilityGrantState = 'idle' | 'requested' | 'granted' | 'expired'\n\n/**\n * On-disk shape of a capability-grant lifecycle record. Persisted in\n * the reserved {@link CAPABILITY_REQUESTS_COLLECTION}. Encrypted with\n * that collection's DEK at the storage layer; the in-memory shape\n * here is plaintext.\n *\n * The audit trail invariant: this record carries metadata only —\n * capability name, roles, ttl, reason. Never plaintext payload.\n */\nexport interface CapabilityGrantRecord {\n readonly id: string\n readonly capability: string\n readonly requestedBy: string\n readonly approverRole: Role\n readonly reason: string\n readonly ttlMs: number\n readonly status: 'requested' | 'granted' | 'released' | 'expired'\n readonly requestedAt: string\n readonly approvedBy?: string\n readonly approvedAt?: string\n readonly expiresAt?: string\n}\n\nexport interface UseCapabilityGrantOptions {\n /** TTL in milliseconds for the granted window. */\n readonly ttlMs: number\n /** Role required to call `.approve()`. Mismatch throws on `.approve()`. */\n readonly approver: Role\n /** Audit-ledger string. Stamped on the request record; no plaintext payload. */\n readonly reason: string\n /**\n * Optional explicit vault. Either a `Vault` instance or its name.\n * When omitted, resolves the active Noydb instance via\n * `setActiveNoydb()` and opens the first vault the caller has\n * already loaded.\n */\n readonly vault: Vault | string\n /**\n * Called on the approver's session when `.approve()` succeeds. Wire\n * this to whatever capability flip your codebase uses —\n * `vault.elevate(tier, opts)` for tier-based deployments,\n * `db.grant(...)` for keyring-based, custom for custom.\n *\n * The composable does NOT enforce that the capability was actually\n * granted — it just tracks the lifecycle. The post-expiry \"gated\n * call throws\" contract comes from the underlying mechanism the\n * callback wires up (e.g., `ElevationExpiredError` from\n * `vault.elevate`'s lazy TTL check).\n */\n readonly onGrant?: (ctx: {\n record: CapabilityGrantRecord\n vault: Vault\n }) => void | Promise<void>\n /**\n * Called when the grant ends (TTL expiry OR voluntary release).\n * Mirror of `onGrant`. Idempotent — may be called twice if release\n * and expiry race; callers should no-op on the second invocation.\n */\n readonly onRelease?: (ctx: {\n record: CapabilityGrantRecord\n vault: Vault\n cause: 'released' | 'expired'\n }) => void | Promise<void>\n}\n\nexport interface UseCapabilityGrantReturn {\n readonly state: Ref<CapabilityGrantState>\n /** Milliseconds remaining on the granted window; 0 outside `granted`. */\n readonly timeRemaining: ComputedRef<number>\n /** Most recent error from request/approve/release (resets on next op). */\n readonly error: Ref<Error | null>\n /** Issue a request. State must be `idle`. */\n request(): Promise<void>\n /** Approve a pending request. State must be `requested`. */\n approve(): Promise<void>\n /** Voluntarily revoke an active grant. State must be `granted`. */\n release(): Promise<void>\n}\n\n/**\n * Build a reactive capability-grant lifecycle handle.\n *\n * @example Tier-based capability flip\n * ```ts\n * let elevated: ElevatedHandle | null = null\n * const grant = useCapabilityGrant('canExportPlaintext', {\n * vault: 'V1',\n * ttlMs: 15 * 60_000,\n * approver: 'admin',\n * reason: 'bulk export',\n * onGrant: async ({ vault, record }) => {\n * elevated = await vault.elevate(2, {\n * ttlMs: record.ttlMs,\n * reason: record.reason,\n * })\n * },\n * onRelease: async () => { await elevated?.release(); elevated = null },\n * })\n * ```\n */\nexport function useCapabilityGrant(\n capability: string,\n options: UseCapabilityGrantOptions,\n): UseCapabilityGrantReturn {\n const state = ref<CapabilityGrantState>('idle')\n const error = ref<Error | null>(null)\n const recordRef = shallowRef<CapabilityGrantRecord | null>(null)\n\n // SSR / non-browser host: composable is a no-op. Methods reject; the\n // refs stay at their initial values so server-rendered output shows\n // the idle state.\n const inBrowser = typeof window !== 'undefined'\n\n let expiryTimer: ReturnType<typeof setTimeout> | null = null\n let unsubscribeChangeStream: (() => void) | null = null\n let resolvedVault: Vault | null = null\n let stopped = false\n\n async function resolveVault(): Promise<Vault> {\n if (resolvedVault) return resolvedVault\n if (typeof options.vault === 'string') {\n const noydb = resolveNoydb(null)\n resolvedVault = await noydb.openVault(options.vault)\n } else {\n resolvedVault = options.vault\n }\n // Open the requests collection eagerly so the change stream\n // subscription below has a target. We don't typed-cast here —\n // the collection holds CapabilityGrantRecord shapes only.\n resolvedVault.collection<CapabilityGrantRecord>(\n CAPABILITY_REQUESTS_COLLECTION,\n )\n return resolvedVault\n }\n\n function clearExpiryTimer(): void {\n if (expiryTimer) {\n clearTimeout(expiryTimer)\n expiryTimer = null\n }\n }\n\n function scheduleExpiry(record: CapabilityGrantRecord): void {\n if (!record.expiresAt) return\n const remaining = new Date(record.expiresAt).getTime() - Date.now()\n if (remaining <= 0) {\n void handleExpiry(record)\n return\n }\n clearExpiryTimer()\n expiryTimer = setTimeout(() => { void handleExpiry(record) }, remaining)\n }\n\n async function handleExpiry(record: CapabilityGrantRecord): Promise<void> {\n if (stopped) return\n if (state.value !== 'granted') return\n state.value = 'expired'\n try {\n await options.onRelease?.({\n record,\n vault: resolvedVault!,\n cause: 'expired',\n })\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err))\n }\n // Auto-return to idle after the expiry handler — matches the spec's\n // \"state auto-returns to idle\" contract.\n state.value = 'idle'\n recordRef.value = null\n }\n\n // `now` ticks every second so timeRemaining stays reactive without\n // wiring a per-frame loop.\n const now = ref(Date.now())\n const tickTimer = inBrowser\n ? setInterval(() => { now.value = Date.now() }, 1000)\n : null\n\n const timeRemaining = computed(() => {\n if (state.value !== 'granted' || !recordRef.value?.expiresAt) return 0\n // Subscribe to `now` for reactivity, but read the live clock for\n // accuracy — `now` ticks every second so the computed stays\n // honest between ticks too.\n void now.value\n const ms = new Date(recordRef.value.expiresAt).getTime() - Date.now()\n return ms > 0 ? ms : 0\n })\n\n // Subscribe to the requests collection so an approver session sees\n // pending records appear in real time within the same Noydb session.\n // Cross-session visibility additionally requires the sync engine.\n watch(\n () => recordRef.value?.id,\n async (id) => {\n if (!inBrowser || !id || unsubscribeChangeStream) return\n const vault = await resolveVault()\n const coll = vault.collection<CapabilityGrantRecord>(\n CAPABILITY_REQUESTS_COLLECTION,\n )\n unsubscribeChangeStream = coll.subscribe(\n (evt: CollectionChangeEvent<CapabilityGrantRecord>) => {\n if (evt.type !== 'put' || evt.id !== id) return\n const updated = evt.record\n if (!updated || stopped) return\n recordRef.value = updated\n if (updated.status === 'granted') {\n state.value = 'granted'\n scheduleExpiry(updated)\n } else if (updated.status === 'released' || updated.status === 'expired') {\n state.value = 'idle'\n clearExpiryTimer()\n }\n },\n )\n },\n { immediate: false },\n )\n\n async function request(): Promise<void> {\n if (state.value !== 'idle') {\n error.value = new Error(\n `useCapabilityGrant: cannot request from state \"${state.value}\"`,\n )\n throw error.value\n }\n error.value = null\n if (!inBrowser) return\n try {\n const vault = await resolveVault()\n const coll = vault.collection<CapabilityGrantRecord>(\n CAPABILITY_REQUESTS_COLLECTION,\n )\n const id = `cap-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 10)}`\n const record: CapabilityGrantRecord = {\n id,\n capability,\n requestedBy: vault.userId,\n approverRole: options.approver,\n reason: options.reason,\n ttlMs: options.ttlMs,\n status: 'requested',\n requestedAt: new Date().toISOString(),\n }\n await coll.put(id, record)\n recordRef.value = record\n state.value = 'requested'\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err))\n throw error.value\n }\n }\n\n async function approve(): Promise<void> {\n const record = recordRef.value\n if (state.value !== 'requested' || !record) {\n error.value = new Error(\n `useCapabilityGrant: cannot approve from state \"${state.value}\"`,\n )\n throw error.value\n }\n error.value = null\n try {\n const vault = await resolveVault()\n if (vault.role !== options.approver && vault.role !== 'owner') {\n throw new Error(\n `useCapabilityGrant: caller role \"${vault.role}\" cannot approve a \"${options.approver}\"-tier grant`,\n )\n }\n const approvedAt = new Date()\n const expiresAt = new Date(approvedAt.getTime() + options.ttlMs)\n const granted: CapabilityGrantRecord = {\n ...record,\n status: 'granted',\n approvedBy: vault.userId,\n approvedAt: approvedAt.toISOString(),\n expiresAt: expiresAt.toISOString(),\n }\n const coll = vault.collection<CapabilityGrantRecord>(\n CAPABILITY_REQUESTS_COLLECTION,\n )\n await coll.put(record.id, granted)\n recordRef.value = granted\n state.value = 'granted'\n scheduleExpiry(granted)\n await options.onGrant?.({ record: granted, vault })\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err))\n throw error.value\n }\n }\n\n async function release(): Promise<void> {\n const record = recordRef.value\n if (state.value !== 'granted' || !record) {\n // Releasing from non-granted state is a no-op.\n return\n }\n error.value = null\n try {\n clearExpiryTimer()\n const vault = await resolveVault()\n const released: CapabilityGrantRecord = { ...record, status: 'released' }\n const coll = vault.collection<CapabilityGrantRecord>(\n CAPABILITY_REQUESTS_COLLECTION,\n )\n await coll.put(record.id, released)\n recordRef.value = null\n state.value = 'idle'\n await options.onRelease?.({ record, vault, cause: 'released' })\n } catch (err) {\n error.value = err instanceof Error ? err : new Error(String(err))\n throw error.value\n }\n }\n\n if (getCurrentScope()) {\n onScopeDispose(() => {\n stopped = true\n clearExpiryTimer()\n if (tickTimer) clearInterval(tickTimer)\n unsubscribeChangeStream?.()\n })\n }\n\n return { state, timeRemaining, error, request, approve, release }\n}\n"],"mappings":"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","names":["import_pinia","import_vue","id","items","import_vue","import_vue","import_vue","resolveVault"]}
|