mihrab-methods 1.0.0 → 1.0.1
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 +3 -3
- 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/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/locate.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -204,13 +204,13 @@ npm install mihrab-methods
|
|
|
204
204
|
```
|
|
205
205
|
|
|
206
206
|
One entry point per authority, so an app ships only the data it needs
|
|
207
|
-
(Indonesia's is
|
|
207
|
+
(Indonesia's is 90 KB; Diyanet's worldwide points and list are 1.6 MB):
|
|
208
208
|
|
|
209
209
|
| Import | Loads |
|
|
210
210
|
|---|---|
|
|
211
211
|
| `mihrab-methods` | all five |
|
|
212
212
|
| `mihrab-methods/sweden`, `/diyanet`, `/morocco`, `/algeria`, `/indonesia` | one authority: its places, list and certification |
|
|
213
|
-
| `mihrab-methods/locate` | `countryAt`, `defaultAuthorityFor` — the country at a point, and the authority whose table it prays by (
|
|
213
|
+
| `mihrab-methods/locate` | `countryAt`, `defaultAuthorityFor` — the country at a point, and the authority whose table it prays by (470 KB of outlines) |
|
|
214
214
|
| `mihrab-methods/core` | the calls and types with no data (load authorities yourself) |
|
|
215
215
|
|
|
216
216
|
CommonJS and ES modules, with types. Node 18+, or any bundler; nothing is
|
|
@@ -231,7 +231,7 @@ timesFor(place, '2026-10-15'); // or { year: 2026, month: 10, day
|
|
|
231
231
|
// on the printed minute all but rarely, never a minute further
|
|
232
232
|
// date: '2026-10-15', certifiedFrom: '2024-01-01', certifiedUntil: '2027-12-31', revision: 1,
|
|
233
233
|
// clock: { zone: 'Europe/Stockholm', offsetMinutes: 120, label: 'Europe/Stockholm' },
|
|
234
|
-
// times: { fajr: { instant: Date, printed: '05:
|
|
234
|
+
// times: { fajr: { instant: Date, printed: '05:09' }, sunrise: …, dhuhr: …, asr: …, maghrib: …, isha: … }
|
|
235
235
|
// }
|
|
236
236
|
timesFor(place, '2031-01-01', { strict: true }); // throws UncertifiedMethodError instead
|
|
237
237
|
timesFor(place, '2026-10-15', { asrShadow: 2 }); // the Hanafi Asr (the tables print the standard one)
|
|
@@ -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/cjs/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/cjs/core/api.js
CHANGED
|
@@ -38,7 +38,8 @@ function certifiedSpan(authority) {
|
|
|
38
38
|
if (!isCertified(authority))
|
|
39
39
|
return null;
|
|
40
40
|
const checks = certificationOf(authority).checks;
|
|
41
|
-
|
|
41
|
+
const tos = checks.map(c => c.to).sort();
|
|
42
|
+
return { from: checks.map(c => c.from).sort()[0], to: tos[tos.length - 1] };
|
|
42
43
|
}
|
|
43
44
|
/** The authority's places, each at the point its method is computed at. */
|
|
44
45
|
function placesOf(authority) {
|
package/dist/cjs/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/cjs/core/clock.js
CHANGED
|
@@ -5,15 +5,36 @@ exports.placeClockAt = placeClockAt;
|
|
|
5
5
|
exports.offsetLabel = offsetLabel;
|
|
6
6
|
exports.tableClockAt = tableClockAt;
|
|
7
7
|
exports.printedAt = printedAt;
|
|
8
|
-
const
|
|
9
|
-
/**
|
|
8
|
+
const formats = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* Minutes east of UTC that a zone keeps at noon UTC of a date (month 0-based).
|
|
11
|
+
*
|
|
12
|
+
* Read from the zone's wall clock (year … minute, as every Intl has them),
|
|
13
|
+
* not from `timeZoneName: 'longOffset'`, which not every runtime supports:
|
|
14
|
+
* where it is missing the name comes back as 'GMT+1' or a zone's own
|
|
15
|
+
* abbreviation, and an offset parsed from that is silently wrong.
|
|
16
|
+
*/
|
|
10
17
|
function clockOffsetAt(tz, year, month, day) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
const at = Date.UTC(year, month, day, 12);
|
|
19
|
+
let f = formats.get(tz);
|
|
20
|
+
if (!f) {
|
|
21
|
+
f = new Intl.DateTimeFormat('en-US', {
|
|
22
|
+
timeZone: tz,
|
|
23
|
+
year: 'numeric',
|
|
24
|
+
month: 'numeric',
|
|
25
|
+
day: 'numeric',
|
|
26
|
+
hour: 'numeric',
|
|
27
|
+
minute: 'numeric',
|
|
28
|
+
hourCycle: 'h23',
|
|
29
|
+
});
|
|
30
|
+
formats.set(tz, f);
|
|
31
|
+
}
|
|
32
|
+
const parts = f.formatToParts(new Date(at));
|
|
33
|
+
const n = (type) => Number(parts.find(p => p.type === type)?.value);
|
|
34
|
+
const wall = Date.UTC(n('year'), n('month') - 1, n('day'), n('hour') % 24, n('minute'));
|
|
35
|
+
if (!Number.isFinite(wall))
|
|
36
|
+
throw new RangeError(`The zone ${tz} could not be read on this runtime.`);
|
|
37
|
+
return Math.round((wall - at) / 60_000);
|
|
17
38
|
}
|
|
18
39
|
/** 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. */
|
|
19
40
|
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>;
|
|
@@ -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/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/locate.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* that into the authority whose table the point's community prays by, or
|
|
11
11
|
* null where none of the five applies.
|
|
12
12
|
*
|
|
13
|
-
* Its own entry point because the outlines are
|
|
13
|
+
* Its own entry point because the outlines are 470 KB an app that already
|
|
14
14
|
* knows its country does not need.
|
|
15
15
|
*/
|
|
16
16
|
import outlines from './data/generated/countryOutlines.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mihrab-methods",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "The prayer-time authorities' own calculation methods, each reproducing its published timetable to the minute: Sweden (Diyanet, as Islamiska Förbundet prints it), Diyanet worldwide, Morocco (Habous), Algeria (Ministry of Religious Affairs), Indonesia (Kemenag).",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"author": "Hassan El Ghamri",
|