mihrab-methods 1.0.0 → 1.0.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/README.md +91 -291
- package/certification.json +16 -5
- package/dist/cjs/authorities/algeria.d.ts +1 -1
- package/dist/cjs/authorities/indonesia.d.ts +1 -1
- package/dist/cjs/authorities/morocco.d.ts +1 -1
- package/dist/cjs/authorities/sweden.d.ts +1 -1
- package/dist/cjs/core/api.d.ts +1 -1
- package/dist/cjs/core/api.js +2 -1
- package/dist/cjs/core/clock.d.ts +8 -1
- package/dist/cjs/core/clock.js +29 -8
- package/dist/cjs/core/fetchLatest.d.ts +5 -1
- package/dist/cjs/core/fetchLatest.js +101 -46
- package/dist/cjs/core/registry.d.ts +1 -1
- package/dist/cjs/core/registry.js +5 -0
- package/dist/cjs/core/types.d.ts +4 -0
- package/dist/cjs/data/generated/algeriaCertification.js +1 -1
- package/dist/cjs/data/generated/diyanetCertification.js +1 -1
- package/dist/cjs/data/generated/indonesiaCertification.js +1 -1
- package/dist/cjs/data/generated/moroccoCertification.js +1 -1
- package/dist/cjs/data/generated/swedenCertification.js +1 -1
- package/dist/cjs/locate.js +1 -1
- package/dist/esm/authorities/algeria.d.ts +1 -1
- package/dist/esm/authorities/indonesia.d.ts +1 -1
- package/dist/esm/authorities/morocco.d.ts +1 -1
- package/dist/esm/authorities/sweden.d.ts +1 -1
- package/dist/esm/core/api.d.ts +1 -1
- package/dist/esm/core/api.js +2 -1
- package/dist/esm/core/clock.d.ts +8 -1
- package/dist/esm/core/clock.js +29 -8
- package/dist/esm/core/fetchLatest.d.ts +5 -1
- package/dist/esm/core/fetchLatest.js +99 -13
- package/dist/esm/core/registry.d.ts +1 -1
- package/dist/esm/core/registry.js +5 -0
- package/dist/esm/core/types.d.ts +4 -0
- package/dist/esm/data/generated/algeriaCertification.js +1 -1
- package/dist/esm/data/generated/diyanetCertification.js +1 -1
- package/dist/esm/data/generated/indonesiaCertification.js +1 -1
- package/dist/esm/data/generated/moroccoCertification.js +1 -1
- package/dist/esm/data/generated/swedenCertification.js +1 -1
- package/dist/esm/locate.js +1 -1
- package/package.json +3 -1
- package/schemas/certification.schema.json +119 -24
|
@@ -9,7 +9,7 @@ export type FetchOptions = {
|
|
|
9
9
|
baseUrl?: string;
|
|
10
10
|
/** The fetch to use (default: the global one). */
|
|
11
11
|
fetch?: typeof fetch;
|
|
12
|
-
/** SHA-256 of a UTF-8 string as lowercase hex; default: WebCrypto
|
|
12
|
+
/** SHA-256 of a UTF-8 string as lowercase hex; default: WebCrypto where there is one, else a built-in digest. */
|
|
13
13
|
sha256?: (text: string) => Promise<string>;
|
|
14
14
|
/** Put a newer list in force in this process (default true). */
|
|
15
15
|
apply?: boolean;
|
|
@@ -52,6 +52,10 @@ export type FetchResult =
|
|
|
52
52
|
have: ListSummary;
|
|
53
53
|
};
|
|
54
54
|
export declare function summarize(list: CorrectionsFile): ListSummary;
|
|
55
|
+
/** UTF-8 bytes of a string, without TextEncoder (not every runtime has it). */
|
|
56
|
+
export declare function utf8(text: string): Uint8Array;
|
|
57
|
+
/** SHA-256 (FIPS 180-4) of bytes. */
|
|
58
|
+
export declare function sha256(data: Uint8Array): Uint8Array;
|
|
55
59
|
/** Put a list in force for the process — one verified by `fetchLatestCorrections`, or restored from the app's own storage. */
|
|
56
60
|
export declare function applyCorrections(authority: Authority, list: CorrectionsFile, revision: number): boolean;
|
|
57
61
|
export declare function fetchLatestCorrections(opts: FetchOptions): Promise<FetchResult>;
|
|
@@ -1,40 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.DEFAULT_FEED_URL = void 0;
|
|
37
4
|
exports.summarize = summarize;
|
|
5
|
+
exports.utf8 = utf8;
|
|
6
|
+
exports.sha256 = sha256;
|
|
38
7
|
exports.applyCorrections = applyCorrections;
|
|
39
8
|
exports.fetchLatestCorrections = fetchLatestCorrections;
|
|
40
9
|
/**
|
|
@@ -52,20 +21,107 @@ function summarize(list) {
|
|
|
52
21
|
return { from: list.from, to: list.to, entries: Object.values(list.points).reduce((n, e) => n + e.length, 0) };
|
|
53
22
|
}
|
|
54
23
|
const same = (a, b) => a.from === b.from && a.to === b.to && a.entries === b.entries;
|
|
24
|
+
/**
|
|
25
|
+
* SHA-256 as lowercase hex. WebCrypto where the runtime has it (browsers,
|
|
26
|
+
* Node 18+, Deno, Bun); elsewhere — Hermes in React Native has none — the
|
|
27
|
+
* plain-JavaScript digest below. No `node:crypto` import: a bundler for a
|
|
28
|
+
* phone or a browser fails on the import itself, used or not.
|
|
29
|
+
*/
|
|
55
30
|
async function defaultSha256(text) {
|
|
56
|
-
const bytes =
|
|
31
|
+
const bytes = utf8(text);
|
|
57
32
|
const subtle = globalThis.crypto?.subtle;
|
|
58
33
|
if (subtle) {
|
|
59
|
-
|
|
60
|
-
|
|
34
|
+
try {
|
|
35
|
+
const buf = await subtle.digest('SHA-256', bytes);
|
|
36
|
+
return hex(new Uint8Array(buf));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* a subtle that refuses (an insecure context): the digest below */
|
|
40
|
+
}
|
|
61
41
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
42
|
+
return hex(sha256(bytes));
|
|
43
|
+
}
|
|
44
|
+
const hex = (b) => Array.from(b, x => x.toString(16).padStart(2, '0')).join('');
|
|
45
|
+
/** UTF-8 bytes of a string, without TextEncoder (not every runtime has it). */
|
|
46
|
+
function utf8(text) {
|
|
47
|
+
const out = [];
|
|
48
|
+
for (let i = 0; i < text.length; i++) {
|
|
49
|
+
let c = text.charCodeAt(i);
|
|
50
|
+
if (c >= 0xd800 && c < 0xdc00 && i + 1 < text.length) {
|
|
51
|
+
const d = text.charCodeAt(i + 1);
|
|
52
|
+
if (d >= 0xdc00 && d < 0xe000) {
|
|
53
|
+
c = 0x10000 + ((c - 0xd800) << 10) + (d - 0xdc00);
|
|
54
|
+
i++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (c < 0x80)
|
|
58
|
+
out.push(c);
|
|
59
|
+
else if (c < 0x800)
|
|
60
|
+
out.push(0xc0 | (c >> 6), 0x80 | (c & 63));
|
|
61
|
+
else if (c < 0x10000)
|
|
62
|
+
out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
|
|
63
|
+
else
|
|
64
|
+
out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 63), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
|
|
65
65
|
}
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
return Uint8Array.from(out);
|
|
67
|
+
}
|
|
68
|
+
const K = new Uint32Array([
|
|
69
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
70
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
71
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
72
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
73
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
74
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
75
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
76
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
77
|
+
]);
|
|
78
|
+
/** SHA-256 (FIPS 180-4) of bytes. */
|
|
79
|
+
function sha256(data) {
|
|
80
|
+
const bitLen = data.length * 8;
|
|
81
|
+
const padded = new Uint8Array(((data.length + 9 + 63) >> 6) << 6);
|
|
82
|
+
padded.set(data);
|
|
83
|
+
padded[data.length] = 0x80;
|
|
84
|
+
const view = new DataView(padded.buffer);
|
|
85
|
+
view.setUint32(padded.length - 8, Math.floor(bitLen / 0x100000000));
|
|
86
|
+
view.setUint32(padded.length - 4, bitLen >>> 0);
|
|
87
|
+
const h = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
|
|
88
|
+
const w = new Uint32Array(64);
|
|
89
|
+
const rotr = (x, n) => (x >>> n) | (x << (32 - n));
|
|
90
|
+
for (let off = 0; off < padded.length; off += 64) {
|
|
91
|
+
for (let i = 0; i < 16; i++)
|
|
92
|
+
w[i] = view.getUint32(off + i * 4);
|
|
93
|
+
for (let i = 16; i < 64; i++) {
|
|
94
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
|
|
95
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
96
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
97
|
+
}
|
|
98
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
99
|
+
for (let i = 0; i < 64; i++) {
|
|
100
|
+
const t1 = (hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i]) >>> 0;
|
|
101
|
+
const t2 = ((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
|
|
102
|
+
hh = g;
|
|
103
|
+
g = f;
|
|
104
|
+
f = e;
|
|
105
|
+
e = (d + t1) >>> 0;
|
|
106
|
+
d = c;
|
|
107
|
+
c = b;
|
|
108
|
+
b = a;
|
|
109
|
+
a = (t1 + t2) >>> 0;
|
|
110
|
+
}
|
|
111
|
+
h[0] += a;
|
|
112
|
+
h[1] += b;
|
|
113
|
+
h[2] += c;
|
|
114
|
+
h[3] += d;
|
|
115
|
+
h[4] += e;
|
|
116
|
+
h[5] += f;
|
|
117
|
+
h[6] += g;
|
|
118
|
+
h[7] += hh;
|
|
68
119
|
}
|
|
120
|
+
const out = new Uint8Array(32);
|
|
121
|
+
const ov = new DataView(out.buffer);
|
|
122
|
+
for (let i = 0; i < 8; i++)
|
|
123
|
+
ov.setUint32(i * 4, h[i]);
|
|
124
|
+
return out;
|
|
69
125
|
}
|
|
70
126
|
/** Put a list in force for the process — one verified by `fetchLatestCorrections`, or restored from the app's own storage. */
|
|
71
127
|
function applyCorrections(authority, list, revision) {
|
|
@@ -123,14 +179,13 @@ async function fetchLatestCorrections(opts) {
|
|
|
123
179
|
const summary = summarize(list);
|
|
124
180
|
if (!same(summary, named))
|
|
125
181
|
return { status: 'rejected', reason: 'the list is not the one the certification names', have };
|
|
182
|
+
// A certification without a hash (one written before 1.0.0) is taken on its identity alone.
|
|
126
183
|
let integrity = 'unchecked';
|
|
127
184
|
if (named.sha256) {
|
|
128
185
|
const hash = opts.sha256 ? await opts.sha256(text) : await defaultSha256(text);
|
|
129
|
-
if (hash !==
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
integrity = 'verified';
|
|
133
|
-
}
|
|
186
|
+
if (hash.toLowerCase() !== named.sha256.toLowerCase())
|
|
187
|
+
return { status: 'rejected', reason: 'the list’s SHA-256 is not the one the certification names', have };
|
|
188
|
+
integrity = 'verified';
|
|
134
189
|
}
|
|
135
190
|
const applied = opts.apply === false ? false : applyCorrections(opts.authority, list, revision);
|
|
136
191
|
return { status: 'updated', list, summary, integrity, applied };
|
|
@@ -10,7 +10,7 @@ export type MethodDescription = Pick<MethodInfo, 'name' | 'method' | 'countries'
|
|
|
10
10
|
export type AuthorityData = {
|
|
11
11
|
authority: Authority;
|
|
12
12
|
revision: number;
|
|
13
|
-
places: Place[];
|
|
13
|
+
places: readonly Place[];
|
|
14
14
|
corrections: CorrectionList;
|
|
15
15
|
certification: Certification | null;
|
|
16
16
|
description: MethodDescription;
|
|
@@ -10,6 +10,11 @@ function registerAuthority(entry) {
|
|
|
10
10
|
const have = registry.get(entry.authority);
|
|
11
11
|
if (have)
|
|
12
12
|
return have;
|
|
13
|
+
// Frozen: every caller shares these objects, and a place moved by one
|
|
14
|
+
// (`place.lat = …`) would move the corrections it is looked up by for all.
|
|
15
|
+
for (const p of entry.places)
|
|
16
|
+
Object.freeze(p);
|
|
17
|
+
Object.freeze(entry.places);
|
|
13
18
|
const data = { ...entry, corrections: new correctionList_js_1.CorrectionList(entry.corrections) };
|
|
14
19
|
registry.set(entry.authority, data);
|
|
15
20
|
return data;
|
package/dist/cjs/core/types.d.ts
CHANGED
|
@@ -77,6 +77,8 @@ export type CheckRecord = {
|
|
|
77
77
|
times: number;
|
|
78
78
|
/** Times not on the printed minute. Certified only at 0. */
|
|
79
79
|
off: number;
|
|
80
|
+
/** Of the same times, how many the method alone (without its correction list) leaves off the printed minute. */
|
|
81
|
+
offWithoutCorrections?: number;
|
|
80
82
|
from: string;
|
|
81
83
|
to: string;
|
|
82
84
|
/** What the check set aside as the source's fault, not the method's: listed so it can be judged. */
|
|
@@ -90,6 +92,8 @@ export type CheckRecord = {
|
|
|
90
92
|
export type Certification = {
|
|
91
93
|
certified: boolean;
|
|
92
94
|
checkedAt: string;
|
|
95
|
+
/** How many places the library offers for the authority. */
|
|
96
|
+
places?: number;
|
|
93
97
|
checks: CheckRecord[];
|
|
94
98
|
/**
|
|
95
99
|
* The correction list this certification is for: the dates it covers, how
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const data = { "certified": true, "checkedAt": "2026-10-
|
|
3
|
+
const data = { "certified": true, "checkedAt": "2026-10-18", "places": 68, "checks": [{ "tables": "Algeria — Ministry of Religious Affairs", "places": 68, "times": 101184, "off": 0, "offWithoutCorrections": 642, "from": "2026-10-01", "to": "2027-06-05" }], "corrections": { "from": "2026-10-01", "to": "2027-06-05", "entries": 642, "sha256": "f8d227cdd88aabe5bcd1775ec0324abfdae07a8a3986fdd19816337c2d4ab3ca" }, "revision": 1 };
|
|
4
4
|
exports.default = data;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const data = { "certified": true, "checkedAt": "2026-10-
|
|
3
|
+
const data = { "certified": true, "checkedAt": "2026-10-18", "places": 8132, "checks": [{ "tables": "Diyanet — its tables worldwide", "places": 8132, "times": 69999603, "off": 0, "offWithoutCorrections": 9129, "from": "2024-01-01", "to": "2027-12-31" }], "corrections": { "from": "2024-01-01", "to": "2027-12-31", "entries": 9118, "sha256": "47997767e178ce48babd80f0e4a53460c5d379eb098bff1e7ebbba1e6afdf605" }, "revision": 1 };
|
|
4
4
|
exports.default = data;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const data = { "certified": true, "checkedAt": "2026-10-
|
|
3
|
+
const data = { "certified": true, "checkedAt": "2026-10-18", "places": 517, "checks": [{ "tables": "Indonesia — Kemenag", "places": 517, "times": 266772, "off": 0, "offWithoutCorrections": 84, "from": "2026-10-07", "to": "2026-12-31" }], "corrections": { "from": "2026-10-07", "to": "2026-12-31", "entries": 84, "sha256": "7c7d3b09a5b01a4d77c605ad8a37fbdac4fd3ef65df483332d75c67891b2359e" }, "revision": 1 };
|
|
4
4
|
exports.default = data;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const data = { "certified": true, "checkedAt": "2026-10-
|
|
3
|
+
const data = { "certified": true, "checkedAt": "2026-10-18", "places": 164, "checks": [{ "tables": "Morocco — Habous", "places": 164, "times": 75768, "off": 0, "offWithoutCorrections": 665, "from": "2026-08-27", "to": "2026-11-11" }], "corrections": { "from": "2026-08-27", "to": "2026-11-11", "entries": 665, "sha256": "26ccb0979ee3e632c5acd736fdba3f8272530b48577cb1c0f2897d061e695bd0" }, "revision": 1 };
|
|
4
4
|
exports.default = data;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const data = { "certified": true, "checkedAt": "2026-10-
|
|
3
|
+
const data = { "certified": true, "checkedAt": "2026-10-18", "places": 108, "checks": [{ "tables": "Sweden — Islamiska Förbundet’s tables", "places": 107, "times": 59640, "off": 0, "offWithoutCorrections": 20, "from": "2026-10-11", "to": "2027-11-19", "setAside": { "misplaced": [], "sourceFaults": 33395, "copiedRows": 646 } }, { "tables": "Sweden — Diyanet’s own tables", "places": 159, "times": 1393794, "off": 0, "offWithoutCorrections": 259, "from": "2024-01-01", "to": "2027-12-31" }], "corrections": { "from": "2024-01-01", "to": "2027-12-31", "entries": 270, "sha256": "173d7145b5ec917882f8ac9890dd1fa5f3143bd714eb540482861bf8a6bac660" }, "revision": 1 };
|
|
4
4
|
exports.default = data;
|
package/dist/cjs/locate.js
CHANGED
|
@@ -18,7 +18,7 @@ exports.defaultAuthorityFor = defaultAuthorityFor;
|
|
|
18
18
|
* that into the authority whose table the point's community prays by, or
|
|
19
19
|
* null where none of the five applies.
|
|
20
20
|
*
|
|
21
|
-
* Its own entry point because the outlines are
|
|
21
|
+
* Its own entry point because the outlines are 470 KB an app that already
|
|
22
22
|
* knows its country does not need.
|
|
23
23
|
*/
|
|
24
24
|
const countryOutlines_js_1 = __importDefault(require("./data/generated/countryOutlines.js"));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { Place } from '../core/types.js';
|
|
2
2
|
export declare function indonesiaZone(province: string): string;
|
|
3
3
|
export declare const indonesia: import("../core/registry.js").AuthorityData;
|
|
4
|
-
export declare const places: Place[];
|
|
4
|
+
export declare const places: readonly Place[];
|
|
@@ -2,4 +2,4 @@ import type { Place } from '../core/types.js';
|
|
|
2
2
|
/** The ministry's clock: GMT+1, then GMT — its tables print GMT from 17 September 2026, three days before the clocks moved. */
|
|
3
3
|
export declare const MOROCCO_CLOCK: [string, number][];
|
|
4
4
|
export declare const morocco: import("../core/registry.js").AuthorityData;
|
|
5
|
-
export declare const places: Place[];
|
|
5
|
+
export declare const places: readonly Place[];
|
package/dist/esm/core/api.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare function certifiedSpan(authority: Authority): {
|
|
|
17
17
|
to: string;
|
|
18
18
|
} | null;
|
|
19
19
|
/** The authority's places, each at the point its method is computed at. */
|
|
20
|
-
export declare function placesOf(authority: Authority): Place[];
|
|
20
|
+
export declare function placesOf(authority: Authority): readonly Place[];
|
|
21
21
|
/** The authorities this build has loaded. */
|
|
22
22
|
export declare function loadedAuthorityIds(): Authority[];
|
|
23
23
|
/** A place by its stable id (`<authority>:<key>`), among the loaded authorities. */
|
package/dist/esm/core/api.js
CHANGED
|
@@ -24,7 +24,8 @@ export function certifiedSpan(authority) {
|
|
|
24
24
|
if (!isCertified(authority))
|
|
25
25
|
return null;
|
|
26
26
|
const checks = certificationOf(authority).checks;
|
|
27
|
-
|
|
27
|
+
const tos = checks.map(c => c.to).sort();
|
|
28
|
+
return { from: checks.map(c => c.from).sort()[0], to: tos[tos.length - 1] };
|
|
28
29
|
}
|
|
29
30
|
/** The authority's places, each at the point its method is computed at. */
|
|
30
31
|
export function placesOf(authority) {
|
package/dist/esm/core/clock.d.ts
CHANGED
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
* the clocks go forward a Fajr before the change is printed in the new time.
|
|
6
6
|
*/
|
|
7
7
|
import type { Place, TableClock } from './types.js';
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Minutes east of UTC that a zone keeps at noon UTC of a date (month 0-based).
|
|
10
|
+
*
|
|
11
|
+
* Read from the zone's wall clock (year … minute, as every Intl has them),
|
|
12
|
+
* not from `timeZoneName: 'longOffset'`, which not every runtime supports:
|
|
13
|
+
* where it is missing the name comes back as 'GMT+1' or a zone's own
|
|
14
|
+
* abbreviation, and an offset parsed from that is silently wrong.
|
|
15
|
+
*/
|
|
9
16
|
export declare function clockOffsetAt(tz: string, year: number, month: number, day: number): number;
|
|
10
17
|
/** The clock a place's table prints a date in: its own (`Place.clock`) where it has one, else its zone's. Month 0-based. */
|
|
11
18
|
export declare function placeClockAt(place: Place, year: number, month: number, day: number): number;
|
package/dist/esm/core/clock.js
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
|
-
const
|
|
2
|
-
/**
|
|
1
|
+
const formats = new Map();
|
|
2
|
+
/**
|
|
3
|
+
* Minutes east of UTC that a zone keeps at noon UTC of a date (month 0-based).
|
|
4
|
+
*
|
|
5
|
+
* Read from the zone's wall clock (year … minute, as every Intl has them),
|
|
6
|
+
* not from `timeZoneName: 'longOffset'`, which not every runtime supports:
|
|
7
|
+
* where it is missing the name comes back as 'GMT+1' or a zone's own
|
|
8
|
+
* abbreviation, and an offset parsed from that is silently wrong.
|
|
9
|
+
*/
|
|
3
10
|
export function clockOffsetAt(tz, year, month, day) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
const at = Date.UTC(year, month, day, 12);
|
|
12
|
+
let f = formats.get(tz);
|
|
13
|
+
if (!f) {
|
|
14
|
+
f = new Intl.DateTimeFormat('en-US', {
|
|
15
|
+
timeZone: tz,
|
|
16
|
+
year: 'numeric',
|
|
17
|
+
month: 'numeric',
|
|
18
|
+
day: 'numeric',
|
|
19
|
+
hour: 'numeric',
|
|
20
|
+
minute: 'numeric',
|
|
21
|
+
hourCycle: 'h23',
|
|
22
|
+
});
|
|
23
|
+
formats.set(tz, f);
|
|
24
|
+
}
|
|
25
|
+
const parts = f.formatToParts(new Date(at));
|
|
26
|
+
const n = (type) => Number(parts.find(p => p.type === type)?.value);
|
|
27
|
+
const wall = Date.UTC(n('year'), n('month') - 1, n('day'), n('hour') % 24, n('minute'));
|
|
28
|
+
if (!Number.isFinite(wall))
|
|
29
|
+
throw new RangeError(`The zone ${tz} could not be read on this runtime.`);
|
|
30
|
+
return Math.round((wall - at) / 60_000);
|
|
10
31
|
}
|
|
11
32
|
/** The clock a place's table prints a date in: its own (`Place.clock`) where it has one, else its zone's. Month 0-based. */
|
|
12
33
|
export function placeClockAt(place, year, month, day) {
|
|
@@ -9,7 +9,7 @@ export type FetchOptions = {
|
|
|
9
9
|
baseUrl?: string;
|
|
10
10
|
/** The fetch to use (default: the global one). */
|
|
11
11
|
fetch?: typeof fetch;
|
|
12
|
-
/** SHA-256 of a UTF-8 string as lowercase hex; default: WebCrypto
|
|
12
|
+
/** SHA-256 of a UTF-8 string as lowercase hex; default: WebCrypto where there is one, else a built-in digest. */
|
|
13
13
|
sha256?: (text: string) => Promise<string>;
|
|
14
14
|
/** Put a newer list in force in this process (default true). */
|
|
15
15
|
apply?: boolean;
|
|
@@ -52,6 +52,10 @@ export type FetchResult =
|
|
|
52
52
|
have: ListSummary;
|
|
53
53
|
};
|
|
54
54
|
export declare function summarize(list: CorrectionsFile): ListSummary;
|
|
55
|
+
/** UTF-8 bytes of a string, without TextEncoder (not every runtime has it). */
|
|
56
|
+
export declare function utf8(text: string): Uint8Array;
|
|
57
|
+
/** SHA-256 (FIPS 180-4) of bytes. */
|
|
58
|
+
export declare function sha256(data: Uint8Array): Uint8Array;
|
|
55
59
|
/** Put a list in force for the process — one verified by `fetchLatestCorrections`, or restored from the app's own storage. */
|
|
56
60
|
export declare function applyCorrections(authority: Authority, list: CorrectionsFile, revision: number): boolean;
|
|
57
61
|
export declare function fetchLatestCorrections(opts: FetchOptions): Promise<FetchResult>;
|
|
@@ -13,20 +13,107 @@ export function summarize(list) {
|
|
|
13
13
|
return { from: list.from, to: list.to, entries: Object.values(list.points).reduce((n, e) => n + e.length, 0) };
|
|
14
14
|
}
|
|
15
15
|
const same = (a, b) => a.from === b.from && a.to === b.to && a.entries === b.entries;
|
|
16
|
+
/**
|
|
17
|
+
* SHA-256 as lowercase hex. WebCrypto where the runtime has it (browsers,
|
|
18
|
+
* Node 18+, Deno, Bun); elsewhere — Hermes in React Native has none — the
|
|
19
|
+
* plain-JavaScript digest below. No `node:crypto` import: a bundler for a
|
|
20
|
+
* phone or a browser fails on the import itself, used or not.
|
|
21
|
+
*/
|
|
16
22
|
async function defaultSha256(text) {
|
|
17
|
-
const bytes =
|
|
23
|
+
const bytes = utf8(text);
|
|
18
24
|
const subtle = globalThis.crypto?.subtle;
|
|
19
25
|
if (subtle) {
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
try {
|
|
27
|
+
const buf = await subtle.digest('SHA-256', bytes);
|
|
28
|
+
return hex(new Uint8Array(buf));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* a subtle that refuses (an insecure context): the digest below */
|
|
32
|
+
}
|
|
22
33
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
return hex(sha256(bytes));
|
|
35
|
+
}
|
|
36
|
+
const hex = (b) => Array.from(b, x => x.toString(16).padStart(2, '0')).join('');
|
|
37
|
+
/** UTF-8 bytes of a string, without TextEncoder (not every runtime has it). */
|
|
38
|
+
export function utf8(text) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (let i = 0; i < text.length; i++) {
|
|
41
|
+
let c = text.charCodeAt(i);
|
|
42
|
+
if (c >= 0xd800 && c < 0xdc00 && i + 1 < text.length) {
|
|
43
|
+
const d = text.charCodeAt(i + 1);
|
|
44
|
+
if (d >= 0xdc00 && d < 0xe000) {
|
|
45
|
+
c = 0x10000 + ((c - 0xd800) << 10) + (d - 0xdc00);
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (c < 0x80)
|
|
50
|
+
out.push(c);
|
|
51
|
+
else if (c < 0x800)
|
|
52
|
+
out.push(0xc0 | (c >> 6), 0x80 | (c & 63));
|
|
53
|
+
else if (c < 0x10000)
|
|
54
|
+
out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
|
|
55
|
+
else
|
|
56
|
+
out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 63), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
|
|
26
57
|
}
|
|
27
|
-
|
|
28
|
-
|
|
58
|
+
return Uint8Array.from(out);
|
|
59
|
+
}
|
|
60
|
+
const K = new Uint32Array([
|
|
61
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
62
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
63
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
64
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
65
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
66
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
67
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
68
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
69
|
+
]);
|
|
70
|
+
/** SHA-256 (FIPS 180-4) of bytes. */
|
|
71
|
+
export function sha256(data) {
|
|
72
|
+
const bitLen = data.length * 8;
|
|
73
|
+
const padded = new Uint8Array(((data.length + 9 + 63) >> 6) << 6);
|
|
74
|
+
padded.set(data);
|
|
75
|
+
padded[data.length] = 0x80;
|
|
76
|
+
const view = new DataView(padded.buffer);
|
|
77
|
+
view.setUint32(padded.length - 8, Math.floor(bitLen / 0x100000000));
|
|
78
|
+
view.setUint32(padded.length - 4, bitLen >>> 0);
|
|
79
|
+
const h = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
|
|
80
|
+
const w = new Uint32Array(64);
|
|
81
|
+
const rotr = (x, n) => (x >>> n) | (x << (32 - n));
|
|
82
|
+
for (let off = 0; off < padded.length; off += 64) {
|
|
83
|
+
for (let i = 0; i < 16; i++)
|
|
84
|
+
w[i] = view.getUint32(off + i * 4);
|
|
85
|
+
for (let i = 16; i < 64; i++) {
|
|
86
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
|
|
87
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
88
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
89
|
+
}
|
|
90
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
91
|
+
for (let i = 0; i < 64; i++) {
|
|
92
|
+
const t1 = (hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i]) >>> 0;
|
|
93
|
+
const t2 = ((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
|
|
94
|
+
hh = g;
|
|
95
|
+
g = f;
|
|
96
|
+
f = e;
|
|
97
|
+
e = (d + t1) >>> 0;
|
|
98
|
+
d = c;
|
|
99
|
+
c = b;
|
|
100
|
+
b = a;
|
|
101
|
+
a = (t1 + t2) >>> 0;
|
|
102
|
+
}
|
|
103
|
+
h[0] += a;
|
|
104
|
+
h[1] += b;
|
|
105
|
+
h[2] += c;
|
|
106
|
+
h[3] += d;
|
|
107
|
+
h[4] += e;
|
|
108
|
+
h[5] += f;
|
|
109
|
+
h[6] += g;
|
|
110
|
+
h[7] += hh;
|
|
29
111
|
}
|
|
112
|
+
const out = new Uint8Array(32);
|
|
113
|
+
const ov = new DataView(out.buffer);
|
|
114
|
+
for (let i = 0; i < 8; i++)
|
|
115
|
+
ov.setUint32(i * 4, h[i]);
|
|
116
|
+
return out;
|
|
30
117
|
}
|
|
31
118
|
/** Put a list in force for the process — one verified by `fetchLatestCorrections`, or restored from the app's own storage. */
|
|
32
119
|
export function applyCorrections(authority, list, revision) {
|
|
@@ -84,14 +171,13 @@ export async function fetchLatestCorrections(opts) {
|
|
|
84
171
|
const summary = summarize(list);
|
|
85
172
|
if (!same(summary, named))
|
|
86
173
|
return { status: 'rejected', reason: 'the list is not the one the certification names', have };
|
|
174
|
+
// A certification without a hash (one written before 1.0.0) is taken on its identity alone.
|
|
87
175
|
let integrity = 'unchecked';
|
|
88
176
|
if (named.sha256) {
|
|
89
177
|
const hash = opts.sha256 ? await opts.sha256(text) : await defaultSha256(text);
|
|
90
|
-
if (hash !==
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
integrity = 'verified';
|
|
94
|
-
}
|
|
178
|
+
if (hash.toLowerCase() !== named.sha256.toLowerCase())
|
|
179
|
+
return { status: 'rejected', reason: 'the list’s SHA-256 is not the one the certification names', have };
|
|
180
|
+
integrity = 'verified';
|
|
95
181
|
}
|
|
96
182
|
const applied = opts.apply === false ? false : applyCorrections(opts.authority, list, revision);
|
|
97
183
|
return { status: 'updated', list, summary, integrity, applied };
|
|
@@ -10,7 +10,7 @@ export type MethodDescription = Pick<MethodInfo, 'name' | 'method' | 'countries'
|
|
|
10
10
|
export type AuthorityData = {
|
|
11
11
|
authority: Authority;
|
|
12
12
|
revision: number;
|
|
13
|
-
places: Place[];
|
|
13
|
+
places: readonly Place[];
|
|
14
14
|
corrections: CorrectionList;
|
|
15
15
|
certification: Certification | null;
|
|
16
16
|
description: MethodDescription;
|
|
@@ -4,6 +4,11 @@ export function registerAuthority(entry) {
|
|
|
4
4
|
const have = registry.get(entry.authority);
|
|
5
5
|
if (have)
|
|
6
6
|
return have;
|
|
7
|
+
// Frozen: every caller shares these objects, and a place moved by one
|
|
8
|
+
// (`place.lat = …`) would move the corrections it is looked up by for all.
|
|
9
|
+
for (const p of entry.places)
|
|
10
|
+
Object.freeze(p);
|
|
11
|
+
Object.freeze(entry.places);
|
|
7
12
|
const data = { ...entry, corrections: new CorrectionList(entry.corrections) };
|
|
8
13
|
registry.set(entry.authority, data);
|
|
9
14
|
return data;
|
package/dist/esm/core/types.d.ts
CHANGED
|
@@ -77,6 +77,8 @@ export type CheckRecord = {
|
|
|
77
77
|
times: number;
|
|
78
78
|
/** Times not on the printed minute. Certified only at 0. */
|
|
79
79
|
off: number;
|
|
80
|
+
/** Of the same times, how many the method alone (without its correction list) leaves off the printed minute. */
|
|
81
|
+
offWithoutCorrections?: number;
|
|
80
82
|
from: string;
|
|
81
83
|
to: string;
|
|
82
84
|
/** What the check set aside as the source's fault, not the method's: listed so it can be judged. */
|
|
@@ -90,6 +92,8 @@ export type CheckRecord = {
|
|
|
90
92
|
export type Certification = {
|
|
91
93
|
certified: boolean;
|
|
92
94
|
checkedAt: string;
|
|
95
|
+
/** How many places the library offers for the authority. */
|
|
96
|
+
places?: number;
|
|
93
97
|
checks: CheckRecord[];
|
|
94
98
|
/**
|
|
95
99
|
* The correction list this certification is for: the dates it covers, how
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const data = { "certified": true, "checkedAt": "2026-10-
|
|
1
|
+
const data = { "certified": true, "checkedAt": "2026-10-18", "places": 68, "checks": [{ "tables": "Algeria — Ministry of Religious Affairs", "places": 68, "times": 101184, "off": 0, "offWithoutCorrections": 642, "from": "2026-10-01", "to": "2027-06-05" }], "corrections": { "from": "2026-10-01", "to": "2027-06-05", "entries": 642, "sha256": "f8d227cdd88aabe5bcd1775ec0324abfdae07a8a3986fdd19816337c2d4ab3ca" }, "revision": 1 };
|
|
2
2
|
export default data;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const data = { "certified": true, "checkedAt": "2026-10-
|
|
1
|
+
const data = { "certified": true, "checkedAt": "2026-10-18", "places": 8132, "checks": [{ "tables": "Diyanet — its tables worldwide", "places": 8132, "times": 69999603, "off": 0, "offWithoutCorrections": 9129, "from": "2024-01-01", "to": "2027-12-31" }], "corrections": { "from": "2024-01-01", "to": "2027-12-31", "entries": 9118, "sha256": "47997767e178ce48babd80f0e4a53460c5d379eb098bff1e7ebbba1e6afdf605" }, "revision": 1 };
|
|
2
2
|
export default data;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const data = { "certified": true, "checkedAt": "2026-10-
|
|
1
|
+
const data = { "certified": true, "checkedAt": "2026-10-18", "places": 517, "checks": [{ "tables": "Indonesia — Kemenag", "places": 517, "times": 266772, "off": 0, "offWithoutCorrections": 84, "from": "2026-10-07", "to": "2026-12-31" }], "corrections": { "from": "2026-10-07", "to": "2026-12-31", "entries": 84, "sha256": "7c7d3b09a5b01a4d77c605ad8a37fbdac4fd3ef65df483332d75c67891b2359e" }, "revision": 1 };
|
|
2
2
|
export default data;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const data = { "certified": true, "checkedAt": "2026-10-
|
|
1
|
+
const data = { "certified": true, "checkedAt": "2026-10-18", "places": 164, "checks": [{ "tables": "Morocco — Habous", "places": 164, "times": 75768, "off": 0, "offWithoutCorrections": 665, "from": "2026-08-27", "to": "2026-11-11" }], "corrections": { "from": "2026-08-27", "to": "2026-11-11", "entries": 665, "sha256": "26ccb0979ee3e632c5acd736fdba3f8272530b48577cb1c0f2897d061e695bd0" }, "revision": 1 };
|
|
2
2
|
export default data;
|