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.
- package/lib/manifest.appwrite.data.js +207 -49
- package/lib/manifest.d.ts +8 -1
- package/lib/manifest.data.js +19 -3
- package/lib/manifest.device.js +99 -38
- package/lib/manifest.integrity.json +5 -5
- package/lib/manifest.native.js +99 -38
- package/lib/manifest.schema.json +15 -1
- package/lib/manifest.sw.js +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
(function () {
|
|
4
4
|
|
|
5
|
-
const MANIFEST_BUILD_VERSION = '0.5.
|
|
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;
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
//
|
|
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(
|
|
710
|
-
const entry =
|
|
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
|
|
727
|
-
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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,
|
|
764
|
-
// isCurrent: false once the requesting load is superseded (logout) — skip, never touch a newer entry
|
|
765
|
-
async
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
|
|
874
|
+
const realtime = services.realtime;
|
|
875
|
+
await syncInstance(realtime);
|
|
876
|
+
if (isCurrent && !isCurrent()) return null;
|
|
776
877
|
|
|
777
|
-
|
|
778
|
-
if (!
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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()])
|
|
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
|
|
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 (!
|
|
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:
|
|
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?:
|
|
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. */
|
package/lib/manifest.data.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
(function () {
|
|
4
4
|
|
|
5
|
-
const MANIFEST_BUILD_VERSION = '0.5.
|
|
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
|
}
|
package/lib/manifest.device.js
CHANGED
|
@@ -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:
|
|
141
|
-
//
|
|
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
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
},
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
},
|
|
178
|
-
|
|
179
|
-
|
|
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:
|
|
396
|
-
// (
|
|
397
|
-
//
|
|
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
|
-
|
|
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
|
|
406
|
-
if (!
|
|
453
|
+
const found = manifestBiometricPlugin();
|
|
454
|
+
if (!found) return false;
|
|
455
|
+
const { name, plugin: B } = found;
|
|
407
456
|
try {
|
|
408
|
-
if (
|
|
409
|
-
if (
|
|
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
|
|
417
|
-
if (!
|
|
466
|
+
const found = manifestBiometricPlugin();
|
|
467
|
+
if (!found) return { verified: false, error: 'unsupported' };
|
|
468
|
+
const { name, plugin: B } = found;
|
|
418
469
|
try {
|
|
419
|
-
|
|
420
|
-
if (
|
|
421
|
-
|
|
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-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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",
|
package/lib/manifest.native.js
CHANGED
|
@@ -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:
|
|
141
|
-
//
|
|
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
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
},
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
},
|
|
178
|
-
|
|
179
|
-
|
|
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:
|
|
396
|
-
// (
|
|
397
|
-
//
|
|
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
|
-
|
|
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
|
|
406
|
-
if (!
|
|
453
|
+
const found = manifestBiometricPlugin();
|
|
454
|
+
if (!found) return false;
|
|
455
|
+
const { name, plugin: B } = found;
|
|
407
456
|
try {
|
|
408
|
-
if (
|
|
409
|
-
if (
|
|
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
|
|
417
|
-
if (!
|
|
466
|
+
const found = manifestBiometricPlugin();
|
|
467
|
+
if (!found) return { verified: false, error: 'unsupported' };
|
|
468
|
+
const { name, plugin: B } = found;
|
|
418
469
|
try {
|
|
419
|
-
|
|
420
|
-
if (
|
|
421
|
-
|
|
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
|
|
package/lib/manifest.schema.json
CHANGED
|
@@ -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
|
package/lib/manifest.sw.js
CHANGED
|
@@ -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.
|
|
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)
|