@halallens/halal-scanner 1.0.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/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HalalLens
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ Attribution Requirement:
16
+ - Any product, service, or derivative work using this Software must include
17
+ visible attribution to HalalLens (https://halallens.no) in user-facing
18
+ documentation or about screens.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ <div align="center">
2
+ <img src="assets/screenshot-hero.png" alt="Free Halal Scanner" width="600">
3
+
4
+ # Free Halal Scanner
5
+
6
+ **Check if any food ingredient is halal, haram, or mushbooh — instantly.**
7
+
8
+ [![npm](https://img.shields.io/npm/v/@halallens/halal-scanner?color=00A878&style=flat-square)](https://www.npmjs.com/package/@halallens/halal-scanner)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](LICENSE)
10
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-3178c6?style=flat-square)](https://www.typescriptlang.org/)
11
+
12
+ [Live Demo](https://halallens-no.github.io/halal-scanner/) · [npm Package](https://www.npmjs.com/package/@halallens/halal-scanner) · [Full Web App](https://app.halallens.no)
13
+ </div>
14
+
15
+ ---
16
+
17
+ ## What is this?
18
+
19
+ A free, open-source npm package and demo UI for checking the halal status of food ingredients. Powered by the [HalalLens](https://halallens.no) API — a database of 10,000+ ingredients analyzed by AI and verified by the community.
20
+
21
+ ### Features
22
+
23
+ - **Instant search** — Type any ingredient name or E-number
24
+ - **Halal status** — HALAL, HARAM, MUSHBOOH, or UNKNOWN with confidence scores
25
+ - **AI explanations** — Understand *why* an ingredient has its status
26
+ - **TypeScript support** — Full type definitions included
27
+ - **Zero dependencies** — Uses native `fetch`, works in Node.js 18+ and all modern browsers
28
+ - **Free forever** — No API key required
29
+
30
+ ## Try it
31
+
32
+ **[Live Demo →](https://halallens-no.github.io/halal-scanner/)**
33
+
34
+ <img src="assets/screenshot-results.png" alt="Search Results" width="600">
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ npm install @halallens/halal-scanner
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```typescript
45
+ import { HalalScanner } from '@halallens/halal-scanner';
46
+
47
+ const scanner = new HalalScanner();
48
+
49
+ // Search for ingredients
50
+ const results = await scanner.search('gelatin');
51
+ console.log(results.items[0].halal_status); // "HARAM"
52
+ console.log(results.items[0].display_status); // "Verified HARAM"
53
+
54
+ // Get detailed ingredient info
55
+ const detail = await scanner.check('en', 'gelatin');
56
+ console.log(detail.explanation);
57
+ console.log(detail.food_source); // "Animal"
58
+
59
+ // Batch analyze multiple ingredients
60
+ const analysis = await scanner.analyze(['gelatin', 'sugar', 'E471']);
61
+ console.log(`${analysis.halal_count} halal, ${analysis.haram_count} haram`);
62
+ ```
63
+
64
+ ## API
65
+
66
+ ### `new HalalScanner(options?)`
67
+
68
+ | Option | Type | Default | Description |
69
+ |--------|------|---------|-------------|
70
+ | `baseUrl` | `string` | `https://halallens.no/api` | API base URL |
71
+ | `deviceId` | `string` | Auto-generated | Device ID for usage tracking |
72
+ | `timeout` | `number` | `10000` | Request timeout in ms |
73
+
74
+ ### `scanner.search(query, params?)`
75
+
76
+ Search for ingredients by name or E-number.
77
+
78
+ ```typescript
79
+ const results = await scanner.search('E471', { perPage: 5 });
80
+ ```
81
+
82
+ ### `scanner.check(lang, name)`
83
+
84
+ Get detailed info about a specific ingredient.
85
+
86
+ ```typescript
87
+ const info = await scanner.check('en', 'sugar');
88
+ ```
89
+
90
+ ### `scanner.analyze(ingredients)`
91
+
92
+ Analyze multiple ingredients at once.
93
+
94
+ ```typescript
95
+ const result = await scanner.analyze(['sugar', 'gelatin', 'citric acid']);
96
+ ```
97
+
98
+ ## Status Values
99
+
100
+ | Status | Meaning |
101
+ |--------|---------|
102
+ | `HALAL` | Permissible to consume |
103
+ | `HARAM` | Not permissible to consume |
104
+ | `MUSHBOOH` | Doubtful — depends on source or processing |
105
+ | `UNKNOWN` | Not yet analyzed |
106
+
107
+ ## Want more?
108
+
109
+ This package provides ingredient lookup. For the **full experience**:
110
+
111
+ - **Scan product labels** with your camera (OCR-powered)
112
+ - **AI-powered analysis** of entire ingredient lists
113
+ - **Multi-language support** in 14+ languages
114
+ - **Mobile apps** for Android and iOS
115
+
116
+ **[Try HalalLens →](https://app.halallens.no)**
117
+
118
+ ## Contributing
119
+
120
+ Contributions welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) before submitting a PR.
121
+
122
+ ## License
123
+
124
+ [MIT](LICENSE) — Free to use with attribution to [HalalLens](https://halallens.no).
125
+
126
+ ---
127
+
128
+ <div align="center">
129
+ <sub>Built with care by <a href="https://halallens.no">HalalLens</a> — AI-powered halal food verification</sub>
130
+ </div>
@@ -0,0 +1,185 @@
1
+ /** Halal status of an ingredient */
2
+ type HalalStatus = 'HALAL' | 'HARAM' | 'MUSHBOOH' | 'UNKNOWN' | 'NOT_FOOD';
3
+ /** Data source for the status determination */
4
+ type IngredientSource = 'database' | 'llm' | 'manual' | 'cache';
5
+ /** Scanner configuration options */
6
+ interface ScannerOptions {
7
+ /** API base URL. Defaults to https://halallens.no/api */
8
+ baseUrl?: string;
9
+ /** Device ID for usage tracking. Auto-generated if not provided. */
10
+ deviceId?: string;
11
+ /** Request timeout in milliseconds. Defaults to 10000. */
12
+ timeout?: number;
13
+ }
14
+ interface SearchParams {
15
+ /** Search query text */
16
+ query: string;
17
+ /** Filter by halal status */
18
+ status?: HalalStatus;
19
+ /** Filter by category */
20
+ category?: string;
21
+ /** Language code (ISO 639-1). Defaults to "en". */
22
+ language?: string;
23
+ /** Only return verified ingredients */
24
+ verifiedOnly?: boolean;
25
+ /** Only return E-numbers */
26
+ eNumber?: boolean;
27
+ /** Page number. Defaults to 1. */
28
+ page?: number;
29
+ /** Results per page. Defaults to 20, max 100. */
30
+ perPage?: number;
31
+ }
32
+ interface SearchResult {
33
+ items: IngredientSummary[];
34
+ total: number;
35
+ page: number;
36
+ per_page: number;
37
+ pages: number;
38
+ language_matches: LanguageMatch[];
39
+ fuzzy_suggestions: FuzzySuggestion[];
40
+ }
41
+ interface IngredientSummary {
42
+ id: number;
43
+ name: string;
44
+ category: string | null;
45
+ language_code: string;
46
+ halal_status: HalalStatus;
47
+ display_status: string;
48
+ status_verified: boolean;
49
+ status_color: string;
50
+ image_url: string | null;
51
+ has_grounding: boolean;
52
+ has_elaborated_explanation: boolean;
53
+ is_gemini_enriched: boolean;
54
+ ai_review: AIReviewSummary | null;
55
+ similar_items: SimilarItem[];
56
+ }
57
+ interface AIReviewSummary {
58
+ ai_review_count: number;
59
+ agreement_percentage: number;
60
+ consensus_status: HalalStatus | null;
61
+ consensus_confidence: number | null;
62
+ analyzed_at: string | null;
63
+ opinions: AIOpinion[];
64
+ show_all_opinions: boolean;
65
+ }
66
+ interface AIOpinion {
67
+ halal_status: HalalStatus;
68
+ confidence: number;
69
+ explanation: string;
70
+ food_source: string | null;
71
+ }
72
+ interface SimilarItem {
73
+ id: number;
74
+ name: string;
75
+ halal_status: HalalStatus;
76
+ status_verified: boolean;
77
+ status_color: string;
78
+ ai_review_count: number;
79
+ }
80
+ interface LanguageMatch {
81
+ language_code: string;
82
+ language_name: string;
83
+ match_count: number;
84
+ }
85
+ interface FuzzySuggestion {
86
+ id: number;
87
+ name: string;
88
+ halal_status: HalalStatus;
89
+ display_status: string;
90
+ status_color: string;
91
+ similarity: number;
92
+ }
93
+ interface IngredientDetail extends IngredientSummary {
94
+ explanation: string | null;
95
+ food_source: string | null;
96
+ verified_at: string | null;
97
+ verification_notes: string | null;
98
+ translations: IngredientTranslation[];
99
+ explanation_language: string;
100
+ translation_available: boolean;
101
+ created_at: string | null;
102
+ updated_at: string | null;
103
+ }
104
+ interface IngredientTranslation {
105
+ language_code: string;
106
+ translated_name: string;
107
+ is_validated: boolean;
108
+ }
109
+ interface AnalyzeResult {
110
+ queue_id: string | null;
111
+ status: 'pending' | 'processing' | 'completed' | 'error';
112
+ ingredients: AnalyzedIngredient[];
113
+ total_count: number;
114
+ halal_count: number;
115
+ haram_count: number;
116
+ mushbooh_count: number;
117
+ unknown_count: number;
118
+ completion_percentage: number;
119
+ error: string | null;
120
+ }
121
+ interface AnalyzedIngredient {
122
+ ocr_item_id: string | null;
123
+ ingredient_id: number | null;
124
+ original_text: string;
125
+ display_name: string;
126
+ source_language: string;
127
+ halal_status: HalalStatus;
128
+ confidence: number;
129
+ source: IngredientSource;
130
+ explanation: string | null;
131
+ image_url: string | null;
132
+ is_verified: boolean;
133
+ has_grounding: boolean;
134
+ }
135
+
136
+ declare class HalalScanner {
137
+ private readonly baseUrl;
138
+ private readonly deviceId;
139
+ private readonly timeout;
140
+ constructor(options?: ScannerOptions);
141
+ /**
142
+ * Search for ingredients by name.
143
+ *
144
+ * @example
145
+ * const results = await scanner.search('gelatin');
146
+ * console.log(results.items[0].halal_status); // "HARAM"
147
+ */
148
+ search(query: string, params?: Partial<SearchParams>): Promise<SearchResult>;
149
+ /**
150
+ * Get detailed information about a specific ingredient.
151
+ *
152
+ * @param lang - Language code (e.g., "en", "ar", "no")
153
+ * @param name - Ingredient URL slug (e.g., "gelatin", "sugar")
154
+ *
155
+ * @example
156
+ * const detail = await scanner.check('en', 'gelatin');
157
+ * console.log(detail.explanation);
158
+ */
159
+ check(lang: string, name: string): Promise<IngredientDetail>;
160
+ /**
161
+ * Analyze a list of ingredients for halal status.
162
+ *
163
+ * @param ingredients - Array of ingredient names to check
164
+ *
165
+ * @example
166
+ * const result = await scanner.analyze(['gelatin', 'sugar', 'E471']);
167
+ * console.log(`${result.halal_count} halal, ${result.haram_count} haram`);
168
+ */
169
+ analyze(ingredients: string[]): Promise<AnalyzeResult>;
170
+ private request;
171
+ }
172
+
173
+ declare class HalalScannerError extends Error {
174
+ readonly statusCode?: number | undefined;
175
+ readonly endpoint?: string | undefined;
176
+ constructor(message: string, statusCode?: number | undefined, endpoint?: string | undefined);
177
+ }
178
+ declare class RateLimitError extends HalalScannerError {
179
+ constructor(endpoint: string);
180
+ }
181
+ declare class NetworkError extends HalalScannerError {
182
+ constructor(message: string, endpoint: string);
183
+ }
184
+
185
+ export { type AIOpinion, type AIReviewSummary, type AnalyzeResult, type AnalyzedIngredient, type FuzzySuggestion, HalalScanner, HalalScannerError, type HalalStatus, type IngredientDetail, type IngredientSource, type IngredientSummary, type IngredientTranslation, type LanguageMatch, NetworkError, RateLimitError, type ScannerOptions, type SearchParams, type SearchResult, type SimilarItem };
@@ -0,0 +1,185 @@
1
+ /** Halal status of an ingredient */
2
+ type HalalStatus = 'HALAL' | 'HARAM' | 'MUSHBOOH' | 'UNKNOWN' | 'NOT_FOOD';
3
+ /** Data source for the status determination */
4
+ type IngredientSource = 'database' | 'llm' | 'manual' | 'cache';
5
+ /** Scanner configuration options */
6
+ interface ScannerOptions {
7
+ /** API base URL. Defaults to https://halallens.no/api */
8
+ baseUrl?: string;
9
+ /** Device ID for usage tracking. Auto-generated if not provided. */
10
+ deviceId?: string;
11
+ /** Request timeout in milliseconds. Defaults to 10000. */
12
+ timeout?: number;
13
+ }
14
+ interface SearchParams {
15
+ /** Search query text */
16
+ query: string;
17
+ /** Filter by halal status */
18
+ status?: HalalStatus;
19
+ /** Filter by category */
20
+ category?: string;
21
+ /** Language code (ISO 639-1). Defaults to "en". */
22
+ language?: string;
23
+ /** Only return verified ingredients */
24
+ verifiedOnly?: boolean;
25
+ /** Only return E-numbers */
26
+ eNumber?: boolean;
27
+ /** Page number. Defaults to 1. */
28
+ page?: number;
29
+ /** Results per page. Defaults to 20, max 100. */
30
+ perPage?: number;
31
+ }
32
+ interface SearchResult {
33
+ items: IngredientSummary[];
34
+ total: number;
35
+ page: number;
36
+ per_page: number;
37
+ pages: number;
38
+ language_matches: LanguageMatch[];
39
+ fuzzy_suggestions: FuzzySuggestion[];
40
+ }
41
+ interface IngredientSummary {
42
+ id: number;
43
+ name: string;
44
+ category: string | null;
45
+ language_code: string;
46
+ halal_status: HalalStatus;
47
+ display_status: string;
48
+ status_verified: boolean;
49
+ status_color: string;
50
+ image_url: string | null;
51
+ has_grounding: boolean;
52
+ has_elaborated_explanation: boolean;
53
+ is_gemini_enriched: boolean;
54
+ ai_review: AIReviewSummary | null;
55
+ similar_items: SimilarItem[];
56
+ }
57
+ interface AIReviewSummary {
58
+ ai_review_count: number;
59
+ agreement_percentage: number;
60
+ consensus_status: HalalStatus | null;
61
+ consensus_confidence: number | null;
62
+ analyzed_at: string | null;
63
+ opinions: AIOpinion[];
64
+ show_all_opinions: boolean;
65
+ }
66
+ interface AIOpinion {
67
+ halal_status: HalalStatus;
68
+ confidence: number;
69
+ explanation: string;
70
+ food_source: string | null;
71
+ }
72
+ interface SimilarItem {
73
+ id: number;
74
+ name: string;
75
+ halal_status: HalalStatus;
76
+ status_verified: boolean;
77
+ status_color: string;
78
+ ai_review_count: number;
79
+ }
80
+ interface LanguageMatch {
81
+ language_code: string;
82
+ language_name: string;
83
+ match_count: number;
84
+ }
85
+ interface FuzzySuggestion {
86
+ id: number;
87
+ name: string;
88
+ halal_status: HalalStatus;
89
+ display_status: string;
90
+ status_color: string;
91
+ similarity: number;
92
+ }
93
+ interface IngredientDetail extends IngredientSummary {
94
+ explanation: string | null;
95
+ food_source: string | null;
96
+ verified_at: string | null;
97
+ verification_notes: string | null;
98
+ translations: IngredientTranslation[];
99
+ explanation_language: string;
100
+ translation_available: boolean;
101
+ created_at: string | null;
102
+ updated_at: string | null;
103
+ }
104
+ interface IngredientTranslation {
105
+ language_code: string;
106
+ translated_name: string;
107
+ is_validated: boolean;
108
+ }
109
+ interface AnalyzeResult {
110
+ queue_id: string | null;
111
+ status: 'pending' | 'processing' | 'completed' | 'error';
112
+ ingredients: AnalyzedIngredient[];
113
+ total_count: number;
114
+ halal_count: number;
115
+ haram_count: number;
116
+ mushbooh_count: number;
117
+ unknown_count: number;
118
+ completion_percentage: number;
119
+ error: string | null;
120
+ }
121
+ interface AnalyzedIngredient {
122
+ ocr_item_id: string | null;
123
+ ingredient_id: number | null;
124
+ original_text: string;
125
+ display_name: string;
126
+ source_language: string;
127
+ halal_status: HalalStatus;
128
+ confidence: number;
129
+ source: IngredientSource;
130
+ explanation: string | null;
131
+ image_url: string | null;
132
+ is_verified: boolean;
133
+ has_grounding: boolean;
134
+ }
135
+
136
+ declare class HalalScanner {
137
+ private readonly baseUrl;
138
+ private readonly deviceId;
139
+ private readonly timeout;
140
+ constructor(options?: ScannerOptions);
141
+ /**
142
+ * Search for ingredients by name.
143
+ *
144
+ * @example
145
+ * const results = await scanner.search('gelatin');
146
+ * console.log(results.items[0].halal_status); // "HARAM"
147
+ */
148
+ search(query: string, params?: Partial<SearchParams>): Promise<SearchResult>;
149
+ /**
150
+ * Get detailed information about a specific ingredient.
151
+ *
152
+ * @param lang - Language code (e.g., "en", "ar", "no")
153
+ * @param name - Ingredient URL slug (e.g., "gelatin", "sugar")
154
+ *
155
+ * @example
156
+ * const detail = await scanner.check('en', 'gelatin');
157
+ * console.log(detail.explanation);
158
+ */
159
+ check(lang: string, name: string): Promise<IngredientDetail>;
160
+ /**
161
+ * Analyze a list of ingredients for halal status.
162
+ *
163
+ * @param ingredients - Array of ingredient names to check
164
+ *
165
+ * @example
166
+ * const result = await scanner.analyze(['gelatin', 'sugar', 'E471']);
167
+ * console.log(`${result.halal_count} halal, ${result.haram_count} haram`);
168
+ */
169
+ analyze(ingredients: string[]): Promise<AnalyzeResult>;
170
+ private request;
171
+ }
172
+
173
+ declare class HalalScannerError extends Error {
174
+ readonly statusCode?: number | undefined;
175
+ readonly endpoint?: string | undefined;
176
+ constructor(message: string, statusCode?: number | undefined, endpoint?: string | undefined);
177
+ }
178
+ declare class RateLimitError extends HalalScannerError {
179
+ constructor(endpoint: string);
180
+ }
181
+ declare class NetworkError extends HalalScannerError {
182
+ constructor(message: string, endpoint: string);
183
+ }
184
+
185
+ export { type AIOpinion, type AIReviewSummary, type AnalyzeResult, type AnalyzedIngredient, type FuzzySuggestion, HalalScanner, HalalScannerError, type HalalStatus, type IngredientDetail, type IngredientSource, type IngredientSummary, type IngredientTranslation, type LanguageMatch, NetworkError, RateLimitError, type ScannerOptions, type SearchParams, type SearchResult, type SimilarItem };
package/dist/index.js ADDED
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ HalalScanner: () => HalalScanner,
24
+ HalalScannerError: () => HalalScannerError,
25
+ NetworkError: () => NetworkError,
26
+ RateLimitError: () => RateLimitError
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/errors.ts
31
+ var HalalScannerError = class extends Error {
32
+ constructor(message, statusCode, endpoint) {
33
+ super(message);
34
+ this.statusCode = statusCode;
35
+ this.endpoint = endpoint;
36
+ this.name = "HalalScannerError";
37
+ }
38
+ };
39
+ var RateLimitError = class extends HalalScannerError {
40
+ constructor(endpoint) {
41
+ super("Rate limit exceeded. Please wait before making more requests.", 429, endpoint);
42
+ this.name = "RateLimitError";
43
+ }
44
+ };
45
+ var NetworkError = class extends HalalScannerError {
46
+ constructor(message, endpoint) {
47
+ super(`Network error: ${message}`, void 0, endpoint);
48
+ this.name = "NetworkError";
49
+ }
50
+ };
51
+
52
+ // src/client.ts
53
+ var DEFAULT_BASE_URL = "https://halallens.no/api";
54
+ var DEFAULT_TIMEOUT = 1e4;
55
+ function generateDeviceId() {
56
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
57
+ let id = "hs-";
58
+ for (let i = 0; i < 16; i++) {
59
+ id += chars[Math.floor(Math.random() * chars.length)];
60
+ }
61
+ return id;
62
+ }
63
+ var HalalScanner = class {
64
+ constructor(options = {}) {
65
+ this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
66
+ this.deviceId = options.deviceId || generateDeviceId();
67
+ this.timeout = options.timeout || DEFAULT_TIMEOUT;
68
+ }
69
+ /**
70
+ * Search for ingredients by name.
71
+ *
72
+ * @example
73
+ * const results = await scanner.search('gelatin');
74
+ * console.log(results.items[0].halal_status); // "HARAM"
75
+ */
76
+ async search(query, params = {}) {
77
+ const searchParams = new URLSearchParams({ search: query });
78
+ if (params.status) searchParams.set("status", params.status);
79
+ if (params.category) searchParams.set("category", params.category);
80
+ if (params.language) searchParams.set("language", params.language);
81
+ if (params.verifiedOnly) searchParams.set("verified_only", "true");
82
+ if (params.eNumber) searchParams.set("e_number", "true");
83
+ if (params.page) searchParams.set("page", String(params.page));
84
+ if (params.perPage) searchParams.set("per_page", String(params.perPage));
85
+ return this.request(
86
+ `/v1/public/ingredients?${searchParams.toString()}`
87
+ );
88
+ }
89
+ /**
90
+ * Get detailed information about a specific ingredient.
91
+ *
92
+ * @param lang - Language code (e.g., "en", "ar", "no")
93
+ * @param name - Ingredient URL slug (e.g., "gelatin", "sugar")
94
+ *
95
+ * @example
96
+ * const detail = await scanner.check('en', 'gelatin');
97
+ * console.log(detail.explanation);
98
+ */
99
+ async check(lang, name) {
100
+ return this.request(
101
+ `/v1/public/ingredients/${encodeURIComponent(lang)}/${encodeURIComponent(name)}`
102
+ );
103
+ }
104
+ /**
105
+ * Analyze a list of ingredients for halal status.
106
+ *
107
+ * @param ingredients - Array of ingredient names to check
108
+ *
109
+ * @example
110
+ * const result = await scanner.analyze(['gelatin', 'sugar', 'E471']);
111
+ * console.log(`${result.halal_count} halal, ${result.haram_count} haram`);
112
+ */
113
+ async analyze(ingredients) {
114
+ return this.request("/v1/queue/ingredients/json", {
115
+ method: "POST",
116
+ headers: { "Content-Type": "application/json" },
117
+ body: JSON.stringify({
118
+ ingredients: ingredients.map((name) => ({
119
+ name,
120
+ halal_status: "UNKNOWN",
121
+ language: "en",
122
+ confidence: 0
123
+ })),
124
+ device_id: this.deviceId
125
+ })
126
+ });
127
+ }
128
+ async request(path, init) {
129
+ const url = `${this.baseUrl}${path}`;
130
+ const controller = new AbortController();
131
+ const timer = setTimeout(() => controller.abort(), this.timeout);
132
+ try {
133
+ const response = await fetch(url, {
134
+ ...init,
135
+ signal: controller.signal,
136
+ headers: {
137
+ Accept: "application/json",
138
+ ...init?.headers
139
+ }
140
+ });
141
+ if (response.status === 429) {
142
+ throw new RateLimitError(path);
143
+ }
144
+ if (!response.ok) {
145
+ const text = await response.text().catch(() => "");
146
+ throw new HalalScannerError(
147
+ text || `HTTP ${response.status}`,
148
+ response.status,
149
+ path
150
+ );
151
+ }
152
+ return await response.json();
153
+ } catch (error) {
154
+ if (error instanceof HalalScannerError) throw error;
155
+ if (error instanceof DOMException && error.name === "AbortError") {
156
+ throw new NetworkError("Request timed out", path);
157
+ }
158
+ throw new NetworkError(
159
+ error instanceof Error ? error.message : "Unknown error",
160
+ path
161
+ );
162
+ } finally {
163
+ clearTimeout(timer);
164
+ }
165
+ }
166
+ };
167
+ // Annotate the CommonJS export names for ESM import in node:
168
+ 0 && (module.exports = {
169
+ HalalScanner,
170
+ HalalScannerError,
171
+ NetworkError,
172
+ RateLimitError
173
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,143 @@
1
+ // src/errors.ts
2
+ var HalalScannerError = class extends Error {
3
+ constructor(message, statusCode, endpoint) {
4
+ super(message);
5
+ this.statusCode = statusCode;
6
+ this.endpoint = endpoint;
7
+ this.name = "HalalScannerError";
8
+ }
9
+ };
10
+ var RateLimitError = class extends HalalScannerError {
11
+ constructor(endpoint) {
12
+ super("Rate limit exceeded. Please wait before making more requests.", 429, endpoint);
13
+ this.name = "RateLimitError";
14
+ }
15
+ };
16
+ var NetworkError = class extends HalalScannerError {
17
+ constructor(message, endpoint) {
18
+ super(`Network error: ${message}`, void 0, endpoint);
19
+ this.name = "NetworkError";
20
+ }
21
+ };
22
+
23
+ // src/client.ts
24
+ var DEFAULT_BASE_URL = "https://halallens.no/api";
25
+ var DEFAULT_TIMEOUT = 1e4;
26
+ function generateDeviceId() {
27
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
28
+ let id = "hs-";
29
+ for (let i = 0; i < 16; i++) {
30
+ id += chars[Math.floor(Math.random() * chars.length)];
31
+ }
32
+ return id;
33
+ }
34
+ var HalalScanner = class {
35
+ constructor(options = {}) {
36
+ this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
37
+ this.deviceId = options.deviceId || generateDeviceId();
38
+ this.timeout = options.timeout || DEFAULT_TIMEOUT;
39
+ }
40
+ /**
41
+ * Search for ingredients by name.
42
+ *
43
+ * @example
44
+ * const results = await scanner.search('gelatin');
45
+ * console.log(results.items[0].halal_status); // "HARAM"
46
+ */
47
+ async search(query, params = {}) {
48
+ const searchParams = new URLSearchParams({ search: query });
49
+ if (params.status) searchParams.set("status", params.status);
50
+ if (params.category) searchParams.set("category", params.category);
51
+ if (params.language) searchParams.set("language", params.language);
52
+ if (params.verifiedOnly) searchParams.set("verified_only", "true");
53
+ if (params.eNumber) searchParams.set("e_number", "true");
54
+ if (params.page) searchParams.set("page", String(params.page));
55
+ if (params.perPage) searchParams.set("per_page", String(params.perPage));
56
+ return this.request(
57
+ `/v1/public/ingredients?${searchParams.toString()}`
58
+ );
59
+ }
60
+ /**
61
+ * Get detailed information about a specific ingredient.
62
+ *
63
+ * @param lang - Language code (e.g., "en", "ar", "no")
64
+ * @param name - Ingredient URL slug (e.g., "gelatin", "sugar")
65
+ *
66
+ * @example
67
+ * const detail = await scanner.check('en', 'gelatin');
68
+ * console.log(detail.explanation);
69
+ */
70
+ async check(lang, name) {
71
+ return this.request(
72
+ `/v1/public/ingredients/${encodeURIComponent(lang)}/${encodeURIComponent(name)}`
73
+ );
74
+ }
75
+ /**
76
+ * Analyze a list of ingredients for halal status.
77
+ *
78
+ * @param ingredients - Array of ingredient names to check
79
+ *
80
+ * @example
81
+ * const result = await scanner.analyze(['gelatin', 'sugar', 'E471']);
82
+ * console.log(`${result.halal_count} halal, ${result.haram_count} haram`);
83
+ */
84
+ async analyze(ingredients) {
85
+ return this.request("/v1/queue/ingredients/json", {
86
+ method: "POST",
87
+ headers: { "Content-Type": "application/json" },
88
+ body: JSON.stringify({
89
+ ingredients: ingredients.map((name) => ({
90
+ name,
91
+ halal_status: "UNKNOWN",
92
+ language: "en",
93
+ confidence: 0
94
+ })),
95
+ device_id: this.deviceId
96
+ })
97
+ });
98
+ }
99
+ async request(path, init) {
100
+ const url = `${this.baseUrl}${path}`;
101
+ const controller = new AbortController();
102
+ const timer = setTimeout(() => controller.abort(), this.timeout);
103
+ try {
104
+ const response = await fetch(url, {
105
+ ...init,
106
+ signal: controller.signal,
107
+ headers: {
108
+ Accept: "application/json",
109
+ ...init?.headers
110
+ }
111
+ });
112
+ if (response.status === 429) {
113
+ throw new RateLimitError(path);
114
+ }
115
+ if (!response.ok) {
116
+ const text = await response.text().catch(() => "");
117
+ throw new HalalScannerError(
118
+ text || `HTTP ${response.status}`,
119
+ response.status,
120
+ path
121
+ );
122
+ }
123
+ return await response.json();
124
+ } catch (error) {
125
+ if (error instanceof HalalScannerError) throw error;
126
+ if (error instanceof DOMException && error.name === "AbortError") {
127
+ throw new NetworkError("Request timed out", path);
128
+ }
129
+ throw new NetworkError(
130
+ error instanceof Error ? error.message : "Unknown error",
131
+ path
132
+ );
133
+ } finally {
134
+ clearTimeout(timer);
135
+ }
136
+ }
137
+ };
138
+ export {
139
+ HalalScanner,
140
+ HalalScannerError,
141
+ NetworkError,
142
+ RateLimitError
143
+ };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@halallens/halal-scanner",
3
+ "version": "1.0.0",
4
+ "description": "Free halal food scanner - check ingredients instantly. Powered by HalalLens AI.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format cjs,esm --dts",
22
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "lint": "tsc --noEmit",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "keywords": [
29
+ "halal",
30
+ "halal-scanner",
31
+ "food-scanner",
32
+ "ingredient-checker",
33
+ "halal-food",
34
+ "halal-checker",
35
+ "free",
36
+ "open-source",
37
+ "e-numbers",
38
+ "food-ingredients",
39
+ "halal-status",
40
+ "muslim",
41
+ "islamic",
42
+ "food-safety"
43
+ ],
44
+ "author": "HalalLens <mac@halallens.no> (https://halallens.no)",
45
+ "license": "MIT",
46
+ "homepage": "https://halallens.no",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/halallens-no/halal-scanner.git"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/halallens-no/halal-scanner/issues"
53
+ },
54
+ "devDependencies": {
55
+ "tsup": "^8.0.0",
56
+ "typescript": "^5.4.0",
57
+ "vitest": "^1.6.0"
58
+ }
59
+ }