@teincfood/core 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -157,6 +157,7 @@ export { notifyAuthChanged, subscribeAuth } from "./adapters/auth";
157
157
  export { isAuthenticated, hasActiveBusiness, canBusinessSync, setOfflineSyncEnabled, isOfflineSyncEnabled } from "./sync/sync-guard";
158
158
  export { ACCESS_EXPIRED_CODE, SESSION_DEAD_CODES, SKIP_AUTH_RETRY, extractAuthFailure, isAccessExpired, isSessionDead, hasAuthRetrySkip, refreshSession, withAuthRetry, setOfflineSessionRecoveryProvider, } from "./auth/session";
159
159
  export * from "./offline/license";
160
+ export * from "./offline/random";
160
161
  export * from "./offline/store";
161
162
  export * from "./offline/service";
162
163
  export type { AuthFailure, SessionTokens } from "./auth/session";
package/dist/index.js CHANGED
@@ -173,6 +173,7 @@ export { isAuthenticated, hasActiveBusiness, canBusinessSync, setOfflineSyncEnab
173
173
  export { ACCESS_EXPIRED_CODE, SESSION_DEAD_CODES, SKIP_AUTH_RETRY, extractAuthFailure, isAccessExpired, isSessionDead, hasAuthRetrySkip, refreshSession, withAuthRetry, setOfflineSessionRecoveryProvider, } from "./auth/session";
174
174
  // ── Offline device license + member unlock ──
175
175
  export * from "./offline/license";
176
+ export * from "./offline/random";
176
177
  export * from "./offline/store";
177
178
  export * from "./offline/service";
178
179
  // ── Image cache (durable) ──
@@ -10,6 +10,7 @@ import { applyPendingOrderEvent, applyOrderStateChangeEvent, notifyPendingOrders
10
10
  import { applyMenuCommandLocal } from "../reference/menu-apply";
11
11
  import { REFERENCE_CHANGED_EVENT_TYPE } from "../reference/events";
12
12
  import { applyReferenceChangeEvent } from "../reference/events";
13
+ import { referenceSyncService } from "../reference/service";
13
14
  import { openSyncDatabase } from "../db/connection";
14
15
  import { insertOrder } from "../db/operations";
15
16
  import { createLocalOrder, transitionLocalOrder } from "../domain/orders/service";
@@ -567,6 +568,16 @@ class LocalNodeServiceCore {
567
568
  syncLogger.info("LocalNodeService", "Connected to peer", { deviceId: node.deviceId });
568
569
  this.peerInfos.set(node.deviceId, { deviceId: node.deviceId, host: node.host, port: node.port });
569
570
  localNodeStore.getState().addConnectedClient({ deviceId: node.deviceId, deviceName: node.deviceName, host: node.host, connectedAt: new Date().toISOString() });
571
+ // A terminal may have missed `reference_data.changed` while it was
572
+ // disconnected. Compare versions with the peer on reconnect so menu,
573
+ // members, hours and tax changes converge even on a dark LAN.
574
+ const connectivity = getConnectivityState();
575
+ if (this.businessId &&
576
+ (!connectivity.isOnline || connectivity.isCloudReachable === "unreachable")) {
577
+ void referenceSyncService
578
+ .sync({ businessId: this.businessId })
579
+ .catch((e) => syncLogger.warn("LocalNodeService", "LAN reference catch-up failed", { error: String(e) }));
580
+ }
570
581
  },
571
582
  onDisconnected: () => {
572
583
  this.settleDial(node.deviceId);
@@ -0,0 +1,3 @@
1
+ /** Shared secure-random provider for offline keys, nonces and PIN salts. */
2
+ export declare function setOfflineRandomBytesProvider(next: ((length: number) => Uint8Array) | null): void;
3
+ export declare function offlineRandomBytes(length: number): Uint8Array;
@@ -0,0 +1,14 @@
1
+ /** Shared secure-random provider for offline keys, nonces and PIN salts. */
2
+ let provider = null;
3
+ export function setOfflineRandomBytesProvider(next) {
4
+ provider = next;
5
+ }
6
+ export function offlineRandomBytes(length) {
7
+ if (provider)
8
+ return provider(length);
9
+ const cryptoObj = globalThis.crypto;
10
+ if (cryptoObj?.getRandomValues) {
11
+ return cryptoObj.getRandomValues(new Uint8Array(length));
12
+ }
13
+ throw new Error("No secure random source available. Call setOfflineRandomBytesProvider().");
14
+ }
@@ -34,7 +34,6 @@ export interface OfflineSessionState {
34
34
  keyId: string | null;
35
35
  members: OfflineMemberCredential[];
36
36
  }
37
- export declare function setOfflineRandomBytesProvider(provider: ((length: number) => Uint8Array) | null): void;
38
37
  /**
39
38
  * First-time activation. Generates the device keypair, registers the public
40
39
  * key, stores the signed license and enrolls the current member's PIN.
@@ -9,20 +9,11 @@ import { getAppInstanceIdAsync } from "../adapters/devices";
9
9
  import { getHttpClient } from "../adapters/http";
10
10
  import { getAuthToken } from "../adapters/auth";
11
11
  import { syncLogger } from "../utils/sync-logger";
12
+ import { offlineRandomBytes } from "./random";
12
13
  import { base64UrlDecode, base64UrlEncode, canonicalDeviceChallenge, decodeLicensePayload, evaluateOfflineLicense, generateOfflineDeviceKeypair, loadAndVerifyOfflineLicense, newOfflineNonce, signDeviceChallenge, verifyOfflineLicense, } from "./license";
13
14
  import { clearOfflineDevice, createOfflineMemberCredential, getDevicePrivateKey, getOfflineMember, getStoredLicense, getStoredLicenseKeyId, getStoredLicensePublicKey, listOfflineMembers, markOfflineMemberLogin, removeOfflineMember, saveDevicePrivateKey, saveStoredLicense, saveStoredLicenseKeyId, saveStoredLicensePublicKey, verifyOfflineMemberPin, } from "./store";
14
- let randomBytesProvider = null;
15
- export function setOfflineRandomBytesProvider(provider) {
16
- randomBytesProvider = provider;
17
- }
18
15
  function randomBytes(length) {
19
- if (randomBytesProvider)
20
- return randomBytesProvider(length);
21
- const cryptoObj = globalThis.crypto;
22
- if (cryptoObj?.getRandomValues) {
23
- return cryptoObj.getRandomValues(new Uint8Array(length));
24
- }
25
- throw new Error("No secure random source available. Call setOfflineRandomBytesProvider().");
16
+ return offlineRandomBytes(length);
26
17
  }
27
18
  async function getPrivateKeyBytes() {
28
19
  const encoded = await getDevicePrivateKey();
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { getItem, removeItem, setItem } from "../adapters/kv";
12
12
  import { sha256Hex } from "../reference/entitlement-crypto";
13
+ import { offlineRandomBytes } from "./random";
13
14
  export const OFFLINE_STORAGE_KEYS = {
14
15
  LICENSE: "offline:license",
15
16
  PUBLIC_KEY: "offline:license:public_key",
@@ -88,12 +89,11 @@ export async function clearOfflineDevice() {
88
89
  }
89
90
  const PIN_ROUNDS = 20000;
90
91
  function randomHex(length) {
91
- const alphabet = "0123456789abcdef";
92
+ const bytes = offlineRandomBytes(Math.ceil(length / 2));
92
93
  let out = "";
93
- for (let i = 0; i < length; i++) {
94
- out += alphabet[Math.floor(Math.random() * alphabet.length)];
95
- }
96
- return out;
94
+ for (let i = 0; i < bytes.length; i++)
95
+ out += bytes[i].toString(16).padStart(2, "0");
96
+ return out.slice(0, length);
97
97
  }
98
98
  async function hashPin(pin, salt) {
99
99
  let digest = `${salt}:${pin}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teincfood/core",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "TeincFood shared offline-first core — types, sync engine, local DB, reference data, and repositories for mobile + desktop",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",