mnfst 0.5.248 → 0.5.250

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.248';
5
+ const MANIFEST_BUILD_VERSION = '0.5.250';
6
6
 
7
7
  /* Manifest Data Sources - Appwrite Integration */
8
8
 
@@ -683,17 +683,40 @@ window.ManifestDataAppwrite = {
683
683
 
684
684
 
685
685
  /* Manifest Data Sources - Real-time Subscriptions
686
- One shared Realtime client per tab; channels ref-counted across sources. */
687
-
688
- // Map<channel, { refs, unsubscribe, handlers: Map<sourceName, handler> }>
689
- const channels = new Map();
690
- // Map<sourceName, { channel, token }> — token guards a stale unsubscribe closure
686
+ One shared Realtime client per tab; subscriptions ref-counted by
687
+ (channel + canonical server-filter set) across sources.
688
+
689
+ Appwrite Realtime queries (server-side event filtering), verified against
690
+ the Web SDK and server source 2026-10:
691
+ - Realtime#subscribe(channels, callback, queries?) — queries are per
692
+ subscription (Query strings, ANDed; OR across subscriptions), supported
693
+ ops: equal/notEqual/lessThan(Equal)/greaterThan(Equal)/isNull/isNotNull/
694
+ and/or. SDK >=25.0.0 sends {type:'subscribe', data:[{subscriptionId,
695
+ channels, queries}]} socket messages and dispatches by the matched
696
+ subscription ids the server echoes; 22.4-24 carried per-slot queries in
697
+ the socket URL. subscribe() resolves to {unsubscribe, update, close}.
698
+ - Feature detection: a query-capable instance has pendingSubscribes (>=25)
699
+ or slotToSubscriptionId (22.4-24). Without either marker: channel-only
700
+ subscribe, exactly the 0.5.248 path.
701
+ - The server evaluates queries against the event payload only, so an
702
+ update that makes a held row STOP matching is not delivered. Therefore
703
+ serverFilter defaults to 'scope': only scope-column filters (permission-
704
+ mirrored, effectively immutable) ride the subscription; user queries stay
705
+ client-side so stop-matching rows still drop live. 'all' pushes every
706
+ expressible filter server-side (such rows then correct on the next load);
707
+ false sends none. The client matcher stays on in every mode. */
708
+
709
+ // Map<subKey, { channel, queries, refs, unsubscribe, handlers: Map<sourceName, handler> }>
710
+ const entries = new Map();
711
+ // Map<sourceName, { subKey, channel, token, handler, getFilters }> — token guards a stale unsubscribe closure
691
712
  const sources = new Map();
692
713
  // Map<sourceName, fn(eventType, row)> — pre-filters, run per event per source
693
714
  const filters = new Map();
694
715
  // Back-compat/debug surface: Map<sourceName, unsubscribeFunction>
695
716
  const subscriptions = new Map();
696
- // Instance the current channel subscriptions live on
717
+ // Map<sourceName, n> — explicit teardowns; aborts a subscribe in flight
718
+ const tombstones = new Map();
719
+ // Instance the current subscriptions live on
697
720
  let activeRealtime = null;
698
721
 
699
722
  function classify(event) {
@@ -704,10 +727,22 @@ function classify(event) {
704
727
  return null;
705
728
  }
706
729
 
730
+ // Query-capable Realtime service (see header): detect per instance, never assume
731
+ function supportsQueries(realtime) {
732
+ return !!(realtime && (realtime.pendingSubscribes || realtime.slotToSubscriptionId));
733
+ }
734
+
735
+ // (channel, resolved filter set) → stable subscription key; resolved.key is
736
+ // already canonical (sorted) so equivalent filter sets share one subscription
737
+ function subKeyFor(channel, resolved) {
738
+ if (!resolved || !resolved.key) return channel;
739
+ return `${channel}\u001f${resolved.key}`;
740
+ }
741
+
707
742
  // Fan one SDK message out: each registered source handler gets each event once,
708
743
  // after that source's pre-filter (setFilter) — before any merge/dedup work
709
- function dispatch(channel, response) {
710
- const entry = channels.get(channel);
744
+ function dispatch(subKey, response) {
745
+ const entry = entries.get(subKey);
711
746
  if (!entry || !response || !response.events) return;
712
747
  const events = Array.isArray(response.events) ? response.events : [response.events];
713
748
  let payload = response.payload;
@@ -723,97 +758,183 @@ function dispatch(channel, response) {
723
758
  }
724
759
  }
725
760
 
726
- function sdkSubscribe(realtime, channel) {
727
- return realtime.subscribe(channel, response => dispatch(channel, response));
761
+ // subscribe() returns an unsubscribe function (legacy client) or a
762
+ // Promise<RealtimeSubscription> (Realtime service): normalize teardown
763
+ function dropSub(sub) {
764
+ if (typeof sub === 'function') sub();
765
+ else if (sub && typeof sub.unsubscribe === 'function') sub.unsubscribe(); // per-subscription; never risks the shared socket
766
+ else if (sub && typeof sub.close === 'function') sub.close();
728
767
  }
729
768
 
730
- // Drop a source's registration; tear the channel down when the last source leaves
731
- function detach(name, token) {
732
- const cur = sources.get(name);
733
- if (!cur || (token && cur.token !== token)) return;
734
- sources.delete(name);
735
- const entry = channels.get(cur.channel);
769
+ function makeUnsubscriber(result) {
770
+ if (!result || typeof result.then !== 'function') {
771
+ return () => { try { dropSub(result); } catch (e) { /* socket already gone */ } };
772
+ }
773
+ const settled = result.catch(() => null);
774
+ return () => { settled.then(dropSub).catch(() => { /* socket already gone */ }); };
775
+ }
776
+
777
+ function sdkSubscribe(realtime, subKey, channel, queries) {
778
+ const cb = response => dispatch(subKey, response);
779
+ const result = (queries && queries.length)
780
+ ? realtime.subscribe(channel, cb, queries)
781
+ : realtime.subscribe(channel, cb);
782
+ return makeUnsubscriber(result);
783
+ }
784
+
785
+ // Get or create the shared entry for (channel, filters); registered in the map
786
+ // before the SDK call so an event arriving mid-subscribe already dispatches
787
+ function ensureEntry(realtime, subKey, channel, queries) {
788
+ let entry = entries.get(subKey);
789
+ if (entry) return entry;
790
+ entry = { channel, queries: queries || [], refs: 0, unsubscribe: null, handlers: new Map() };
791
+ entries.set(subKey, entry);
792
+ try {
793
+ entry.unsubscribe = sdkSubscribe(realtime, subKey, channel, queries);
794
+ } catch (error) {
795
+ entries.delete(subKey);
796
+ throw error;
797
+ }
798
+ return entry;
799
+ }
800
+
801
+ function removeFromEntry(name, subKey) {
802
+ const entry = entries.get(subKey);
736
803
  if (!entry) return;
737
804
  entry.handlers.delete(name);
738
805
  entry.refs = entry.handlers.size;
739
806
  if (entry.refs === 0) {
740
- channels.delete(cur.channel);
807
+ entries.delete(subKey);
741
808
  if (typeof entry.unsubscribe === 'function') {
742
809
  try { entry.unsubscribe(); } catch (e) { /* socket already gone */ }
743
810
  }
744
811
  }
745
812
  }
746
813
 
747
- // Instance changed (client rebuilt on identity change): re-bind live channels
814
+ // Drop a source's registration; tear the subscription down when the last source leaves
815
+ function detach(name, token) {
816
+ const cur = sources.get(name);
817
+ if (!cur || (token && cur.token !== token)) return;
818
+ sources.delete(name);
819
+ removeFromEntry(name, cur.subKey);
820
+ }
821
+
822
+ // Instance changed (client rebuilt on identity change): re-resolve each source's
823
+ // filters ($auth. values are stale) and regroup subscriptions on the new instance.
824
+ // Chained so overlapping rebuilds run in order; a superseded rebuild stops.
825
+ let syncChain = Promise.resolve();
748
826
  function syncInstance(realtime) {
749
- if (activeRealtime === realtime) return;
827
+ if (activeRealtime === realtime) return syncChain;
750
828
  activeRealtime = realtime;
751
- for (const [channel, entry] of channels) {
829
+ syncChain = syncChain
830
+ .then(() => rebindAll(realtime))
831
+ .catch(error => console.error('[Manifest Data] Realtime rebind failed:', error));
832
+ return syncChain;
833
+ }
834
+
835
+ async function rebindAll(realtime) {
836
+ for (const [, entry] of entries) {
752
837
  if (typeof entry.unsubscribe === 'function') {
753
838
  try { entry.unsubscribe(); } catch (e) { /* old instance */ }
754
839
  }
840
+ }
841
+ entries.clear();
842
+ const canQuery = supportsQueries(realtime);
843
+ for (const [name, src] of sources) {
844
+ if (activeRealtime !== realtime) return; // superseded by a newer instance
845
+ const resolved = (canQuery && src.getFilters) ? await src.getFilters() : null;
846
+ const subKey = subKeyFor(src.channel, resolved);
755
847
  try {
756
- entry.unsubscribe = sdkSubscribe(realtime, channel);
848
+ const entry = ensureEntry(realtime, subKey, src.channel, resolved?.queries);
849
+ entry.handlers.set(name, src.handler);
850
+ entry.refs = entry.handlers.size;
851
+ src.subKey = subKey;
757
852
  } catch (error) {
758
- console.error('[Manifest Data] Failed to re-subscribe channel:', channel, error);
853
+ console.error('[Manifest Data] Failed to re-subscribe channel:', src.channel, error);
759
854
  }
760
855
  }
761
856
  }
762
857
 
763
- // One SDK subscribe per distinct channel, however many sources share it.
764
- // isCurrent: false once the requesting load is superseded (logout) — skip, never touch a newer entry
765
- async function subscribeChannel(dataSourceName, channel, onEvent, isCurrent) {
766
- detach(dataSourceName);
767
- subscriptions.delete(dataSourceName);
768
-
858
+ // One SDK subscribe per distinct (channel, filter set); sources sharing both share it.
859
+ // isCurrent: false once the requesting load is superseded (logout) — skip, never touch a newer entry.
860
+ // getFilters: async () => ({ queries, key }) | null, re-run per (re)subscribe so
861
+ // $auth. values resolve at subscribe time; null → channel-only (0.5.248 path).
862
+ // A replaced registration detaches only after the new subscription is live (no
863
+ // event gap); overlap double-delivery is absorbed by the per-source event dedupe.
864
+ async function subscribeChannel(dataSourceName, channel, onEvent, isCurrent, getFilters) {
865
+ const epoch = tombstones.get(dataSourceName) || 0;
769
866
  const services = await window.ManifestDataAppwrite.getAppwriteDataServices();
770
867
  if (isCurrent && !isCurrent()) return null;
771
868
  if (!services?.realtime) {
772
869
  console.warn('[Manifest Data] Realtime service not available for', dataSourceName);
870
+ detach(dataSourceName);
871
+ subscriptions.delete(dataSourceName);
773
872
  return null;
774
873
  }
775
- syncInstance(services.realtime);
874
+ const realtime = services.realtime;
875
+ await syncInstance(realtime);
876
+ if (isCurrent && !isCurrent()) return null;
776
877
 
777
- let entry = channels.get(channel);
778
- if (!entry) {
779
- entry = { refs: 0, unsubscribe: null, handlers: new Map() };
780
- try {
781
- entry.unsubscribe = sdkSubscribe(services.realtime, channel);
782
- } catch (error) {
783
- console.error('[Manifest Data] Failed to subscribe:', channel, error);
784
- return null;
785
- }
786
- channels.set(channel, entry);
878
+ const resolved = (supportsQueries(realtime) && getFilters) ? await getFilters() : null;
879
+ if (isCurrent && !isCurrent()) return null;
880
+ // Torn down while resolving: land nothing
881
+ if ((tombstones.get(dataSourceName) || 0) !== epoch) return null;
882
+ // Instance replaced while resolving (identity rebuild): re-run against the live one
883
+ if (activeRealtime !== realtime) return subscribeChannel(dataSourceName, channel, onEvent, isCurrent, getFilters);
884
+ const subKey = subKeyFor(channel, resolved);
885
+
886
+ const prev = sources.get(dataSourceName);
887
+ let entry;
888
+ try {
889
+ entry = ensureEntry(realtime, subKey, channel, resolved?.queries);
890
+ } catch (error) {
891
+ console.error('[Manifest Data] Failed to subscribe:', channel, error);
892
+ detach(dataSourceName);
893
+ subscriptions.delete(dataSourceName);
894
+ return null;
787
895
  }
788
896
  const token = Symbol(dataSourceName);
789
897
  entry.handlers.set(dataSourceName, onEvent);
790
898
  entry.refs = entry.handlers.size;
791
- sources.set(dataSourceName, { channel, token });
899
+ sources.set(dataSourceName, { subKey, channel, token, handler: onEvent, getFilters });
900
+ if (prev && prev.subKey !== subKey) removeFromEntry(dataSourceName, prev.subKey);
792
901
 
793
902
  const unsubscribe = () => detach(dataSourceName, token);
794
903
  subscriptions.set(dataSourceName, unsubscribe);
795
904
  return unsubscribe;
796
905
  }
797
906
 
907
+ // Resolver re-runs per (re)subscribe so $auth. values are current at that moment
908
+ function filterResolver(filterSpec) {
909
+ if (!filterSpec) return null;
910
+ const build = window.ManifestDataQueries?.buildRealtimeQueries;
911
+ if (typeof build !== 'function') return null;
912
+ return () => build(filterSpec.queriesConfig, filterSpec.scope, filterSpec.scopeColumns, filterSpec.mode);
913
+ }
914
+
798
915
  // Subscribe to storage bucket file changes
799
- function subscribeToStorageBucket(dataSourceName, bucketId, scope, onEvent, isCurrent) {
800
- return subscribeChannel(dataSourceName, `buckets.${bucketId}.files`, onEvent, isCurrent);
916
+ function subscribeToStorageBucket(dataSourceName, bucketId, scope, onEvent, isCurrent, filterSpec) {
917
+ return subscribeChannel(dataSourceName, `buckets.${bucketId}.files`, onEvent, isCurrent, filterResolver(filterSpec));
801
918
  }
802
919
 
803
920
  // Subscribe to database table row changes
804
- function subscribeToTable(dataSourceName, databaseId, tableId, scope, onEvent, isCurrent) {
805
- return subscribeChannel(dataSourceName, `databases.${databaseId}.tables.${tableId}.rows`, onEvent, isCurrent);
921
+ function subscribeToTable(dataSourceName, databaseId, tableId, scope, onEvent, isCurrent, filterSpec) {
922
+ return subscribeChannel(dataSourceName, `databases.${databaseId}.tables.${tableId}.rows`, onEvent, isCurrent, filterResolver(filterSpec));
806
923
  }
807
924
 
808
925
  // Unsubscribe from a data source
809
926
  function unsubscribeFromDataSource(dataSourceName) {
927
+ tombstones.set(dataSourceName, (tombstones.get(dataSourceName) || 0) + 1);
810
928
  detach(dataSourceName);
811
929
  subscriptions.delete(dataSourceName);
812
930
  }
813
931
 
814
932
  // Unsubscribe from all data sources
815
933
  function unsubscribeAll() {
816
- for (const name of [...sources.keys()]) detach(name);
934
+ for (const name of [...sources.keys()]) {
935
+ tombstones.set(name, (tombstones.get(name) || 0) + 1);
936
+ detach(name);
937
+ }
817
938
  subscriptions.clear();
818
939
  }
819
940
 
@@ -825,11 +946,11 @@ function setFilter(sourceName, fn) {
825
946
  }
826
947
 
827
948
  // Identity events rebuild the shared Realtime (manifest.data.appwrite.js drops
828
- // its memo on the same events); re-bind any live channels to the new instance
949
+ // its memo on the same events); re-bind live subscriptions to the new instance
829
950
  if (typeof window !== 'undefined') {
830
951
  ['manifest:auth:login', 'manifest:auth:logout', 'manifest:auth:anonymous', 'manifest:auth:session-cleared', 'manifest:auth:initialized']
831
952
  .forEach(type => window.addEventListener(type, async () => {
832
- if (!channels.size) return;
953
+ if (!sources.size) return;
833
954
  const services = await window.ManifestDataAppwrite?.getAppwriteDataServices?.();
834
955
  if (services?.realtime) syncInstance(services.realtime);
835
956
  }));
@@ -843,7 +964,7 @@ window.ManifestDataRealtime = {
843
964
  unsubscribeAll,
844
965
  setFilter,
845
966
  subscriptions, // Expose for debugging
846
- _channels: channels // Expose for debugging
967
+ _channels: entries // Expose for debugging (keyed by channel + filter set)
847
968
  };
848
969
 
849
970
 
@@ -1334,6 +1455,42 @@ function toAppwriteQuery(queryArray) {
1334
1455
  }
1335
1456
  }
1336
1457
 
1458
+ // Realtime-expressible operators: the intersection of what the load serializes
1459
+ // (QUERY_MAP), what the matcher evaluates, and what Appwrite realtime filters
1460
+ // server-side. isNull/isNotNull are realtime-supported but the load drops them,
1461
+ // so filtering on them server-side would drop events the matcher keeps — excluded.
1462
+ const REALTIME_FILTER_OPS = new Set(['equal', 'notEqual', 'lessThan', 'lessThanEqual', 'greaterThan', 'greaterThanEqual']);
1463
+
1464
+ function isRealtimeExpressible(query) {
1465
+ if (!Array.isArray(query) || query.length === 0) return false;
1466
+ const [method, ...args] = query;
1467
+ if (method === 'or') return Array.isArray(args[0]) && args[0].length > 0 && args[0].every(isRealtimeExpressible);
1468
+ return REALTIME_FILTER_OPS.has(method);
1469
+ }
1470
+
1471
+ // Server-side realtime filter set, reduced to the expressible subset. Omitting
1472
+ // a query only WIDENS delivery — the client matcher still filters — so
1473
+ // unexpressible queries are dropped whole, never approximated.
1474
+ // mode 'scope' (default): only scope-column queries ride the subscription —
1475
+ // permission-mirrored, so a row leaving the filter still delivers its last
1476
+ // event and drops live. 'all': user queries too (a held row that stops
1477
+ // matching then corrects on the next load, not instantly).
1478
+ // Returns { queries, key } (key = canonical sorted serialization, for
1479
+ // subscription sharing) or null (not ready / nothing expressible → channel-only
1480
+ // subscribe, the 0.5.248 path).
1481
+ async function buildRealtimeQueries(queriesConfig, scope, scopeColumns, mode = 'scope') {
1482
+ const userConfig = (mode === 'all' && Array.isArray(queriesConfig)) ? queriesConfig : [];
1483
+ const resolved = await buildQueries(userConfig, scope, scopeColumns);
1484
+ if (resolved === null) return null;
1485
+ const queries = resolved
1486
+ .filter(isRealtimeExpressible)
1487
+ .map(query => toAppwriteQuery(query))
1488
+ .filter(query => query !== null);
1489
+ if (queries.length === 0) return null;
1490
+ const key = queries.map(q => typeof q === 'string' ? q : JSON.stringify(q)).sort().join('\u001e');
1491
+ return { queries, key };
1492
+ }
1493
+
1337
1494
  // Build Appwrite queries from configuration
1338
1495
  // scopeColumns: { team, user } column names (default teamId/userId) — see manifest.data.config.js getScopeColumns
1339
1496
  // Returns null ("not ready") straight through — see buildQueries.
@@ -1353,6 +1510,7 @@ window.ManifestDataQueries = {
1353
1510
  getAuthValue,
1354
1511
  buildQueries,
1355
1512
  buildAppwriteQueries,
1513
+ buildRealtimeQueries,
1356
1514
  rowMatchesQueries,
1357
1515
  toAppwriteQuery,
1358
1516
  ALLOWED_VARIABLES,
package/lib/manifest.d.ts CHANGED
@@ -342,7 +342,14 @@ export interface ManifestHaptics {
342
342
  /** `$biometric` from the device plugin. */
343
343
  export interface ManifestBiometric {
344
344
  available(): Promise<boolean>;
345
- verify(opts?: Record<string, unknown>): Promise<unknown>;
345
+ verify(opts?: {
346
+ reason?: string;
347
+ title?: string;
348
+ subtitle?: string;
349
+ cancelTitle?: string;
350
+ fallbackTitle?: string;
351
+ allowDeviceCredential?: boolean;
352
+ }): Promise<{ verified: boolean; error?: string; code?: string }>;
346
353
  }
347
354
 
348
355
  /** `$camera` from the device plugin. */
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.248';
5
+ const MANIFEST_BUILD_VERSION = '0.5.250';
6
6
 
7
7
  /* Manifest Data Sources - Configuration */
8
8
 
@@ -11897,6 +11897,13 @@ function forgetRealtimeRow(dataSourceName, id) {
11897
11897
  if (i !== -1) meta.appended.splice(i, 1);
11898
11898
  }
11899
11899
 
11900
+ // realtime.serverFilter → mode: 'scope' (default) | 'all' (alias true) | null (false: no server filtering)
11901
+ function serverFilterMode(realtimeConfig) {
11902
+ const sf = (realtimeConfig && typeof realtimeConfig === 'object') ? realtimeConfig.serverFilter : undefined;
11903
+ if (sf === false) return null;
11904
+ return (sf === 'all' || sf === true) ? 'all' : 'scope';
11905
+ }
11906
+
11900
11907
  // Query-aware event filter: the source's own queries, evaluated client-side
11901
11908
  // (unevaluable → match; see rowMatchesQueries)
11902
11909
  function eventMatchesQueries(row, queriesConfig) {
@@ -12340,6 +12347,10 @@ async function loadDataSource(dataSourceName, locale = 'en', options = {}) {
12340
12347
  const rtQueries = Array.isArray(queriesConfig?.default || queriesConfig) ? (queriesConfig.default || queriesConfig) : [];
12341
12348
  const limitQ = rtQueries.find(q => Array.isArray(q) && q[0] === 'limit');
12342
12349
  configureRealtimeMeta(dataSourceName, dataSource.realtime, Array.isArray(data) ? data.length : 0, limitQ ? limitQ[1] : null);
12350
+ // Events server-filtered where the SDK supports realtime queries; the client
12351
+ // matcher below still runs. serverFilter: 'scope' (default) | 'all' (alias true) | false
12352
+ const rtMode = serverFilterMode(dataSource.realtime);
12353
+ const rtFilterSpec = rtMode ? { queriesConfig: rtQueries, scope, scopeColumns, mode: rtMode } : null;
12343
12354
  const unsubscribe = await window.ManifestDataRealtime.subscribeToTable(
12344
12355
  dataSourceName,
12345
12356
  appwriteConfig.databaseId,
@@ -12348,7 +12359,8 @@ async function loadDataSource(dataSourceName, locale = 'en', options = {}) {
12348
12359
  async (eventType, payload) => {
12349
12360
  await handleTableRealtimeEvent(dataSourceName, appwriteConfig.databaseId, tableId, scope, scopeColumns, eventType, payload, generation, rtQueries);
12350
12361
  },
12351
- () => !superseded()
12362
+ () => !superseded(),
12363
+ rtFilterSpec
12352
12364
  );
12353
12365
  if (superseded()) { dropSubscription(dataSourceName, unsubscribe); landed = true; return null; }
12354
12366
  }
@@ -12393,6 +12405,9 @@ async function loadDataSource(dataSourceName, locale = 'en', options = {}) {
12393
12405
  const rtQueries = Array.isArray(queriesConfig?.default || queriesConfig) ? (queriesConfig.default || queriesConfig) : [];
12394
12406
  const limitQ = rtQueries.find(q => Array.isArray(q) && q[0] === 'limit');
12395
12407
  configureRealtimeMeta(dataSourceName, dataSource.realtime, Array.isArray(data) ? data.length : 0, limitQ ? limitQ[1] : null);
12408
+ // Buckets have no scope columns: server filtering only under serverFilter 'all'
12409
+ const rtMode = serverFilterMode(dataSource.realtime);
12410
+ const rtFilterSpec = rtMode ? { queriesConfig: rtQueries, scope: null, mode: rtMode } : null;
12396
12411
  const unsubscribe = await window.ManifestDataRealtime.subscribeToStorageBucket(
12397
12412
  dataSourceName,
12398
12413
  bucketId,
@@ -12401,7 +12416,8 @@ async function loadDataSource(dataSourceName, locale = 'en', options = {}) {
12401
12416
  if (superseded()) return;
12402
12417
  await handleStorageRealtimeEvent(dataSourceName, bucketId, scope, eventType, payload, generation, rtQueries);
12403
12418
  },
12404
- () => !superseded()
12419
+ () => !superseded(),
12420
+ rtFilterSpec
12405
12421
  );
12406
12422
  if (superseded()) { dropSubscription(dataSourceName, unsubscribe); landed = true; return null; }
12407
12423
  }
@@ -15,6 +15,14 @@ function manifestNativePlugin(name) {
15
15
  const cap = window.Capacitor;
16
16
  return (cap && cap.Plugins && cap.Plugins[name]) || null;
17
17
  }
18
+ // Whether a plugin exposes a native method. Bundled apps get a registerPlugin
19
+ // Proxy where every property is a function, so prefer the native header.
20
+ function manifestNativeHas(name, plugin, method) {
21
+ const headers = window.Capacitor && window.Capacitor.PluginHeaders;
22
+ const h = Array.isArray(headers) && headers.find(x => x && x.name === name);
23
+ if (h && Array.isArray(h.methods)) return h.methods.some(m => m && m.name === method);
24
+ return !!plugin && typeof plugin[method] === 'function';
25
+ }
18
26
  function manifestNativePlatform() {
19
27
  const cap = window.Capacitor;
20
28
  if (cap && typeof cap.getPlatform === 'function') { try { return cap.getPlatform(); } catch (e) {} }
@@ -137,9 +145,8 @@ function initManifestNativeNetwork() {
137
145
 
138
146
  // $secure — Keychain/Keystore-backed key/value storage (native) with a
139
147
  // namespaced localStorage fallback on the web. Intended for session tokens etc.
140
- // Native: install a Capacitor secure-storage plugin registered as
141
- // window.Capacitor.Plugins.SecureStorage; if its method shape differs from the
142
- // adapters below, override with $secure.use(adapter). Verify on device.
148
+ // Native: a plugin registered as SecureStorage (@aparajita/capacitor-secure-storage,
149
+ // or any getItem/setItem or get/set shape); $secure.use(adapter) overrides.
143
150
  // The web fallback is NOT encrypted — it degrades honestly, not securely.
144
151
 
145
152
  const MANIFEST_SECURE_NS = 'mnfst:';
@@ -158,33 +165,63 @@ function manifestSecureWebBackend() {
158
165
  };
159
166
  }
160
167
 
161
- // Adapts the two common Capacitor secure-storage plugin shapes: localStorage-like
162
- // (getItem/setItem/removeItem) or object-arg (get/set/remove with {key,value}).
163
- function manifestSecureNativeBackend(plugin) {
164
- const itemApi = typeof plugin.getItem === 'function';
168
+ // aparajita's default prefix, so keys interop with its own getItem/setItem
169
+ const MANIFEST_SECURE_NATIVE_PREFIX = 'capacitor-storage_';
170
+
171
+ function manifestSecureFail(op, e) {
172
+ try { console.error('[Manifest] $secure.' + op + ' failed', e); } catch (_) {}
173
+ throw e instanceof Error ? e : new Error('$secure.' + op + ' failed: ' + ((e && (e.message || e.code)) || e));
174
+ }
175
+
176
+ // Adapts three native shapes: aparajita (internalGetItem…), localStorage-like
177
+ // (getItem/setItem/removeItem) and object-arg (get/set/remove with {key,value}).
178
+ function manifestSecureNativeBackend(plugin, name) {
179
+ const n = name || 'SecureStorage';
180
+ const has = (m) => manifestNativeHas(n, plugin, m);
181
+ const P = MANIFEST_SECURE_NATIVE_PREFIX;
182
+ const shape = has('internalGetItem') ? 'internal' : has('getItem') ? 'item' : has('get') ? 'object' : null;
183
+ const call = async (op, fn) => {
184
+ if (!shape) manifestSecureFail(op, new Error(n + ' plugin has no supported API; use $secure.use(adapter)'));
185
+ try { return await fn(); } catch (e) { manifestSecureFail(op, e); }
186
+ };
165
187
  return {
166
- async get(key) {
167
- try {
168
- if (itemApi) { const v = await plugin.getItem(key); return v == null ? null : v; }
169
- const r = await plugin.get({ key }); return r && typeof r.value !== 'undefined' ? r.value : (r == null ? null : r);
170
- } catch (e) { return null; }
171
- },
172
- async set(key, value) {
173
- try { if (itemApi) await plugin.setItem(key, String(value)); else await plugin.set({ key, value: String(value) }); } catch (e) {}
174
- },
175
- async remove(key) {
176
- try { if (typeof plugin.removeItem === 'function') await plugin.removeItem(key); else await plugin.remove({ key }); } catch (e) {}
177
- },
178
- async keys() { try { const r = await plugin.keys(); return Array.isArray(r) ? r : (r && (r.keys || r.value)) || []; } catch (e) { return []; } },
179
- async clear() { try { await plugin.clear(); } catch (e) {} }
188
+ get: (key) => call('get', async () => {
189
+ if (key == null || key === '') return null;
190
+ if (shape === 'internal') { const r = await plugin.internalGetItem({ prefixedKey: P + key }); return r && r.data != null ? r.data : null; }
191
+ if (shape === 'item') { const v = await plugin.getItem(key); return v == null ? null : v; }
192
+ const r = await plugin.get({ key }); return r && typeof r.value !== 'undefined' ? r.value : (r == null ? null : r);
193
+ }),
194
+ set: (key, value) => call('set', async () => {
195
+ if (key == null || key === '') throw new Error('No key provided');
196
+ if (shape === 'internal') await plugin.internalSetItem({ prefixedKey: P + key, data: String(value) });
197
+ else if (shape === 'item') await plugin.setItem(key, String(value));
198
+ else await plugin.set({ key, value: String(value) });
199
+ }),
200
+ remove: (key) => call('remove', async () => {
201
+ if (key == null || key === '') return;
202
+ if (shape === 'internal') await plugin.internalRemoveItem({ prefixedKey: P + key });
203
+ else if (shape === 'item') {
204
+ if (!has('removeItem')) throw new Error(n + ' plugin has no removeItem(); use $secure.use(adapter)');
205
+ await plugin.removeItem(key);
206
+ }
207
+ else await plugin.remove({ key });
208
+ }),
209
+ keys: () => call('keys', async () => {
210
+ if (shape === 'internal') { const r = await plugin.internalGetPrefixedKeys({ prefix: P }); return ((r && r.keys) || []).map(k => k.slice(P.length)); }
211
+ const r = await plugin.keys(); return Array.isArray(r) ? r : (r && (r.keys || r.value)) || [];
212
+ }),
213
+ clear: () => call('clear', async () => {
214
+ if (shape === 'internal') await plugin.internalClearItemsWithPrefix({ prefix: P });
215
+ else await plugin.clear();
216
+ })
180
217
  };
181
218
  }
182
219
 
183
220
  let _manifestSecureBackend = null;
184
221
  function manifestSecureBackend() {
185
222
  if (_manifestSecureBackend) return _manifestSecureBackend;
186
- const plugin = manifestNativePlugin('SecureStorage');
187
- _manifestSecureBackend = plugin ? manifestSecureNativeBackend(plugin) : manifestSecureWebBackend();
223
+ const plugin = manifestNativeIsNative() ? manifestNativePlugin('SecureStorage') : null;
224
+ _manifestSecureBackend = plugin ? manifestSecureNativeBackend(plugin, 'SecureStorage') : manifestSecureWebBackend();
188
225
  return _manifestSecureBackend;
189
226
  }
190
227
 
@@ -392,33 +429,57 @@ function initManifestHaptics() {
392
429
  }
393
430
 
394
431
 
395
- // $biometric — Face ID / Touch ID. Native: a Capacitor biometric plugin
396
- // (BiometricAuth or NativeBiometric; method shapes differ, adapted below). The web
397
- // has no equivalent one-shot verify (WebAuthn is a separate server ceremony), so
398
- // available() is false and verify() reports unsupported — a genuine native win.
432
+ // $biometric — Face ID / Touch ID. Native: @aparajita/capacitor-biometric-auth
433
+ // (BiometricAuthNative) or @capgo/capacitor-native-biometric (NativeBiometric).
434
+ // No web equivalent: available() is false and verify() reports unsupported.
399
435
 
436
+ const MANIFEST_BIOMETRIC_PLUGINS = ['BiometricAuthNative', 'BiometricAuth', 'NativeBiometric'];
437
+
438
+ // Native only: bundled web builds expose plugin web stubs that always "verify".
400
439
  function manifestBiometricPlugin() {
401
- return manifestNativePlugin('BiometricAuth') || manifestNativePlugin('NativeBiometric') || null;
440
+ if (!manifestNativeIsNative()) return null;
441
+ for (const name of MANIFEST_BIOMETRIC_PLUGINS) {
442
+ const plugin = manifestNativePlugin(name);
443
+ if (plugin) return { name, plugin };
444
+ }
445
+ return null;
446
+ }
447
+
448
+ function manifestBiometricWarn(msg, e) {
449
+ try { console.warn('[Manifest] $biometric: ' + msg, e || ''); } catch (_) {}
402
450
  }
403
451
 
404
452
  async function manifestBiometricAvailable() {
405
- const B = manifestBiometricPlugin();
406
- if (!B) return false;
453
+ const found = manifestBiometricPlugin();
454
+ if (!found) return false;
455
+ const { name, plugin: B } = found;
407
456
  try {
408
- if (typeof B.checkBiometry === 'function') { const r = await B.checkBiometry(); return !!(r && r.isAvailable); }
409
- if (typeof B.isAvailable === 'function') { const r = await B.isAvailable(); return !!(r && r.isAvailable !== false); }
410
- } catch (e) {}
457
+ if (manifestNativeHas(name, B, 'checkBiometry')) { const r = await B.checkBiometry(); return !!(r && r.isAvailable); }
458
+ if (manifestNativeHas(name, B, 'isAvailable')) { const r = await B.isAvailable(); return !!(r && r.isAvailable !== false); }
459
+ } catch (e) { manifestBiometricWarn('availability check failed', e); return false; }
460
+ manifestBiometricWarn(name + ' has no checkBiometry() or isAvailable()');
411
461
  return false;
412
462
  }
413
463
 
414
464
  async function manifestBiometricVerify(opts) {
415
465
  const o = opts || {};
416
- const B = manifestBiometricPlugin();
417
- if (!B) return { verified: false, error: 'unsupported' };
466
+ const found = manifestBiometricPlugin();
467
+ if (!found) return { verified: false, error: 'unsupported' };
468
+ const { name, plugin: B } = found;
418
469
  try {
419
- if (typeof B.authenticate === 'function') { await B.authenticate({ reason: o.reason, title: o.title, subtitle: o.subtitle, cancelTitle: o.cancelTitle }); return { verified: true }; }
420
- if (typeof B.verifyIdentity === 'function') { await B.verifyIdentity({ reason: o.reason, title: o.title, subtitle: o.subtitle }); return { verified: true }; }
421
- } catch (e) { return { verified: false, error: (e && (e.message || e.code)) || 'failed' }; }
470
+ // aparajita: native method + its own option names
471
+ if (manifestNativeHas(name, B, 'internalAuthenticate') || manifestNativeHas(name, B, 'authenticate')) {
472
+ const args = { reason: o.reason, cancelTitle: o.cancelTitle, iosFallbackTitle: o.fallbackTitle, androidTitle: o.title, androidSubtitle: o.subtitle, allowDeviceCredential: o.allowDeviceCredential };
473
+ if (manifestNativeHas(name, B, 'internalAuthenticate')) await B.internalAuthenticate(args);
474
+ else await B.authenticate(args);
475
+ return { verified: true };
476
+ }
477
+ if (manifestNativeHas(name, B, 'verifyIdentity')) {
478
+ await B.verifyIdentity({ reason: o.reason, description: o.reason, title: o.title, subtitle: o.subtitle, negativeButtonText: o.cancelTitle, useFallback: !!o.allowDeviceCredential, fallbackTitle: o.fallbackTitle });
479
+ return { verified: true };
480
+ }
481
+ } catch (e) { return { verified: false, error: (e && (e.message || e.code)) || 'failed', code: (e && e.code) || undefined }; }
482
+ manifestBiometricWarn(name + ' has no supported verify method');
422
483
  return { verified: false, error: 'unsupported' };
423
484
  }
424
485
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest.appwrite.auth.js": "sha384-dsIWnH0qCAGSzAXMGrd8fvOrVl4tOIE6j5tC35JPTrQXZ56qLD36ARyvhR7vi3Id",
3
- "manifest.appwrite.data.js": "sha384-cmzVdqqqKGSMnroBhHczc9m7W72E9O/fV2KEHRDgHkTiLTbZjJj2bAoaVbXtn3A8",
3
+ "manifest.appwrite.data.js": "sha384-7n9o9oibFpnpHDDJ8oYPtiIPddMZyKmmQfXVSwPvTQQw5kK4hz6QNxjmDqjPbbgU",
4
4
  "manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
5
5
  "manifest.bindings.js": "sha384-lPtMf8Ykaic9gNVZKq8Scia1881ZeMAyxer3WCIkUNwwrgzGBWto1OXADUwjvORF",
6
6
  "manifest.charts.js": "sha384-G+huD7UvjF9zDovdVVlh4I5X1Mq4oNnJfoFg4NnGdP3R/4Skq9hy5cGuHNYLxDYm",
@@ -11,24 +11,24 @@
11
11
  "manifest.combobox.js": "sha384-MILTkrgsTzsYUjGo4ZuADqV85Xs4GmfB4xexfe9vbJMIoQTZ42W4zs+DTaVSPEsx",
12
12
  "manifest.components.js": "sha384-BUFA8h+G5Plr82sV7bKzO42pPoiRZwRQcKj1lt2ROFiKjjlkayU/vmkAFqEsdBA+",
13
13
  "manifest.computed.js": "sha384-GYML/e81gQfN+iDbqhPUgny8bINCyL+SrbTofoGk4hlCnLLjv51mCv42JMqq+ku5",
14
- "manifest.data.js": "sha384-+1UOcfFJFXzjpwvvFiLB08B0++DrRbt/CssP8XuAmO+BdqqFfXafC1KmYLlzUEDC",
14
+ "manifest.data.js": "sha384-E3eYfLsWJzw3OjoBBG/tcJPnvEq8guDOBCtnro7hMHA73LcCjlKIjMDFLzpnmUSn",
15
15
  "manifest.datepicker.js": "sha384-2XwFFnS4Bg5FDvG/v8x6KUvrjcBQEOYyTxynX2K/D/Qb3PI6+Y04lEwCnVDJLzwL",
16
16
  "manifest.defer.js": "sha384-pMRGqQKiEoJhpUcD40QeM4jEHau7sz9Xn/YCpiUamT1T4xJBZDSiYX+xnVrSWoq+",
17
- "manifest.device.js": "sha384-g+txmrEszYtuOvRelTp/c1wSNR2AtllVI9rx5NPPD7DzhVUnMNughonZykv2xlBW",
17
+ "manifest.device.js": "sha384-LseLVXfne1wPR8xXM1jqSqylUxekRF3v26TIEaEICE3a4vxjdXoxOJ3wsLAU0zq+",
18
18
  "manifest.dropdowns.js": "sha384-acufUps7R9WjqB4XXW5JLACqniuHJGM4CNTb2rF4B2Q/jv7ZRmsAwfr2McanOz3s",
19
19
  "manifest.edit.js": "sha384-MOgQNdNLeHijhR0u2W6ntoOulKFglsATCFDqkinesOU15tHoILk/JNzHKkGi5dUG",
20
20
  "manifest.export.js": "sha384-Rx6XvqBZc4apzP/FwcDr6yIln4/zNa08Q4ZqBZcEqUz1aNZQfl4h0+Ae7mqCQyuG",
21
21
  "manifest.icons.js": "sha384-kKOINpX70YzNO3FvkV6umrmO3f0KIu0DNbwB+ej55a7y/lhU7v/myQtXL0bwCTbt",
22
22
  "manifest.localization.js": "sha384-EImsIw8Z3l13V5eH9ZJF/yfsOfIvR1oPRNSArANkPji7V8O3IRf6zMwsNiAGj3Hk",
23
23
  "manifest.markdown.js": "sha384-heyDR40LXHaAmAuA+fa381uFwShk1oAqpFFY6E2Qq4T3VN0TQfRqyc++MF40K5Hj",
24
- "manifest.native.js": "sha384-g+txmrEszYtuOvRelTp/c1wSNR2AtllVI9rx5NPPD7DzhVUnMNughonZykv2xlBW",
24
+ "manifest.native.js": "sha384-LseLVXfne1wPR8xXM1jqSqylUxekRF3v26TIEaEICE3a4vxjdXoxOJ3wsLAU0zq+",
25
25
  "manifest.payments.js": "sha384-8pofk2zhxtapPMpwnFyTelPMrYhShNhDCE722uQh/EYFvZFuPLmSdgunqsarVQjv",
26
26
  "manifest.rich.text.js": "sha384-ZBV9jXmu2nmprEHicddRE/m7nR4RPZtVc969Jte0+EJYBcVph7itkjcgN/61PyeC",
27
27
  "manifest.router.js": "sha384-JcKnEb0E9azl6DVSN1DQMysyZOpGkskSQREjD947sMEs87nFEeZpCigB0iq8psz5",
28
28
  "manifest.slides.js": "sha384-3uRTkyK9XPLmnxI2+igZlpi4EyPlU/7IHj5j3BZJJ2KN455vXyk99fiXV3feO/XY",
29
29
  "manifest.status.js": "sha384-MBO345ab7H6Uq1X2dZLt4Ge1Gcra+Z2qYApvlAeIV8CJhDDJrz2FPXpp+Kcp8zcM",
30
30
  "manifest.svg.js": "sha384-qr/8H9+2cIhSXNbRWRtIdrDlTOxKlNJqwEefHvCYoiBNW/kKhPTFa109Orv/EBnb",
31
- "manifest.sw.js": "sha384-sTcnAL2w7bznwSS8c7uFDuPd/ImDTZlO1FBddiOuR0iDaiPo0TGPGeYk0ZeWUbQ5",
31
+ "manifest.sw.js": "sha384-3uO0YGmWHY8tHYrkFf+yAVtEwzM+HIbtCZLKR5suOfKWOL7dd8Yul9+kiB2UM1X4",
32
32
  "manifest.tabs.js": "sha384-OIIcyaQnGS0eNYk4zgAFEDboNYect7THvUd9D0V6nqCeDOxQcoiqLrBEmmWVTTet",
33
33
  "manifest.tailwind.js": "sha384-r/Jvqb+Piz21jUts10BbVAJoKkShOdJRVFpywyi4HPIi/OfOJq5CtPyWAPbdkMgt",
34
34
  "manifest.toasts.js": "sha384-3zkWxzw693pxg3QDFsoYCA3CqYaLKmBaMArULXODD66Utolpewl0OAeTB92aWUVv",
@@ -15,6 +15,14 @@ function manifestNativePlugin(name) {
15
15
  const cap = window.Capacitor;
16
16
  return (cap && cap.Plugins && cap.Plugins[name]) || null;
17
17
  }
18
+ // Whether a plugin exposes a native method. Bundled apps get a registerPlugin
19
+ // Proxy where every property is a function, so prefer the native header.
20
+ function manifestNativeHas(name, plugin, method) {
21
+ const headers = window.Capacitor && window.Capacitor.PluginHeaders;
22
+ const h = Array.isArray(headers) && headers.find(x => x && x.name === name);
23
+ if (h && Array.isArray(h.methods)) return h.methods.some(m => m && m.name === method);
24
+ return !!plugin && typeof plugin[method] === 'function';
25
+ }
18
26
  function manifestNativePlatform() {
19
27
  const cap = window.Capacitor;
20
28
  if (cap && typeof cap.getPlatform === 'function') { try { return cap.getPlatform(); } catch (e) {} }
@@ -137,9 +145,8 @@ function initManifestNativeNetwork() {
137
145
 
138
146
  // $secure — Keychain/Keystore-backed key/value storage (native) with a
139
147
  // namespaced localStorage fallback on the web. Intended for session tokens etc.
140
- // Native: install a Capacitor secure-storage plugin registered as
141
- // window.Capacitor.Plugins.SecureStorage; if its method shape differs from the
142
- // adapters below, override with $secure.use(adapter). Verify on device.
148
+ // Native: a plugin registered as SecureStorage (@aparajita/capacitor-secure-storage,
149
+ // or any getItem/setItem or get/set shape); $secure.use(adapter) overrides.
143
150
  // The web fallback is NOT encrypted — it degrades honestly, not securely.
144
151
 
145
152
  const MANIFEST_SECURE_NS = 'mnfst:';
@@ -158,33 +165,63 @@ function manifestSecureWebBackend() {
158
165
  };
159
166
  }
160
167
 
161
- // Adapts the two common Capacitor secure-storage plugin shapes: localStorage-like
162
- // (getItem/setItem/removeItem) or object-arg (get/set/remove with {key,value}).
163
- function manifestSecureNativeBackend(plugin) {
164
- const itemApi = typeof plugin.getItem === 'function';
168
+ // aparajita's default prefix, so keys interop with its own getItem/setItem
169
+ const MANIFEST_SECURE_NATIVE_PREFIX = 'capacitor-storage_';
170
+
171
+ function manifestSecureFail(op, e) {
172
+ try { console.error('[Manifest] $secure.' + op + ' failed', e); } catch (_) {}
173
+ throw e instanceof Error ? e : new Error('$secure.' + op + ' failed: ' + ((e && (e.message || e.code)) || e));
174
+ }
175
+
176
+ // Adapts three native shapes: aparajita (internalGetItem…), localStorage-like
177
+ // (getItem/setItem/removeItem) and object-arg (get/set/remove with {key,value}).
178
+ function manifestSecureNativeBackend(plugin, name) {
179
+ const n = name || 'SecureStorage';
180
+ const has = (m) => manifestNativeHas(n, plugin, m);
181
+ const P = MANIFEST_SECURE_NATIVE_PREFIX;
182
+ const shape = has('internalGetItem') ? 'internal' : has('getItem') ? 'item' : has('get') ? 'object' : null;
183
+ const call = async (op, fn) => {
184
+ if (!shape) manifestSecureFail(op, new Error(n + ' plugin has no supported API; use $secure.use(adapter)'));
185
+ try { return await fn(); } catch (e) { manifestSecureFail(op, e); }
186
+ };
165
187
  return {
166
- async get(key) {
167
- try {
168
- if (itemApi) { const v = await plugin.getItem(key); return v == null ? null : v; }
169
- const r = await plugin.get({ key }); return r && typeof r.value !== 'undefined' ? r.value : (r == null ? null : r);
170
- } catch (e) { return null; }
171
- },
172
- async set(key, value) {
173
- try { if (itemApi) await plugin.setItem(key, String(value)); else await plugin.set({ key, value: String(value) }); } catch (e) {}
174
- },
175
- async remove(key) {
176
- try { if (typeof plugin.removeItem === 'function') await plugin.removeItem(key); else await plugin.remove({ key }); } catch (e) {}
177
- },
178
- async keys() { try { const r = await plugin.keys(); return Array.isArray(r) ? r : (r && (r.keys || r.value)) || []; } catch (e) { return []; } },
179
- async clear() { try { await plugin.clear(); } catch (e) {} }
188
+ get: (key) => call('get', async () => {
189
+ if (key == null || key === '') return null;
190
+ if (shape === 'internal') { const r = await plugin.internalGetItem({ prefixedKey: P + key }); return r && r.data != null ? r.data : null; }
191
+ if (shape === 'item') { const v = await plugin.getItem(key); return v == null ? null : v; }
192
+ const r = await plugin.get({ key }); return r && typeof r.value !== 'undefined' ? r.value : (r == null ? null : r);
193
+ }),
194
+ set: (key, value) => call('set', async () => {
195
+ if (key == null || key === '') throw new Error('No key provided');
196
+ if (shape === 'internal') await plugin.internalSetItem({ prefixedKey: P + key, data: String(value) });
197
+ else if (shape === 'item') await plugin.setItem(key, String(value));
198
+ else await plugin.set({ key, value: String(value) });
199
+ }),
200
+ remove: (key) => call('remove', async () => {
201
+ if (key == null || key === '') return;
202
+ if (shape === 'internal') await plugin.internalRemoveItem({ prefixedKey: P + key });
203
+ else if (shape === 'item') {
204
+ if (!has('removeItem')) throw new Error(n + ' plugin has no removeItem(); use $secure.use(adapter)');
205
+ await plugin.removeItem(key);
206
+ }
207
+ else await plugin.remove({ key });
208
+ }),
209
+ keys: () => call('keys', async () => {
210
+ if (shape === 'internal') { const r = await plugin.internalGetPrefixedKeys({ prefix: P }); return ((r && r.keys) || []).map(k => k.slice(P.length)); }
211
+ const r = await plugin.keys(); return Array.isArray(r) ? r : (r && (r.keys || r.value)) || [];
212
+ }),
213
+ clear: () => call('clear', async () => {
214
+ if (shape === 'internal') await plugin.internalClearItemsWithPrefix({ prefix: P });
215
+ else await plugin.clear();
216
+ })
180
217
  };
181
218
  }
182
219
 
183
220
  let _manifestSecureBackend = null;
184
221
  function manifestSecureBackend() {
185
222
  if (_manifestSecureBackend) return _manifestSecureBackend;
186
- const plugin = manifestNativePlugin('SecureStorage');
187
- _manifestSecureBackend = plugin ? manifestSecureNativeBackend(plugin) : manifestSecureWebBackend();
223
+ const plugin = manifestNativeIsNative() ? manifestNativePlugin('SecureStorage') : null;
224
+ _manifestSecureBackend = plugin ? manifestSecureNativeBackend(plugin, 'SecureStorage') : manifestSecureWebBackend();
188
225
  return _manifestSecureBackend;
189
226
  }
190
227
 
@@ -392,33 +429,57 @@ function initManifestHaptics() {
392
429
  }
393
430
 
394
431
 
395
- // $biometric — Face ID / Touch ID. Native: a Capacitor biometric plugin
396
- // (BiometricAuth or NativeBiometric; method shapes differ, adapted below). The web
397
- // has no equivalent one-shot verify (WebAuthn is a separate server ceremony), so
398
- // available() is false and verify() reports unsupported — a genuine native win.
432
+ // $biometric — Face ID / Touch ID. Native: @aparajita/capacitor-biometric-auth
433
+ // (BiometricAuthNative) or @capgo/capacitor-native-biometric (NativeBiometric).
434
+ // No web equivalent: available() is false and verify() reports unsupported.
399
435
 
436
+ const MANIFEST_BIOMETRIC_PLUGINS = ['BiometricAuthNative', 'BiometricAuth', 'NativeBiometric'];
437
+
438
+ // Native only: bundled web builds expose plugin web stubs that always "verify".
400
439
  function manifestBiometricPlugin() {
401
- return manifestNativePlugin('BiometricAuth') || manifestNativePlugin('NativeBiometric') || null;
440
+ if (!manifestNativeIsNative()) return null;
441
+ for (const name of MANIFEST_BIOMETRIC_PLUGINS) {
442
+ const plugin = manifestNativePlugin(name);
443
+ if (plugin) return { name, plugin };
444
+ }
445
+ return null;
446
+ }
447
+
448
+ function manifestBiometricWarn(msg, e) {
449
+ try { console.warn('[Manifest] $biometric: ' + msg, e || ''); } catch (_) {}
402
450
  }
403
451
 
404
452
  async function manifestBiometricAvailable() {
405
- const B = manifestBiometricPlugin();
406
- if (!B) return false;
453
+ const found = manifestBiometricPlugin();
454
+ if (!found) return false;
455
+ const { name, plugin: B } = found;
407
456
  try {
408
- if (typeof B.checkBiometry === 'function') { const r = await B.checkBiometry(); return !!(r && r.isAvailable); }
409
- if (typeof B.isAvailable === 'function') { const r = await B.isAvailable(); return !!(r && r.isAvailable !== false); }
410
- } catch (e) {}
457
+ if (manifestNativeHas(name, B, 'checkBiometry')) { const r = await B.checkBiometry(); return !!(r && r.isAvailable); }
458
+ if (manifestNativeHas(name, B, 'isAvailable')) { const r = await B.isAvailable(); return !!(r && r.isAvailable !== false); }
459
+ } catch (e) { manifestBiometricWarn('availability check failed', e); return false; }
460
+ manifestBiometricWarn(name + ' has no checkBiometry() or isAvailable()');
411
461
  return false;
412
462
  }
413
463
 
414
464
  async function manifestBiometricVerify(opts) {
415
465
  const o = opts || {};
416
- const B = manifestBiometricPlugin();
417
- if (!B) return { verified: false, error: 'unsupported' };
466
+ const found = manifestBiometricPlugin();
467
+ if (!found) return { verified: false, error: 'unsupported' };
468
+ const { name, plugin: B } = found;
418
469
  try {
419
- if (typeof B.authenticate === 'function') { await B.authenticate({ reason: o.reason, title: o.title, subtitle: o.subtitle, cancelTitle: o.cancelTitle }); return { verified: true }; }
420
- if (typeof B.verifyIdentity === 'function') { await B.verifyIdentity({ reason: o.reason, title: o.title, subtitle: o.subtitle }); return { verified: true }; }
421
- } catch (e) { return { verified: false, error: (e && (e.message || e.code)) || 'failed' }; }
470
+ // aparajita: native method + its own option names
471
+ if (manifestNativeHas(name, B, 'internalAuthenticate') || manifestNativeHas(name, B, 'authenticate')) {
472
+ const args = { reason: o.reason, cancelTitle: o.cancelTitle, iosFallbackTitle: o.fallbackTitle, androidTitle: o.title, androidSubtitle: o.subtitle, allowDeviceCredential: o.allowDeviceCredential };
473
+ if (manifestNativeHas(name, B, 'internalAuthenticate')) await B.internalAuthenticate(args);
474
+ else await B.authenticate(args);
475
+ return { verified: true };
476
+ }
477
+ if (manifestNativeHas(name, B, 'verifyIdentity')) {
478
+ await B.verifyIdentity({ reason: o.reason, description: o.reason, title: o.title, subtitle: o.subtitle, negativeButtonText: o.cancelTitle, useFallback: !!o.allowDeviceCredential, fallbackTitle: o.fallbackTitle });
479
+ return { verified: true };
480
+ }
481
+ } catch (e) { return { verified: false, error: (e && (e.message || e.code)) || 'failed', code: (e && e.code) || undefined }; }
482
+ manifestBiometricWarn(name + ' has no supported verify method');
422
483
  return { verified: false, error: 'unsupported' };
423
484
  }
424
485
 
@@ -246,7 +246,7 @@
246
246
  }
247
247
  },
248
248
  "realtime": {
249
- "description": "Realtime subscription config. false disables the subscription. { mode, maxRows } bounds growth: 'append' (default) stops appending at maxRows and counts skipped events in $realtimeOverflow; 'window' keeps the initial window size by evicting the oldest realtime-appended rows. maxRows defaults to max(500, 2x the initially loaded rows); 0 opts into legacy unbounded growth.",
249
+ "description": "Realtime subscription config. false disables the subscription. { mode, maxRows } bounds growth: 'append' (default) stops appending at maxRows and counts skipped events in $realtimeOverflow; 'window' keeps the initial window size by evicting the oldest realtime-appended rows. maxRows defaults to max(500, 2x the initially loaded rows); 0 opts into legacy unbounded growth. serverFilter picks which filters ride the Appwrite realtime subscription server-side: 'scope' (default) sends only scope-column filters; 'all' (alias true) sends every expressible filter — rows that stop matching then drop on the next load instead of instantly; false sends none.",
250
250
  "oneOf": [
251
251
  {
252
252
  "type": "boolean"
@@ -264,6 +264,20 @@
264
264
  "maxRows": {
265
265
  "type": "number",
266
266
  "minimum": 0
267
+ },
268
+ "serverFilter": {
269
+ "oneOf": [
270
+ {
271
+ "type": "boolean"
272
+ },
273
+ {
274
+ "type": "string",
275
+ "enum": [
276
+ "scope",
277
+ "all"
278
+ ]
279
+ }
280
+ ]
267
281
  }
268
282
  },
269
283
  "additionalProperties": false
@@ -16,7 +16,7 @@
16
16
 
17
17
  // Build stamps the published version here; unstamped (dev) falls back to the
18
18
  // registration URL's `v`, then 'dev'.
19
- const BUILD_VERSION = '0.5.248';
19
+ const BUILD_VERSION = '0.5.250';
20
20
  const CACHE_PREFIX = 'mnfst-sw:';
21
21
  const META_CACHE = CACHE_PREFIX + 'meta';
22
22
  // Sized to hold a full precache.json warm-up (PRECACHE_MAX_ENTRIES server-side)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.248",
3
+ "version": "0.5.250",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",