@teincfood/core 0.10.19 → 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.
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teincfood/core",
3
- "version": "0.10.19",
3
+ "version": "0.10.20",
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",