@teincfood/core 0.10.25 → 0.10.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/sync/offline-capable.js +60 -48
- package/package.json +1 -1
|
@@ -24,61 +24,73 @@ import { applyMenuCommandLocal } from "../reference/menu-apply";
|
|
|
24
24
|
import { emitReferenceChanged } from "../reference/events";
|
|
25
25
|
import { assertEntitledForCommand } from "../reference/entitlement-guard";
|
|
26
26
|
import { isOnlineCapable } from "./online-gates";
|
|
27
|
+
/**
|
|
28
|
+
* Enforce the cached RBAC capabilities downloaded from the backend.
|
|
29
|
+
*
|
|
30
|
+
* Fail-open only when no capabilities record exists yet (first boot); once a
|
|
31
|
+
* record is present, an unknown capability is a real denial and must never
|
|
32
|
+
* reach the local cache or the outbox.
|
|
33
|
+
*/
|
|
34
|
+
async function assertCachedCapability(businessId, required) {
|
|
35
|
+
try {
|
|
36
|
+
const db = await openSyncDatabase();
|
|
37
|
+
const rec = await getReferenceData(db, businessId, "capabilities");
|
|
38
|
+
const caps = rec?.payload?.data
|
|
39
|
+
?.capabilities ?? [];
|
|
40
|
+
if (rec && caps.length > 0 && !caps.includes(required)) {
|
|
41
|
+
throw new ApiError(`Missing capability: ${required}`, 403, "FORBIDDEN");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
if (e instanceof ApiError && e.status === 403)
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
27
49
|
export async function runOfflineCapableMutation(options) {
|
|
28
|
-
//
|
|
50
|
+
// Online-required gate first: these commands can never succeed offline —
|
|
51
|
+
// throw loud immediately instead of queuing what no transport can fulfill.
|
|
52
|
+
assertOnlineFor(options.commandType, options.payload);
|
|
53
|
+
// Per-command capability guard before touching local state or the network.
|
|
29
54
|
const required = CAPABILITY_BY_COMMAND[options.commandType];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
// Local-first for engine-routed domains (menu:*): persist to SQLite and the
|
|
56
|
+
// durable outbox immediately, update the local reference cache, and let
|
|
57
|
+
// replay upload to the cloud/LAN in the background. A slow or absent
|
|
58
|
+
// network must never hold the form open.
|
|
59
|
+
if (options.commandType.startsWith("menu:")) {
|
|
60
|
+
if (required)
|
|
61
|
+
await assertCachedCapability(options.businessId, required);
|
|
62
|
+
// Preserve the entitlement gate that the engine normally applies before
|
|
63
|
+
// queueing: local-first must not weaken subscription enforcement.
|
|
64
|
+
await assertEntitledForCommand(options.businessId, options.commandType);
|
|
65
|
+
const command = buildCommand({
|
|
66
|
+
commandType: options.commandType,
|
|
67
|
+
businessId: options.businessId,
|
|
68
|
+
entityId: options.entityId,
|
|
69
|
+
payload: options.payload,
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
const { keys } = await applyMenuCommandLocal(options.businessId, command);
|
|
73
|
+
if (keys.length)
|
|
74
|
+
emitReferenceChanged(options.businessId, keys);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
syncLogger.debug("OfflineCapable", "menu local apply failed", {
|
|
78
|
+
error: String(error),
|
|
47
79
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
syncLogger.debug("OfflineCapable", "menu local apply failed", {
|
|
80
|
+
}
|
|
81
|
+
const event = await commandQueueService.enqueue(command);
|
|
82
|
+
if (isOnlineCapable()) {
|
|
83
|
+
// Background only: save must return now.
|
|
84
|
+
void syncEngine.replayQueuedCommands().catch((error) => {
|
|
85
|
+
syncLogger.debug("OfflineCapable", "menu background replay failed", {
|
|
55
86
|
error: String(error),
|
|
56
87
|
});
|
|
57
|
-
}
|
|
58
|
-
const event = await commandQueueService.enqueue(command);
|
|
59
|
-
if (isOnlineCapable()) {
|
|
60
|
-
// Background only: save must return now.
|
|
61
|
-
void syncEngine.replayQueuedCommands().catch((error) => {
|
|
62
|
-
syncLogger.debug("OfflineCapable", "menu background replay failed", {
|
|
63
|
-
error: String(error),
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
return event.payload;
|
|
68
|
-
}
|
|
69
|
-
try {
|
|
70
|
-
const db = await openSyncDatabase();
|
|
71
|
-
const rec = await getReferenceData(db, options.businessId, "capabilities");
|
|
72
|
-
const caps = rec?.payload?.data?.capabilities ?? [];
|
|
73
|
-
if (rec && caps.length > 0 && !caps.includes(required)) {
|
|
74
|
-
throw new ApiError(`Missing capability: ${required}`, 403, "FORBIDDEN");
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
catch (e) {
|
|
78
|
-
if (e instanceof ApiError && e.status === 403)
|
|
79
|
-
throw e;
|
|
88
|
+
});
|
|
80
89
|
}
|
|
90
|
+
return event.payload;
|
|
81
91
|
}
|
|
92
|
+
if (required)
|
|
93
|
+
await assertCachedCapability(options.businessId, required);
|
|
82
94
|
try {
|
|
83
95
|
// Cloud already known unreachable (or no network): skip the direct REST
|
|
84
96
|
// attempt entirely — it would only burn a timeout before falling back.
|
package/package.json
CHANGED