qumra-pos-auth 0.1.0

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.cjs ADDED
@@ -0,0 +1,706 @@
1
+ 'use strict';
2
+
3
+ var argon2 = require('@noble/hashes/argon2');
4
+ var utils = require('@noble/hashes/utils');
5
+ var ed25519 = require('@noble/curves/ed25519');
6
+
7
+ // src/crypto/NoblePinHasher.ts
8
+
9
+ // src/crypto/base64.ts
10
+ var STD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
11
+ var URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
12
+ function encode(bytes, alphabet) {
13
+ let out = "";
14
+ for (let i = 0; i < bytes.length; i += 3) {
15
+ const b0 = bytes[i];
16
+ const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
17
+ const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
18
+ const triple = b0 << 16 | b1 << 8 | b2;
19
+ out += alphabet[triple >> 18 & 63];
20
+ out += alphabet[triple >> 12 & 63];
21
+ if (i + 1 < bytes.length) out += alphabet[triple >> 6 & 63];
22
+ if (i + 2 < bytes.length) out += alphabet[triple & 63];
23
+ }
24
+ return out;
25
+ }
26
+ function decode(str, alphabet) {
27
+ const lookup = new Int16Array(128).fill(-1);
28
+ for (let i = 0; i < alphabet.length; i++) lookup[alphabet.charCodeAt(i)] = i;
29
+ const clean = str.replace(/[=\s]/g, "");
30
+ const out = new Uint8Array(Math.floor(clean.length * 6 / 8));
31
+ let bits = 0;
32
+ let value = 0;
33
+ let o = 0;
34
+ for (let i = 0; i < clean.length; i++) {
35
+ const c = clean.charCodeAt(i);
36
+ const idx = c < 128 ? lookup[c] : -1;
37
+ if (idx < 0) throw new Error("base64: \u062D\u0631\u0641 \u063A\u064A\u0631 \u0635\u0627\u0644\u062D");
38
+ value = value << 6 | idx;
39
+ bits += 6;
40
+ if (bits >= 8) {
41
+ bits -= 8;
42
+ out[o++] = value >> bits & 255;
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+ var b64 = {
48
+ encode: (b) => encode(b, STD),
49
+ decode: (s) => decode(s, STD)
50
+ };
51
+ var b64url = {
52
+ encode: (b) => encode(b, URL),
53
+ decode: (s) => decode(s, URL)
54
+ };
55
+
56
+ // src/crypto/NoblePinHasher.ts
57
+ var DEFAULT_ARGON2_PARAMS = {
58
+ t: 3,
59
+ m: 64 * 1024,
60
+ p: 1,
61
+ dkLen: 32
62
+ };
63
+ var V = 19;
64
+ function constantTimeEqual(a, b) {
65
+ if (a.length !== b.length) return false;
66
+ let diff = 0;
67
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
68
+ return diff === 0;
69
+ }
70
+ var NoblePinHasher = class {
71
+ params;
72
+ constructor(params = DEFAULT_ARGON2_PARAMS) {
73
+ this.params = params;
74
+ }
75
+ async hash(pin) {
76
+ const salt = utils.randomBytes(16);
77
+ const { t, m, p, dkLen } = this.params;
78
+ const out = await argon2.argon2idAsync(utils.utf8ToBytes(pin), salt, { t, m, p, dkLen });
79
+ return `$argon2id$v=${V}$m=${m},t=${t},p=${p}$${b64.encode(salt)}$${b64.encode(out)}`;
80
+ }
81
+ async verify(pin, encoded) {
82
+ const parsed = parsePhc(encoded);
83
+ if (!parsed) return false;
84
+ const out = await argon2.argon2idAsync(utils.utf8ToBytes(pin), parsed.salt, {
85
+ t: parsed.t,
86
+ m: parsed.m,
87
+ p: parsed.p,
88
+ dkLen: parsed.hash.length
89
+ });
90
+ return constantTimeEqual(out, parsed.hash);
91
+ }
92
+ };
93
+ function parsePhc(encoded) {
94
+ const parts = encoded.split("$");
95
+ if (parts.length !== 6) return null;
96
+ if (parts[1] !== "argon2id") return null;
97
+ const paramStr = parts[3];
98
+ const saltStr = parts[4];
99
+ const hashStr = parts[5];
100
+ if (paramStr === void 0 || saltStr === void 0 || hashStr === void 0) {
101
+ return null;
102
+ }
103
+ const params = /* @__PURE__ */ new Map();
104
+ for (const kv of paramStr.split(",")) {
105
+ const [k, v] = kv.split("=");
106
+ if (k === void 0 || v === void 0) return null;
107
+ const n = Number(v);
108
+ if (!Number.isFinite(n)) return null;
109
+ params.set(k, n);
110
+ }
111
+ const m = params.get("m");
112
+ const t = params.get("t");
113
+ const p = params.get("p");
114
+ if (m === void 0 || t === void 0 || p === void 0) return null;
115
+ try {
116
+ return { m, t, p, salt: b64.decode(saltStr), hash: b64.decode(hashStr) };
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+ var NobleSignatureVerifier = class {
122
+ async verify(message, signature, publicKey) {
123
+ try {
124
+ return ed25519.ed25519.verify(signature, message, publicKey);
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+ };
130
+
131
+ // src/errors.ts
132
+ var AuthNetworkError = class extends Error {
133
+ constructor(message, cause) {
134
+ super(message, { cause });
135
+ this.name = "AuthNetworkError";
136
+ }
137
+ };
138
+ var AuthRejectedError = class extends Error {
139
+ /** كود السيرفر لو متاح (invalid_grant, invalid_credentials …). */
140
+ code;
141
+ constructor(message, code, cause) {
142
+ super(message, { cause });
143
+ this.name = "AuthRejectedError";
144
+ this.code = code;
145
+ }
146
+ };
147
+
148
+ // src/time.ts
149
+ var systemClock = () => Date.now();
150
+ var SECOND = 1e3;
151
+ var MINUTE = 60 * SECOND;
152
+ var HOUR = 60 * MINUTE;
153
+ var DAY = 24 * HOUR;
154
+
155
+ // src/token/TokenManager.ts
156
+ var TokenManager = class {
157
+ vault;
158
+ api;
159
+ now;
160
+ skewMs;
161
+ onServerContact;
162
+ /** single-flight: كل النداءات المتزامنة بتشارك نفس عملية الـ refresh. */
163
+ inflight = null;
164
+ /**
165
+ * نسخة في الذاكرة من آخر توكنات معروفة. الغرض الوحيد: peekValidToken المتزامن
166
+ * — لأن SyncEngine.getAuthToken متزامن بينما الـ vault async. بتتحدّث مع كل
167
+ * تحميل/حفظ للتوكنات، ودورة الـ refresh الخلفية بتحافظ عليها دافية.
168
+ */
169
+ cached = null;
170
+ constructor(opts) {
171
+ this.vault = opts.vault;
172
+ this.api = opts.api;
173
+ this.now = opts.now ?? systemClock;
174
+ this.skewMs = opts.skewMs ?? 30 * SECOND;
175
+ this.onServerContact = opts.onServerContact;
176
+ }
177
+ /** تسجيل دخول أونلاين. بيرمي AuthRejectedError/AuthNetworkError زي الـ api. */
178
+ async login(input) {
179
+ const resp = await this.api.login(input);
180
+ const tokens = this.toTokens(resp);
181
+ await this.vault.saveTokens(tokens);
182
+ this.cached = tokens;
183
+ await this.onServerContact?.(this.now());
184
+ return tokens;
185
+ }
186
+ /** خروج: إبطال best-effort على السيرفر ثم مسح التوكنات المحلية. */
187
+ async logout() {
188
+ const tokens = await this.vault.loadTokens();
189
+ if (tokens && this.api.revoke) {
190
+ try {
191
+ await this.api.revoke(tokens.refreshToken);
192
+ } catch {
193
+ }
194
+ }
195
+ await this.vault.saveTokens(null);
196
+ this.cached = null;
197
+ }
198
+ async getTokens() {
199
+ const tokens = await this.vault.loadTokens();
200
+ this.cached = tokens;
201
+ return tokens;
202
+ }
203
+ /** يملأ الكاش من الـ vault مرة (عند الإقلاع) — علشان peek يشتغل قبل أي نداء. */
204
+ async primeCache() {
205
+ this.cached = await this.vault.loadTokens();
206
+ }
207
+ /**
208
+ * توكن access صالح متزامن من الكاش — من غير أي I/O أو refresh. ده اللي بيتوصّل
209
+ * لـ SyncEngine.getAuthToken. بيرجّع null لو مفيش كاش أو الكاش منتهي؛ دورة الـ
210
+ * refresh الخلفية (getValidToken) هي اللي بتجدّد الكاش.
211
+ */
212
+ peekValidToken() {
213
+ if (!this.cached) return null;
214
+ return this.isAccessValid(this.cached) ? this.cached.accessToken : null;
215
+ }
216
+ /**
217
+ * التوكن الصالح للـ SyncEngine. مابيرميش: أي فشل → null (= pause).
218
+ */
219
+ async getValidToken() {
220
+ const outcome = await this.ensureFresh();
221
+ switch (outcome.status) {
222
+ case "valid":
223
+ case "refreshed":
224
+ return outcome.tokens.accessToken;
225
+ default:
226
+ return null;
227
+ }
228
+ }
229
+ /**
230
+ * يضمن توكن access صالح: يرجّعه لو صالح، أو يعمل refresh دوّار single-flight.
231
+ * مابيرميش — بيرجّع RefreshOutcome. AuthService بيستخدمه لتحديث حالة الوصول.
232
+ */
233
+ async ensureFresh() {
234
+ const tokens = await this.vault.loadTokens();
235
+ this.cached = tokens;
236
+ if (!tokens) return { status: "signed-out" };
237
+ if (this.isAccessValid(tokens)) return { status: "valid", tokens };
238
+ if (!this.isRefreshValid(tokens)) return { status: "network" };
239
+ return this.refreshOnce();
240
+ }
241
+ isAccessValid(tokens) {
242
+ return this.now() < tokens.accessExpiresAt - this.skewMs;
243
+ }
244
+ isRefreshValid(tokens) {
245
+ return this.now() < tokens.refreshExpiresAt;
246
+ }
247
+ /** refresh single-flight: نداء واحد بس بيطير حتى لو اتنادى من كذا مكان. */
248
+ refreshOnce() {
249
+ if (this.inflight) return this.inflight;
250
+ this.inflight = this.doRefresh().finally(() => {
251
+ this.inflight = null;
252
+ });
253
+ return this.inflight;
254
+ }
255
+ async doRefresh() {
256
+ const current = await this.vault.loadTokens();
257
+ if (!current) return { status: "signed-out" };
258
+ if (this.isAccessValid(current)) return { status: "valid", tokens: current };
259
+ try {
260
+ const resp = await this.api.refresh(current.refreshToken);
261
+ const next = this.toTokens(resp);
262
+ await this.vault.saveTokens(next);
263
+ this.cached = next;
264
+ await this.onServerContact?.(this.now());
265
+ return { status: "refreshed", tokens: next };
266
+ } catch (err) {
267
+ if (err instanceof AuthRejectedError) {
268
+ await this.vault.saveTokens(null);
269
+ this.cached = null;
270
+ return { status: "rejected" };
271
+ }
272
+ if (err instanceof AuthNetworkError) {
273
+ return { status: "network" };
274
+ }
275
+ return { status: "network" };
276
+ }
277
+ }
278
+ toTokens(resp) {
279
+ const at = this.now();
280
+ return {
281
+ accessToken: resp.accessToken,
282
+ refreshToken: resp.refreshToken,
283
+ accessExpiresAt: at + resp.expiresInSec * SECOND,
284
+ refreshExpiresAt: at + resp.refreshExpiresInSec * SECOND,
285
+ tokenType: resp.tokenType ?? "Bearer"
286
+ };
287
+ }
288
+ };
289
+
290
+ // src/pin/PinManager.ts
291
+ var PinManager = class {
292
+ directory;
293
+ hasher;
294
+ now;
295
+ maxAttempts;
296
+ lockoutMs;
297
+ attempts = /* @__PURE__ */ new Map();
298
+ constructor(opts) {
299
+ this.directory = opts.directory;
300
+ this.hasher = opts.hasher;
301
+ this.now = opts.now ?? systemClock;
302
+ this.maxAttempts = opts.maxAttempts ?? 5;
303
+ this.lockoutMs = opts.lockoutMs ?? MINUTE;
304
+ }
305
+ /** قائمة الكاشيرية للعرض (من غير الـ hashes). */
306
+ async listCashiers() {
307
+ const rows = await this.directory.listCashiers();
308
+ return rows.map((c) => ({ id: c.id, name: c.name, active: c.active }));
309
+ }
310
+ /**
311
+ * يتحقق من PIN كاشير معيّن. النجاح بيصفّر عدّاد محاولاته؛ الفشل بيزوّده ويقفل
312
+ * مؤقتاً عند الوصول للحد.
313
+ */
314
+ async verify(cashierId, pin) {
315
+ const now = this.now();
316
+ const state = this.attempts.get(cashierId);
317
+ if (state && state.lockedUntil > now) {
318
+ return {
319
+ ok: false,
320
+ reason: "locked",
321
+ retryAfterMs: state.lockedUntil - now
322
+ };
323
+ }
324
+ const cashier = await this.findCashier(cashierId);
325
+ if (!cashier) return { ok: false, reason: "unknown-cashier" };
326
+ if (!cashier.active) return { ok: false, reason: "inactive" };
327
+ const valid = await this.hasher.verify(pin, cashier.pinHash);
328
+ if (valid) {
329
+ this.attempts.delete(cashierId);
330
+ return {
331
+ ok: true,
332
+ cashier: { cashierId: cashier.id, name: cashier.name, since: now }
333
+ };
334
+ }
335
+ return this.registerFailure(cashierId, now);
336
+ }
337
+ /** يفكّ القفل يدوياً (مثلاً المدير سمح). */
338
+ reset(cashierId) {
339
+ this.attempts.delete(cashierId);
340
+ }
341
+ registerFailure(cashierId, now) {
342
+ const prev = this.attempts.get(cashierId);
343
+ const count = (prev?.count ?? 0) + 1;
344
+ if (count >= this.maxAttempts) {
345
+ const lockedUntil = now + this.lockoutMs;
346
+ this.attempts.set(cashierId, { count: 0, lockedUntil });
347
+ return { ok: false, reason: "locked", retryAfterMs: this.lockoutMs };
348
+ }
349
+ this.attempts.set(cashierId, { count, lockedUntil: 0 });
350
+ return {
351
+ ok: false,
352
+ reason: "bad-pin",
353
+ attemptsLeft: this.maxAttempts - count
354
+ };
355
+ }
356
+ async findCashier(id) {
357
+ const rows = await this.directory.listCashiers();
358
+ return rows.find((c) => c.id === id);
359
+ }
360
+ };
361
+
362
+ // src/activation/ActivationManager.ts
363
+ var ActivationManager = class {
364
+ vault;
365
+ verifier;
366
+ publicKey;
367
+ now;
368
+ deviceId;
369
+ constructor(opts) {
370
+ this.vault = opts.vault;
371
+ this.verifier = opts.verifier;
372
+ this.publicKey = opts.publicKey;
373
+ this.now = opts.now ?? systemClock;
374
+ this.deviceId = opts.deviceId;
375
+ }
376
+ /**
377
+ * يتحقق من مفتاح تفعيل ويخزّنه لو صالح. الترتيب: صيغة → توقيع → الجهاز → الانتهاء.
378
+ * التوقيع بيتفحص قبل أي حاجة تانية علشان مانثقش في أي claim قبل التأكد إنه من قمرة.
379
+ */
380
+ async activate(activationKey) {
381
+ const parsed = this.parse(activationKey);
382
+ if (!parsed) return { ok: false, reason: "malformed" };
383
+ const okSig = await this.verifier.verify(
384
+ parsed.signedBytes,
385
+ parsed.signature,
386
+ this.publicKey
387
+ );
388
+ if (!okSig) return { ok: false, reason: "bad-signature" };
389
+ const claims = parsed.claims;
390
+ if (this.deviceId !== void 0 && claims.deviceId !== this.deviceId) {
391
+ return { ok: false, reason: "wrong-device" };
392
+ }
393
+ const now = this.now();
394
+ const graceMs = claims.graceMs ?? 0;
395
+ if (now >= claims.expiresAt + graceMs) {
396
+ return { ok: false, reason: "expired" };
397
+ }
398
+ const stored = {
399
+ claims,
400
+ key: activationKey,
401
+ verifiedAt: now
402
+ };
403
+ await this.vault.saveActivation(stored);
404
+ return { ok: true, claims };
405
+ }
406
+ async getActivation() {
407
+ return this.vault.loadActivation();
408
+ }
409
+ /**
410
+ * يعيد التحقق من المفتاح المخزّن (توقيع + جهاز) ويحدّث verifiedAt. بيُستدعى عند
411
+ * الإقلاع علشان نتأكد إن المفتاح المخزّن لسه سليم ومش متلاعب فيه.
412
+ */
413
+ async revalidateStored() {
414
+ const stored = await this.vault.loadActivation();
415
+ if (!stored) return null;
416
+ return this.activate(stored.key);
417
+ }
418
+ async clear() {
419
+ await this.vault.saveActivation(null);
420
+ }
421
+ parse(key) {
422
+ const dot = key.indexOf(".");
423
+ if (dot <= 0 || dot === key.length - 1) return null;
424
+ const payloadB64 = key.slice(0, dot);
425
+ const sigB64 = key.slice(dot + 1);
426
+ let claims;
427
+ let signature;
428
+ try {
429
+ const json = new TextDecoder().decode(b64url.decode(payloadB64));
430
+ claims = JSON.parse(json);
431
+ signature = b64url.decode(sigB64);
432
+ } catch {
433
+ return null;
434
+ }
435
+ if (!isValidClaims(claims)) return null;
436
+ const signedBytes = new TextEncoder().encode(payloadB64);
437
+ return { claims, signedBytes, signature };
438
+ }
439
+ };
440
+ function isValidClaims(c) {
441
+ if (typeof c !== "object" || c === null) return false;
442
+ const o = c;
443
+ return typeof o.merchantId === "string" && typeof o.deviceId === "string" && typeof o.issuedAt === "number" && typeof o.expiresAt === "number";
444
+ }
445
+
446
+ // src/policy.ts
447
+ var DEFAULT_ACCESS_POLICY = {
448
+ maxOfflineMs: 30 * DAY,
449
+ graceMs: 3 * DAY
450
+ };
451
+
452
+ // src/service/AuthService.ts
453
+ var EMPTY_META = {
454
+ lastServerContactAt: null,
455
+ activatedOnlineAt: null
456
+ };
457
+ var AuthService = class {
458
+ vault;
459
+ now;
460
+ policy;
461
+ tokens;
462
+ pins;
463
+ activation;
464
+ listeners = /* @__PURE__ */ new Set();
465
+ status = {
466
+ mode: "unactivated",
467
+ access: "locked",
468
+ tokenValid: false,
469
+ cashier: null,
470
+ graceEndsAt: null,
471
+ lockedReason: LOCK_REASON_UNACTIVATED,
472
+ lastServerContactAt: null,
473
+ activation: null
474
+ };
475
+ constructor(opts) {
476
+ this.vault = opts.vault;
477
+ this.tokens = opts.tokenManager;
478
+ this.pins = opts.pinManager;
479
+ this.activation = opts.activationManager;
480
+ this.policy = opts.policy ?? DEFAULT_ACCESS_POLICY;
481
+ this.now = opts.now ?? systemClock;
482
+ }
483
+ // -------------------------------------------------------------------------
484
+ // إقلاع + إعادة تقييم
485
+ // -------------------------------------------------------------------------
486
+ /** الإقلاع: يملأ كاش التوكن، يعيد التحقق من مفتاح التفعيل المخزّن، ثم يحسب الحالة. */
487
+ async init() {
488
+ await this.tokens.primeCache();
489
+ const check = await this.activation.revalidateStored();
490
+ if (check && !check.ok && (check.reason === "bad-signature" || check.reason === "wrong-device")) {
491
+ await this.activation.clear();
492
+ }
493
+ return this.reevaluate();
494
+ }
495
+ /**
496
+ * يحاول تحديث التوكن (لو النت رجع) ثم يعيد حساب الحالة. التطبيق بينادي ده دورياً
497
+ * وبعد أي reconnect. مابيرميش.
498
+ */
499
+ async refresh() {
500
+ await this.tokens.ensureFresh();
501
+ return this.reevaluate();
502
+ }
503
+ getStatus() {
504
+ return this.status;
505
+ }
506
+ /** هل مسموح ببيع جديد دلوقتي؟ (locked = لأ). */
507
+ canSell() {
508
+ return this.status.access !== "locked";
509
+ }
510
+ onStatusChange(cb) {
511
+ this.listeners.add(cb);
512
+ cb(this.status);
513
+ return () => this.listeners.delete(cb);
514
+ }
515
+ // -------------------------------------------------------------------------
516
+ // الجهاز/التاجر
517
+ // -------------------------------------------------------------------------
518
+ /** تسجيل دخول أونلاين. بيرمي زي الـ api (rejected/network). */
519
+ async onlineLogin(input) {
520
+ await this.tokens.login(input);
521
+ return this.reevaluate();
522
+ }
523
+ /** إدخال مفتاح تفعيل أوفلاين. */
524
+ async activateOffline(activationKey) {
525
+ const result = await this.activation.activate(activationKey);
526
+ await this.reevaluate();
527
+ return result;
528
+ }
529
+ /** خروج التاجر: يمسح التوكنات وجلسة الكاشير (مش البيانات، مش التفعيل). */
530
+ async logout() {
531
+ await this.tokens.logout();
532
+ await this.vault.saveSession(null);
533
+ return this.reevaluate();
534
+ }
535
+ /** callback الوحيد للـ SyncEngine (async). null = pause. */
536
+ getValidToken() {
537
+ return this.tokens.getValidToken();
538
+ }
539
+ /**
540
+ * نسخة متزامنة للـ SyncEngine (getAuthToken بتاعه متزامن). بترجّع آخر توكن صالح
541
+ * من الكاش من غير I/O؛ refresh()/getValidToken الدورية هي اللي بتدفّي الكاش.
542
+ */
543
+ getAccessTokenSync() {
544
+ return this.tokens.peekValidToken();
545
+ }
546
+ /** يُمرَّر للـ TokenManager: يسجّل آخر اتصال ناجح بالسيرفر ويعلّم التفعيل الأونلاين. */
547
+ async recordServerContact(at) {
548
+ const meta = await this.vault.loadMeta() ?? EMPTY_META;
549
+ await this.vault.saveMeta({
550
+ lastServerContactAt: at,
551
+ activatedOnlineAt: meta.activatedOnlineAt ?? at
552
+ });
553
+ }
554
+ // -------------------------------------------------------------------------
555
+ // الكاشير (PIN)
556
+ // -------------------------------------------------------------------------
557
+ async listCashiers() {
558
+ return this.pins.listCashiers();
559
+ }
560
+ async cashierLogin(cashierId, pin) {
561
+ const result = await this.pins.verify(cashierId, pin);
562
+ if (result.ok) {
563
+ await this.vault.saveSession(result.cashier);
564
+ await this.reevaluate();
565
+ }
566
+ return result;
567
+ }
568
+ async cashierLogout() {
569
+ await this.vault.saveSession(null);
570
+ await this.reevaluate();
571
+ }
572
+ // -------------------------------------------------------------------------
573
+ // حساب الحالة
574
+ // -------------------------------------------------------------------------
575
+ async reevaluate() {
576
+ const next = await this.computeStatus();
577
+ const changed = JSON.stringify(next) !== JSON.stringify(this.status);
578
+ this.status = next;
579
+ if (changed) {
580
+ for (const cb of this.listeners) cb(next);
581
+ }
582
+ return next;
583
+ }
584
+ async computeStatus() {
585
+ const [tokens, session, activation, metaRaw] = await Promise.all([
586
+ this.vault.loadTokens(),
587
+ this.vault.loadSession(),
588
+ this.vault.loadActivation(),
589
+ this.vault.loadMeta()
590
+ ]);
591
+ const meta = metaRaw ?? EMPTY_META;
592
+ const now = this.now();
593
+ const tokenValid = tokens ? this.tokens.isAccessValid(tokens) : false;
594
+ const mode = deriveMode(tokenValid, meta, activation);
595
+ const access = computeAccess(mode, now, meta, activation, this.policy);
596
+ return {
597
+ mode,
598
+ access: access.state,
599
+ tokenValid,
600
+ cashier: session,
601
+ graceEndsAt: access.graceEndsAt,
602
+ lockedReason: access.lockedReason,
603
+ lastServerContactAt: meta.lastServerContactAt,
604
+ activation
605
+ };
606
+ }
607
+ };
608
+ var LOCK_REASON_UNACTIVATED = "\u0627\u0644\u062C\u0647\u0627\u0632 \u063A\u064A\u0631 \u0645\u0641\u0639\u0651\u0644 \u2014 \u0633\u062C\u0651\u0644 \u0627\u0644\u062F\u062E\u0648\u0644 \u0623\u0648\u0646\u0644\u0627\u064A\u0646 \u0623\u0648 \u0623\u062F\u062E\u0644 \u0645\u0641\u062A\u0627\u062D \u062A\u0641\u0639\u064A\u0644";
609
+ var LOCK_REASON_OFFLINE_TOO_LONG = "\u0627\u0644\u062C\u0647\u0627\u0632 \u0623\u0648\u0641\u0644\u0627\u064A\u0646 \u0645\u0646 \u0645\u062F\u0629 \u0637\u0648\u064A\u0644\u0629 \u2014 \u0644\u0627\u0632\u0645 \u064A\u062A\u0635\u0644 \u0628\u0627\u0644\u0633\u064A\u0631\u0641\u0631 \u0644\u062A\u062C\u062F\u064A\u062F \u0627\u0644\u062C\u0644\u0633\u0629";
610
+ var LOCK_REASON_ACTIVATION_EXPIRED = "\u0627\u0646\u062A\u0647\u062A \u0635\u0644\u0627\u062D\u064A\u0629 \u0645\u0641\u062A\u0627\u062D \u0627\u0644\u062A\u0641\u0639\u064A\u0644";
611
+ function deriveMode(tokenValid, meta, activation) {
612
+ if (tokenValid) return "online";
613
+ if (meta.activatedOnlineAt != null) return "offline-degraded";
614
+ if (activation != null) return "offline-activated";
615
+ return "unactivated";
616
+ }
617
+ function computeAccess(mode, now, meta, activation, policy) {
618
+ if (mode === "online") {
619
+ return { state: "active", graceEndsAt: null, lockedReason: null };
620
+ }
621
+ if (mode === "unactivated") {
622
+ return { state: "locked", graceEndsAt: null, lockedReason: LOCK_REASON_UNACTIVATED };
623
+ }
624
+ let allowanceEnd;
625
+ let graceMs;
626
+ let expiredReason;
627
+ if (mode === "offline-activated" && activation) {
628
+ allowanceEnd = activation.claims.expiresAt;
629
+ graceMs = activation.claims.graceMs ?? policy.graceMs;
630
+ expiredReason = LOCK_REASON_ACTIVATION_EXPIRED;
631
+ } else {
632
+ const anchor = meta.lastServerContactAt ?? meta.activatedOnlineAt ?? now;
633
+ allowanceEnd = anchor + policy.maxOfflineMs;
634
+ graceMs = policy.graceMs;
635
+ expiredReason = LOCK_REASON_OFFLINE_TOO_LONG;
636
+ }
637
+ if (now < allowanceEnd) {
638
+ return { state: "active", graceEndsAt: null, lockedReason: null };
639
+ }
640
+ const lockAt = allowanceEnd + graceMs;
641
+ if (now < lockAt) {
642
+ return { state: "grace", graceEndsAt: lockAt, lockedReason: null };
643
+ }
644
+ return { state: "locked", graceEndsAt: null, lockedReason: expiredReason };
645
+ }
646
+
647
+ // src/createAuth.ts
648
+ function createAuth(opts) {
649
+ const now = opts.now ?? systemClock;
650
+ const hasher = opts.hasher ?? new NoblePinHasher();
651
+ const verifier = opts.verifier ?? new NobleSignatureVerifier();
652
+ let service;
653
+ const tokens = new TokenManager({
654
+ vault: opts.vault,
655
+ api: opts.api,
656
+ now,
657
+ skewMs: opts.skewMs,
658
+ onServerContact: (at) => service.recordServerContact(at)
659
+ });
660
+ const pins = new PinManager({
661
+ directory: opts.directory,
662
+ hasher,
663
+ now,
664
+ maxAttempts: opts.maxPinAttempts,
665
+ lockoutMs: opts.pinLockoutMs
666
+ });
667
+ const activation = new ActivationManager({
668
+ vault: opts.vault,
669
+ verifier,
670
+ publicKey: opts.activationPublicKey,
671
+ now,
672
+ deviceId: opts.deviceId
673
+ });
674
+ service = new AuthService({
675
+ vault: opts.vault,
676
+ tokenManager: tokens,
677
+ pinManager: pins,
678
+ activationManager: activation,
679
+ policy: opts.policy,
680
+ now
681
+ });
682
+ return {
683
+ service,
684
+ tokens,
685
+ pins,
686
+ activation,
687
+ getValidToken: () => service.getValidToken(),
688
+ getAccessTokenSync: () => service.getAccessTokenSync()
689
+ };
690
+ }
691
+
692
+ exports.ActivationManager = ActivationManager;
693
+ exports.AuthNetworkError = AuthNetworkError;
694
+ exports.AuthRejectedError = AuthRejectedError;
695
+ exports.AuthService = AuthService;
696
+ exports.DAY = DAY;
697
+ exports.DEFAULT_ACCESS_POLICY = DEFAULT_ACCESS_POLICY;
698
+ exports.HOUR = HOUR;
699
+ exports.MINUTE = MINUTE;
700
+ exports.PinManager = PinManager;
701
+ exports.SECOND = SECOND;
702
+ exports.TokenManager = TokenManager;
703
+ exports.createAuth = createAuth;
704
+ exports.systemClock = systemClock;
705
+ //# sourceMappingURL=index.cjs.map
706
+ //# sourceMappingURL=index.cjs.map