@teincfood/core 0.10.18 → 0.10.20

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
@@ -59,7 +59,7 @@ export * from "./sync/command-builder";
59
59
  export { commandQueueService } from "./sync/command-queue.service";
60
60
  export { buildCommand } from "./sync/command-builder";
61
61
  export { generateLocalOrderNumber, generateLocalCalloutNumber, isLocalOrderNumber, generateOrderNumber, generateOrderNumberForBusiness, isOfflineOrderNumber, parseOrderNumber, resetCalloutForTests, } from "./sync/order-number";
62
- export { deviceLeaseService, getDeviceNumber, isOnlineOnly, ensureLease } from "./sync/device-lease.service";
62
+ export { deviceLeaseService, getDeviceNumber, isOnlineOnly, ensureLease, getLastLeaseFailure, type DeviceLeaseFailure, } from "./sync/device-lease.service";
63
63
  export { useDeviceLease, ensureDeviceLease } from "./sync/use-device-lease";
64
64
  export { getStartupReadiness, ensureStartupReady, REQUIRED_ORDER_KEYS, type StartupReadiness, } from "./sync/startup-readiness";
65
65
  export { useStartupReadiness, type UseStartupReadinessOptions, type UseStartupReadinessResult, } from "./hooks/startup-readiness.hooks";
package/dist/index.js CHANGED
@@ -61,7 +61,7 @@ export * from "./sync/command-builder";
61
61
  export { commandQueueService } from "./sync/command-queue.service";
62
62
  export { buildCommand } from "./sync/command-builder";
63
63
  export { generateLocalOrderNumber, generateLocalCalloutNumber, isLocalOrderNumber, generateOrderNumber, generateOrderNumberForBusiness, isOfflineOrderNumber, parseOrderNumber, resetCalloutForTests, } from "./sync/order-number";
64
- export { deviceLeaseService, getDeviceNumber, isOnlineOnly, ensureLease } from "./sync/device-lease.service";
64
+ export { deviceLeaseService, getDeviceNumber, isOnlineOnly, ensureLease, getLastLeaseFailure, } from "./sync/device-lease.service";
65
65
  export { useDeviceLease, ensureDeviceLease } from "./sync/use-device-lease";
66
66
  export { getStartupReadiness, ensureStartupReady, REQUIRED_ORDER_KEYS, } from "./sync/startup-readiness";
67
67
  export { useStartupReadiness, } from "./hooks/startup-readiness.hooks";
@@ -34,12 +34,26 @@ export interface OfflineSessionState {
34
34
  keyId: string | null;
35
35
  members: OfflineMemberCredential[];
36
36
  }
37
+ interface OfflineActivationBaseArgs {
38
+ businessId: string;
39
+ appInstanceId?: string;
40
+ platform?: string;
41
+ deviceName?: string;
42
+ osVersion?: string;
43
+ appVersion?: string;
44
+ }
37
45
  /**
38
- * First-time activation. Generates the device keypair, registers the public
39
- * key, stores the signed license and enrolls the current member's PIN.
46
+ * Activate the terminal license without creating any member PIN. The backend
47
+ * activation also enrolls the signed-in user, but the quick-login PIN is a
48
+ * separate, optional local credential.
40
49
  */
41
- export declare function activateOfflineDevice(args: {
42
- businessId: string;
50
+ export declare function activateOfflineLicense(args: OfflineActivationBaseArgs): Promise<OfflineActivationResult>;
51
+ /**
52
+ * First-time activation plus the optional local quick-login PIN. Kept as the
53
+ * mobile-compatible entry point; desktop can call activateOfflineLicense()
54
+ * first and set the PIN later.
55
+ */
56
+ export declare function activateOfflineDevice(args: OfflineActivationBaseArgs & {
43
57
  member: {
44
58
  userId: string;
45
59
  fullName: string;
@@ -48,12 +62,23 @@ export declare function activateOfflineDevice(args: {
48
62
  capabilities: string[];
49
63
  };
50
64
  pin: string;
51
- appInstanceId?: string;
52
- platform?: string;
53
- deviceName?: string;
54
- osVersion?: string;
55
- appVersion?: string;
56
65
  }): Promise<OfflineActivationResult>;
66
+ /**
67
+ * Set or change the local 6-digit quick-login PIN. Existing PINs must be
68
+ * confirmed first; changing a PIN never requires the network.
69
+ */
70
+ export declare function setOfflineMemberPin(args: {
71
+ businessId: string;
72
+ userId: string;
73
+ newPin: string;
74
+ currentPin?: string;
75
+ member?: {
76
+ fullName: string;
77
+ email?: string | null;
78
+ role: string;
79
+ capabilities: string[];
80
+ };
81
+ }): Promise<OfflineMemberCredential>;
57
82
  /** Refresh the signed license while online using the normal access token. */
58
83
  export declare function refreshOfflineLicense(businessId: string): Promise<OfflineActivationResult>;
59
84
  /**
@@ -12,7 +12,7 @@ import { getConnectivityState } from "../adapters/connectivity";
12
12
  import { syncLogger } from "../utils/sync-logger";
13
13
  import { offlineRandomBytes } from "./random";
14
14
  import { base64UrlDecode, base64UrlEncode, canonicalDeviceChallenge, decodeLicensePayload, evaluateOfflineLicense, generateOfflineDeviceKeypair, loadAndVerifyOfflineLicense, newOfflineNonce, signDeviceChallenge, verifyOfflineLicense, } from "./license";
15
- import { clearOfflineDevice, createOfflineMemberCredential, getDevicePrivateKey, getOfflineMember, getStoredLicense, getStoredLicenseKeyId, getStoredLicensePublicKey, listOfflineMembers, markOfflineMemberLogin, removeOfflineMember, saveDevicePrivateKey, saveStoredLicense, saveStoredLicenseKeyId, saveStoredLicensePublicKey, verifyOfflineMemberPin, } from "./store";
15
+ import { clearOfflineDevice, createOfflineMemberCredential, getDevicePrivateKey, getOfflineMember, getStoredLicense, getStoredLicenseKeyId, getStoredLicensePublicKey, listOfflineMembers, markOfflineMemberLogin, removeOfflineMember, saveDevicePrivateKey, saveStoredLicense, saveStoredLicenseKeyId, saveStoredLicensePublicKey, updateOfflineMemberPin, verifyOfflineMemberPin, } from "./store";
16
16
  function randomBytes(length) {
17
17
  return offlineRandomBytes(length);
18
18
  }
@@ -29,10 +29,11 @@ async function getPrivateKeyBytes() {
29
29
  }
30
30
  }
31
31
  /**
32
- * First-time activation. Generates the device keypair, registers the public
33
- * key, stores the signed license and enrolls the current member's PIN.
32
+ * Activate the terminal license without creating any member PIN. The backend
33
+ * activation also enrolls the signed-in user, but the quick-login PIN is a
34
+ * separate, optional local credential.
34
35
  */
35
- export async function activateOfflineDevice(args) {
36
+ export async function activateOfflineLicense(args) {
36
37
  if (!getAuthToken())
37
38
  throw new Error("Sign in online before activating this device");
38
39
  const deviceId = args.appInstanceId ?? (await getAppInstanceIdAsync());
@@ -57,6 +58,15 @@ export async function activateOfflineDevice(args) {
57
58
  await saveStoredLicense(args.businessId, data.license);
58
59
  await saveStoredLicensePublicKey(args.businessId, data.public_key);
59
60
  await saveStoredLicenseKeyId(args.businessId, data.key_id);
61
+ return { ...data, payload: verified };
62
+ }
63
+ /**
64
+ * First-time activation plus the optional local quick-login PIN. Kept as the
65
+ * mobile-compatible entry point; desktop can call activateOfflineLicense()
66
+ * first and set the PIN later.
67
+ */
68
+ export async function activateOfflineDevice(args) {
69
+ const result = await activateOfflineLicense(args);
60
70
  await createOfflineMemberCredential({
61
71
  businessId: args.businessId,
62
72
  userId: args.member.userId,
@@ -70,14 +80,51 @@ export async function activateOfflineDevice(args) {
70
80
  // the intent explicit and is idempotent.
71
81
  await enrollMemberOnDevice({
72
82
  businessId: args.businessId,
73
- deviceId,
83
+ deviceId: args.appInstanceId,
74
84
  userId: args.member.userId,
75
85
  }).catch((error) => {
76
86
  syncLogger.warn("OfflineAuth", "Member enrollment call failed", {
77
87
  error: String(error),
78
88
  });
79
89
  });
80
- return { ...data, payload: verified };
90
+ return result;
91
+ }
92
+ /**
93
+ * Set or change the local 6-digit quick-login PIN. Existing PINs must be
94
+ * confirmed first; changing a PIN never requires the network.
95
+ */
96
+ export async function setOfflineMemberPin(args) {
97
+ if (!/^\d{6}$/.test(args.newPin)) {
98
+ throw new Error("Offline PIN must be exactly 6 digits");
99
+ }
100
+ const existing = await getOfflineMember(args.userId, args.businessId);
101
+ if (existing) {
102
+ if (existing.pin_hash) {
103
+ if (!args.currentPin) {
104
+ throw new Error("Enter your current 6-digit PIN");
105
+ }
106
+ const valid = await verifyOfflineMemberPin(args.userId, args.currentPin, args.businessId);
107
+ if (!valid)
108
+ throw new Error("Current PIN is incorrect");
109
+ }
110
+ await updateOfflineMemberPin(args.userId, args.businessId, args.newPin);
111
+ const updated = await getOfflineMember(args.userId, args.businessId);
112
+ if (!updated)
113
+ throw new Error("Could not update the offline PIN");
114
+ return updated;
115
+ }
116
+ if (!args.member) {
117
+ throw new Error("Sign in online once before setting up an offline PIN");
118
+ }
119
+ return createOfflineMemberCredential({
120
+ businessId: args.businessId,
121
+ userId: args.userId,
122
+ fullName: args.member.fullName,
123
+ email: args.member.email,
124
+ role: args.member.role,
125
+ capabilities: args.member.capabilities,
126
+ pin: args.newPin,
127
+ });
81
128
  }
82
129
  /** Refresh the signed license while online using the normal access token. */
83
130
  export async function refreshOfflineLicense(businessId) {
@@ -70,6 +70,8 @@ export declare function createOfflineMemberCredential(args: {
70
70
  capabilities: string[];
71
71
  pin: string;
72
72
  }): Promise<OfflineMemberCredential>;
73
+ /** Replace an existing member's PIN without changing their role/capabilities. */
74
+ export declare function updateOfflineMemberPin(userId: string, businessId: string, pin: string): Promise<void>;
73
75
  export declare const OFFLINE_PIN_MAX_ATTEMPTS = 5;
74
76
  export interface OfflinePinLockState {
75
77
  failedAttempts: number;
@@ -201,6 +201,23 @@ export async function createOfflineMemberCredential(args) {
201
201
  await saveOfflineMember(member);
202
202
  return member;
203
203
  }
204
+ /** Replace an existing member's PIN without changing their role/capabilities. */
205
+ export async function updateOfflineMemberPin(userId, businessId, pin) {
206
+ if (!/^\d{6}$/.test(pin)) {
207
+ throw new Error("Offline PIN must be exactly 6 digits");
208
+ }
209
+ const member = await getOfflineMember(userId, businessId);
210
+ if (!member)
211
+ throw new Error("This member has no offline credential on this device");
212
+ const salt = randomHex(32);
213
+ await saveOfflineMember({
214
+ ...member,
215
+ pin_salt: salt,
216
+ pin_hash: await hashPin(pin, salt),
217
+ failed_attempts: 0,
218
+ locked_until: null,
219
+ });
220
+ }
204
221
  export const OFFLINE_PIN_MAX_ATTEMPTS = 5;
205
222
  export async function getOfflinePinLockState(userId, businessId) {
206
223
  const member = await getOfflineMember(userId, businessId);
@@ -4,6 +4,25 @@
4
4
  * KV persists device_number per businessId.
5
5
  */
6
6
  import { getAppInstanceIdAsync } from "../adapters/devices";
7
+ /**
8
+ * Why the last device-lease attempt failed. Surfaced by startup readiness so a
9
+ * plan/terminal limit is not reported as "offline data incomplete".
10
+ */
11
+ export interface DeviceLeaseFailure {
12
+ code: "TERMINAL_LIMIT_REACHED" | "DEVICE_LIMIT_REACHED" | "AUTH_REQUIRED" | "OFFLINE" | "LEASE_FAILED";
13
+ message: string;
14
+ status?: number;
15
+ limit?: number;
16
+ current?: number;
17
+ }
18
+ export declare function getLastLeaseFailure(businessId: string): DeviceLeaseFailure | null;
19
+ /**
20
+ * Normalise the HTTP-client error into a user-presentable lease failure.
21
+ * Desktop injects its own ApiError shape; mobile/core may inject another, so
22
+ * inspect both the error and the backend `{ error: { code, message, details } }`
23
+ * envelope instead of relying on one class.
24
+ */
25
+ export declare function classifyDeviceLeaseFailure(error: unknown): DeviceLeaseFailure;
7
26
  export declare function getDeviceNumber(businessId: string): string | null;
8
27
  export declare function isOnlineOnly(businessId: string): boolean;
9
28
  export declare function ensureLease(businessId: string): Promise<string | null>;
@@ -10,6 +10,80 @@ import { getAppInstanceIdAsync } from "../adapters/devices";
10
10
  import { getConnectivityState } from "../adapters/connectivity";
11
11
  const LEASE_KEY = (businessId) => `teinc_device_number:${businessId}`;
12
12
  const LEASE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
13
+ const leaseFailures = new Map();
14
+ export function getLastLeaseFailure(businessId) {
15
+ return leaseFailures.get(businessId) ?? null;
16
+ }
17
+ function rememberLeaseFailure(businessId, failure) {
18
+ leaseFailures.set(businessId, failure);
19
+ }
20
+ function clearLeaseFailure(businessId) {
21
+ leaseFailures.delete(businessId);
22
+ }
23
+ function asRecord(value) {
24
+ return value !== null && typeof value === "object" && !Array.isArray(value)
25
+ ? value
26
+ : null;
27
+ }
28
+ function asNumber(value) {
29
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
30
+ }
31
+ /**
32
+ * Normalise the HTTP-client error into a user-presentable lease failure.
33
+ * Desktop injects its own ApiError shape; mobile/core may inject another, so
34
+ * inspect both the error and the backend `{ error: { code, message, details } }`
35
+ * envelope instead of relying on one class.
36
+ */
37
+ export function classifyDeviceLeaseFailure(error) {
38
+ const e = (asRecord(error) ?? {});
39
+ const responseData = asRecord(e.data) ?? asRecord(e.response?.data);
40
+ const errorBody = asRecord(responseData?.error);
41
+ const details = asRecord(errorBody?.details);
42
+ const status = asNumber(e.status);
43
+ const code = typeof errorBody?.code === "string"
44
+ ? errorBody.code
45
+ : typeof e.code === "string"
46
+ ? e.code
47
+ : undefined;
48
+ const message = typeof errorBody?.message === "string"
49
+ ? errorBody.message
50
+ : typeof e.message === "string"
51
+ ? e.message
52
+ : "";
53
+ const limit = asNumber(details?.limit);
54
+ const current = asNumber(details?.current);
55
+ if (status === 402 || code === "TERMINAL_LIMIT_REACHED") {
56
+ return {
57
+ code: "TERMINAL_LIMIT_REACHED",
58
+ message: message ||
59
+ "This business has reached the number of linked terminals allowed by its plan.",
60
+ status,
61
+ limit,
62
+ current,
63
+ };
64
+ }
65
+ if (status === 429 || code === "DEVICE_LIMIT_REACHED") {
66
+ return {
67
+ code: "DEVICE_LIMIT_REACHED",
68
+ message: message || "This business has reached the maximum number of linked terminals.",
69
+ status,
70
+ };
71
+ }
72
+ if (status === 401 ||
73
+ message.includes("TOKEN_MISSING") ||
74
+ message.includes("No authentication token")) {
75
+ return {
76
+ code: "AUTH_REQUIRED",
77
+ message: message || "Sign-in is still finishing. Retry in a moment.",
78
+ status,
79
+ };
80
+ }
81
+ return {
82
+ code: "LEASE_FAILED",
83
+ message: message || "The terminal could not be activated.",
84
+ status,
85
+ };
86
+ }
13
87
  export function getDeviceNumber(businessId) {
14
88
  const v = getSync(LEASE_KEY(businessId));
15
89
  if (v && /^(0[1-9]|[1-9][0-9])$/.test(v))
@@ -23,6 +97,7 @@ export async function ensureLease(businessId) {
23
97
  // Offline-first: keep leased DD even offline — if online check/lease fails, return cached
24
98
  const existingSync = getDeviceNumber(businessId);
25
99
  if (existingSync) {
100
+ clearLeaseFailure(businessId);
26
101
  // Opportunistically refresh touch if online, but never discard cached on failure
27
102
  if (getConnectivityState().isOnline) {
28
103
  try {
@@ -48,6 +123,7 @@ export async function ensureLease(businessId) {
48
123
  setSync(LEASE_KEY(businessId), asyncCached);
49
124
  }
50
125
  catch { }
126
+ clearLeaseFailure(businessId);
51
127
  return asyncCached;
52
128
  }
53
129
  }
@@ -55,13 +131,18 @@ export async function ensureLease(businessId) {
55
131
  // No cached DD — must lease online. If offline, keep null (caller blocks POS with clear message)
56
132
  if (!getConnectivityState().isOnline) {
57
133
  syncLogger.debug("DeviceLease", "Offline and no cached DD — need online once", { businessId });
134
+ rememberLeaseFailure(businessId, {
135
+ code: "OFFLINE",
136
+ message: "Connect to the internet once so this terminal can be activated.",
137
+ });
58
138
  return null;
59
139
  }
60
140
  try {
61
141
  const http = getHttpClient();
62
142
  const deviceUuid = await getAppInstanceIdAsync();
63
143
  const res = await http.post(`/businesses/${businessId}/devices/lease`, { device_uuid: deviceUuid, app_instance_id: deviceUuid });
64
- const data = res.data?.data ?? res.data;
144
+ const payload = res.data;
145
+ const data = payload.data ?? payload;
65
146
  const dn = data.device_number;
66
147
  if (dn && /^(0[1-9]|[1-9][0-9])$/.test(dn)) {
67
148
  setSync(LEASE_KEY(businessId), dn);
@@ -70,51 +151,57 @@ export async function ensureLease(businessId) {
70
151
  await getItem(LEASE_KEY(businessId));
71
152
  }
72
153
  catch { }
154
+ clearLeaseFailure(businessId);
73
155
  syncLogger.info("DeviceLease", "Leased device number", { businessId, device_number: dn });
74
156
  return dn;
75
157
  }
158
+ rememberLeaseFailure(businessId, {
159
+ code: "LEASE_FAILED",
160
+ message: "The server did not return a terminal number. Try again.",
161
+ });
76
162
  }
77
163
  catch (e) {
78
- const apiErr = e;
79
- const msg = String(e?.message ?? "");
80
- const isAuth = apiErr?.status === 401 || msg.includes("TOKEN_MISSING") || msg.includes("No authentication token");
81
- if (isAuth) {
82
- syncLogger.debug("DeviceLease", "No auth yet — lease deferred until login", { businessId });
83
- // fallback to any async cache that may have appeared concurrently
84
- try {
85
- const fallback = await getItem(LEASE_KEY(businessId));
86
- if (fallback && /^(0[1-9]|[1-9][0-9])$/.test(fallback))
87
- return fallback;
88
- }
89
- catch { }
90
- const again = getDeviceNumber(businessId);
91
- if (again)
92
- return again;
93
- return null;
164
+ const failure = classifyDeviceLeaseFailure(e);
165
+ rememberLeaseFailure(businessId, failure);
166
+ if (failure.code === "TERMINAL_LIMIT_REACHED" || failure.code === "DEVICE_LIMIT_REACHED") {
167
+ syncLogger.warn("DeviceLease", "Terminal limit reached — activation needs a free slot", {
168
+ businessId,
169
+ code: failure.code,
170
+ limit: failure.limit,
171
+ current: failure.current,
172
+ });
173
+ return cachedLeaseFallback(businessId);
94
174
  }
95
- if (apiErr?.status === 429 || msg.includes("DEVICE_LIMIT_REACHED")) {
96
- syncLogger.warn("DeviceLease", "Device limit reached — online only", { businessId });
97
- return null;
175
+ if (failure.code === "AUTH_REQUIRED") {
176
+ syncLogger.debug("DeviceLease", "No auth yet — lease deferred until login", { businessId });
177
+ return cachedLeaseFallback(businessId);
98
178
  }
99
- // Network/offline failure while leasing — keep any cached that may exist
179
+ // Network/offline failure while leasing — keep any cached that may exist.
100
180
  syncLogger.warn("DeviceLease", "Lease failed — keep cached if any", {
101
181
  businessId,
102
182
  error: String(e),
103
183
  });
104
- try {
105
- const fallback = await getItem(LEASE_KEY(businessId));
106
- if (fallback && /^(0[1-9]|[1-9][0-9])$/.test(fallback)) {
107
- try {
108
- setSync(LEASE_KEY(businessId), fallback);
109
- }
110
- catch { }
111
- return fallback;
184
+ return cachedLeaseFallback(businessId);
185
+ }
186
+ return cachedLeaseFallback(businessId);
187
+ }
188
+ async function cachedLeaseFallback(businessId) {
189
+ try {
190
+ const fallback = await getItem(LEASE_KEY(businessId));
191
+ if (fallback && /^(0[1-9]|[1-9][0-9])$/.test(fallback)) {
192
+ try {
193
+ setSync(LEASE_KEY(businessId), fallback);
112
194
  }
195
+ catch { }
196
+ clearLeaseFailure(businessId);
197
+ return fallback;
113
198
  }
114
- catch { }
115
- const again = getDeviceNumber(businessId);
116
- if (again)
117
- return again;
199
+ }
200
+ catch { }
201
+ const again = getDeviceNumber(businessId);
202
+ if (again) {
203
+ clearLeaseFailure(businessId);
204
+ return again;
118
205
  }
119
206
  return null;
120
207
  }
@@ -141,6 +228,7 @@ export async function refreshLease(businessId) {
141
228
  export function clearLeaseForTests(businessId) {
142
229
  removeItemSync(LEASE_KEY(businessId));
143
230
  removeItemSync(`${LEASE_KEY(businessId)}:leased_at`);
231
+ clearLeaseFailure(businessId);
144
232
  }
145
233
  export const deviceLeaseService = {
146
234
  getDeviceNumber,
@@ -11,6 +11,8 @@
11
11
  *
12
12
  * Seller-only. Fleet/rider surfaces are fully online and never gate here.
13
13
  */
14
+ import { type DeviceLeaseFailure } from "./device-lease.service";
15
+ import { type ConnectivityState } from "../adapters/connectivity";
14
16
  import type { ReferenceDataKey } from "../reference/types";
15
17
  /**
16
18
  * Reference keys required before the app presents itself as ready to take
@@ -31,6 +33,13 @@ export interface StartupReadiness {
31
33
  * by itself block readiness.
32
34
  */
33
35
  entitlementUnverified: boolean;
36
+ /**
37
+ * Why the terminal lease failed, when `needsLease` is true. Lets the UI show
38
+ * "terminal limit reached" instead of the generic offline-data message.
39
+ */
40
+ leaseError: DeviceLeaseFailure | null;
41
+ /** Connectivity snapshot used for this check. */
42
+ network: ConnectivityState;
34
43
  }
35
44
  /** Evaluate the local cache only (no network). */
36
45
  export declare function getStartupReadiness(businessId: string): Promise<StartupReadiness>;
@@ -15,7 +15,7 @@ import { openSyncDatabase } from "../db/connection";
15
15
  import { getReferenceData } from "../reference/operations";
16
16
  import { referenceSyncService } from "../reference/service";
17
17
  import { evaluateBusinessEntitlements } from "../reference/entitlement-guard";
18
- import { ensureLease } from "./device-lease.service";
18
+ import { ensureLease, getLastLeaseFailure, } from "./device-lease.service";
19
19
  import { getConnectivityState } from "../adapters/connectivity";
20
20
  import { syncLogger } from "../utils/sync-logger";
21
21
  /**
@@ -52,6 +52,7 @@ export async function getStartupReadiness(businessId) {
52
52
  syncLogger.warn("StartupReadiness", "lease check failed", { err: String(e) });
53
53
  }
54
54
  const needsLease = !deviceNumber;
55
+ const leaseError = needsLease ? getLastLeaseFailure(businessId) : null;
55
56
  let entitlementUnverified = missingKeys.includes("subscription:entitlements");
56
57
  if (!entitlementUnverified) {
57
58
  try {
@@ -67,6 +68,8 @@ export async function getStartupReadiness(businessId) {
67
68
  missingKeys,
68
69
  needsLease,
69
70
  entitlementUnverified,
71
+ leaseError,
72
+ network: getConnectivityState(),
70
73
  };
71
74
  }
72
75
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@teincfood/core",
3
- "version": "0.10.18",
4
- "description": "TeincFood shared offline-first core \u2014 types, sync engine, local DB, reference data, and repositories for mobile + desktop",
3
+ "version": "0.10.20",
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",
7
7
  "types": "dist/index.d.ts",