@majikah/majik-api 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +67 -0
- package/README.md +185 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/majik-api.d.ts +173 -0
- package/dist/majik-api.js +642 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +27 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Copyright (c) 2025 Josef Elijah Delos Santos Fabian
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
Apache License
|
|
18
|
+
Version 2.0, January 2004
|
|
19
|
+
http://www.apache.org/licenses/
|
|
20
|
+
|
|
21
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
22
|
+
|
|
23
|
+
1. Definitions.
|
|
24
|
+
|
|
25
|
+
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
|
26
|
+
|
|
27
|
+
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
|
28
|
+
|
|
29
|
+
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
|
30
|
+
|
|
31
|
+
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
|
32
|
+
|
|
33
|
+
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
|
34
|
+
|
|
35
|
+
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
|
36
|
+
|
|
37
|
+
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
|
38
|
+
|
|
39
|
+
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
|
40
|
+
|
|
41
|
+
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
|
42
|
+
|
|
43
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
|
44
|
+
|
|
45
|
+
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
|
46
|
+
|
|
47
|
+
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
|
48
|
+
|
|
49
|
+
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
|
50
|
+
|
|
51
|
+
You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
|
52
|
+
You must cause any modified files to carry prominent notices stating that You changed the files; and
|
|
53
|
+
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
|
54
|
+
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
|
55
|
+
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
|
56
|
+
|
|
57
|
+
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
|
58
|
+
|
|
59
|
+
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
|
60
|
+
|
|
61
|
+
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
|
62
|
+
|
|
63
|
+
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
|
64
|
+
|
|
65
|
+
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
|
66
|
+
|
|
67
|
+
END OF TERMS AND CONDITIONS
|
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Majik API
|
|
2
|
+
|
|
3
|
+
[](https://thezelijah.world) 
|
|
4
|
+
|
|
5
|
+
**Majik API** is an API key management library designed for the Majikah ecosystem. It provides a robust, developer-friendly interface for creating, hashing, and managing API keys with built-in support for rate limiting, IP/Domain whitelisting, and secure rotation.
|
|
6
|
+
|
|
7
|
+
   [](https://opensource.org/licenses/Apache-2.0) 
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
- [Majik API](#majik-api)
|
|
13
|
+
- [Technical Architecture](#technical-architecture)
|
|
14
|
+
- [1. Security via SHA-256 Hashing](#1-security-via-sha-256-hashing)
|
|
15
|
+
- [2. Identity Persistence (UUID)](#2-identity-persistence-uuid)
|
|
16
|
+
- [3. Rate Limit Enforcement](#3-rate-limit-enforcement)
|
|
17
|
+
- [Features](#features)
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Quick Start](#quick-start)
|
|
20
|
+
- [API Reference](#api-reference)
|
|
21
|
+
- [Instance Getters](#instance-getters)
|
|
22
|
+
- [Static Methods](#static-methods)
|
|
23
|
+
- [Instance Methods: Key Management](#instance-methods-key-management)
|
|
24
|
+
- [Instance Methods: Constraints \& Whitelisting](#instance-methods-constraints--whitelisting)
|
|
25
|
+
- [Contributing](#contributing)
|
|
26
|
+
- [License](#license)
|
|
27
|
+
- [Author](#author)
|
|
28
|
+
- [About the Developer](#about-the-developer)
|
|
29
|
+
- [Contact](#contact)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
## Technical Architecture
|
|
36
|
+
|
|
37
|
+
### 1. Security via SHA-256 Hashing
|
|
38
|
+
The library ensures that the raw plaintext API key is never stored within the permanent state of the object. Upon creation or rotation, the key is immediately hashed using SHA-256. The rawApiKey property is a temporary field provided only during the initial generation/rotation event to allow for one-time display to the user.
|
|
39
|
+
|
|
40
|
+
### 2. Identity Persistence (UUID)
|
|
41
|
+
Each key instance maintains a stable id (UUIDv4). This ID remains constant even if the API key text is rotated, allowing for consistent Foreign Key relationships in databases (like Supabase) or audit logs.
|
|
42
|
+
|
|
43
|
+
### 3. Rate Limit Enforcement
|
|
44
|
+
The class includes built-in logic to normalize and validate rate limits. It enforces a "Safe Limit" ceiling of 500 requests per minute by default. Any attempt to set a limit higher than this requires an explicit bypassSafeLimit flag.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- **Automated Lifecycle**: Manage active, restricted, and expired statuses automatically based on timestamps and boolean flags.
|
|
51
|
+
|
|
52
|
+
- **IP Whitelisting**: Supports individual IP addresses and CIDR notation validation.
|
|
53
|
+
|
|
54
|
+
- **Domain Whitelisting**: Validates domains with support for wildcard (*) subdomains.
|
|
55
|
+
|
|
56
|
+
- **Status Calculation**: Dynamic status getter that evaluates if a key is revoked, expired, or active.
|
|
57
|
+
|
|
58
|
+
- **JSON Serialization**: Methods to export/import the class state for database storage (storing only hashes, never raw keys).
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Using npm
|
|
67
|
+
npm install @majikah/majik-api
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Quick Start
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { MajikAPI } from '@majikah/majik-api';
|
|
77
|
+
|
|
78
|
+
// Create a new key for a specific owner
|
|
79
|
+
const key = MajikAPI.create('owner_user_id', undefined, {
|
|
80
|
+
name: 'Production Environment Key'
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Capture the raw key once (it won't be in the object after serialization)
|
|
84
|
+
console.log('Provide this to user:', key.rawApiKey);
|
|
85
|
+
|
|
86
|
+
// Save to DB (this only saves the SHA-256 hash)
|
|
87
|
+
const data = key.toJSON();
|
|
88
|
+
// ... save data to your database ...
|
|
89
|
+
|
|
90
|
+
// Verifying a request later
|
|
91
|
+
const isValid = key.verify('key_provided_by_client'); // returns boolean
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## API Reference
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
### Instance Getters
|
|
102
|
+
|
|
103
|
+
| Property | Type | Description |
|
|
104
|
+
| :--- | :--- | :--- |
|
|
105
|
+
| `id` | `string` | The stable UUIDv4 identifier for the record. |
|
|
106
|
+
| `ownerId` | `string` | The identifier of the key owner (e.g., a user ID). |
|
|
107
|
+
| `name` | `string` | Human-readable label for the API key. |
|
|
108
|
+
| `apiKey` | `string` | The **SHA-256 hash** of the API key. |
|
|
109
|
+
| `rawApiKey` | `string \| undefined` | The plaintext key. Only populated immediately after `create()` or `rotate()`. |
|
|
110
|
+
| `timestamp` | `string` | ISO 8601 string of the last rotation or creation time. |
|
|
111
|
+
| `restricted` | `boolean` | Manual toggle indicating if the key is administratively disabled. |
|
|
112
|
+
| `validUntil` | `string \| null` | ISO 8601 expiration date, or `null` if the key never expires. |
|
|
113
|
+
| `settings` | `MajikAPISettings` | A structured clone of the key's rate limits and whitelist configurations. |
|
|
114
|
+
| `status` | `'revoked' \| 'expired' \| 'active'` | Returns the current operational state based on internal flags and time. |
|
|
115
|
+
| `msUntilExpiry` | `number` | Milliseconds remaining until `validUntil`. Returns `-1` if no expiry is set. |
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
### Static Methods
|
|
120
|
+
|
|
121
|
+
| Method | Parameters | Return Type | Description |
|
|
122
|
+
| :--- | :--- | :--- | :--- |
|
|
123
|
+
| `create` | `ownerID: string`, `text?: string`, `options?: MajikAPICreateOptions` | `MajikAPI` | Instantiates a new key. Generates a random UUID as the key if `text` is omitted. |
|
|
124
|
+
| `fromJSON` | `json: MajikAPIJSON` | `MajikAPI` | Reconstructs an instance from a serialized data object. |
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### Instance Methods: Key Management
|
|
129
|
+
|
|
130
|
+
| Method | Parameters | Return Type | Description |
|
|
131
|
+
| :--- | :--- | :--- | :--- |
|
|
132
|
+
| `verify` | `text: string` | `boolean` | Hashes the input string and performs a constant-time comparison against the stored hash. |
|
|
133
|
+
| `rotate` | `text?: string` | `void` | Generates a new hash and updates the timestamp. Populates `rawApiKey` with the new plaintext. |
|
|
134
|
+
| `revoke` | *None* | `void` | Permanently disables the key by setting `restricted` to `true` and the expiry to `1970-01-01`. |
|
|
135
|
+
| `isActive` | *None* | `boolean` | Returns `true` if the key is not restricted and not expired. |
|
|
136
|
+
| `setName` | `name: string` | `void` | Updates the human-readable label. |
|
|
137
|
+
| `setRestricted` | `restricted: boolean` | `void` | Manually enables or disables the key. |
|
|
138
|
+
| `toJSON` | *None* | `MajikAPIJSON` | Serializes the instance into a plain object for database storage. |
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### Instance Methods: Constraints & Whitelisting
|
|
143
|
+
|
|
144
|
+
| Method | Parameters | Return Type | Description |
|
|
145
|
+
| :--- | :--- | :--- | :--- |
|
|
146
|
+
| `setExpiry` | `date: Date \| string \| null` | `void` | Updates the `valid_until` property. Accepts Date objects or ISO strings. |
|
|
147
|
+
| `setRateLimit` | `amount: number`, `freq: RateLimitFrequency`, `bypass?: boolean` | `void` | Sets requests per window. Caps at 500 req/min unless `bypassSafeLimit` is true. |
|
|
148
|
+
| `enableIPWhitelist` | *None* | `void` | Enables the IP restriction check. |
|
|
149
|
+
| `disableIPWhitelist` | *None* | `void` | Disables the IP restriction check. |
|
|
150
|
+
| `addIP` | `ip: string` | `void` | Adds an IPv4, IPv6, or CIDR range to the whitelist. |
|
|
151
|
+
| `removeIP` | `ip: string` | `void` | Removes a specific IP/range from the whitelist. |
|
|
152
|
+
| `enableDomainWhitelist`| *None* | `void` | Enables the Domain restriction check. |
|
|
153
|
+
| `disableDomainWhitelist`| *None* | `void` | Disables the Domain restriction check. |
|
|
154
|
+
| `addDomain` | `domain: string` | `void` | Adds a domain (supports `*.example.com` wildcards) to the whitelist. |
|
|
155
|
+
| `removeDomain` | `domain: string` | `void` | Removes a specific domain from the whitelist. |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Contributing
|
|
160
|
+
|
|
161
|
+
If you want to contribute or help extend support to more platforms, reach out via email. All contributions are welcome!
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
[Apache-2.0](LICENSE) — free for personal and commercial use.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
## Author
|
|
171
|
+
|
|
172
|
+
Made with 💙 by [@thezelijah](https://github.com/jedlsf)
|
|
173
|
+
|
|
174
|
+
## About the Developer
|
|
175
|
+
|
|
176
|
+
- **Developer**: Josef Elijah Fabian
|
|
177
|
+
- **GitHub**: [https://github.com/jedlsf](https://github.com/jedlsf)
|
|
178
|
+
- **Project Repository**: [https://github.com/Majikah/majik-api](https://github.com/Majikah/majik-api)
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Contact
|
|
183
|
+
|
|
184
|
+
- **Business Email**: [business@thezelijah.world](mailto:business@thezelijah.world)
|
|
185
|
+
- **Official Website**: [https://www.thezelijah.world](https://www.thezelijah.world)
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./majik-api";
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { DomainWhitelist, IPWhitelist, MajikAPICreateOptions, MajikAPIJSON, MajikAPISettings, RateLimit, RateLimitFrequency } from "./types";
|
|
2
|
+
export declare const DEFAULT_RATE_LIMIT: RateLimit;
|
|
3
|
+
/**
|
|
4
|
+
* Hard ceiling for any rate limit set on a MajikAPI key.
|
|
5
|
+
* No key — regardless of trust level — may exceed this without bypassSafeLimit.
|
|
6
|
+
* Expressed in req/min for normalisation purposes; stored as a RateLimit for
|
|
7
|
+
* consistency with the rest of the API.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MAX_RATE_LIMIT: RateLimit;
|
|
10
|
+
export declare class MajikAPI {
|
|
11
|
+
private readonly _id;
|
|
12
|
+
private readonly _owner_id;
|
|
13
|
+
private _name;
|
|
14
|
+
private _api_key;
|
|
15
|
+
private _raw_api_key;
|
|
16
|
+
private readonly _timestamp;
|
|
17
|
+
private _restricted;
|
|
18
|
+
private _valid_until;
|
|
19
|
+
private _settings;
|
|
20
|
+
private constructor();
|
|
21
|
+
/**
|
|
22
|
+
* Create a brand-new MajikAPI key instance.
|
|
23
|
+
*
|
|
24
|
+
* @param ownerID - UUID of the user who owns this key. Required.
|
|
25
|
+
* @param text - Optional raw key text. If omitted, a UUIDv4 is generated.
|
|
26
|
+
* @param options - Optional name, expiry, restrictions, and settings.
|
|
27
|
+
*
|
|
28
|
+
* After creation, `instance.rawApiKey` holds the plaintext key. This is the
|
|
29
|
+
* only moment it is accessible. Store it safely — it cannot be recovered.
|
|
30
|
+
*/
|
|
31
|
+
static create(ownerID: string, text?: string, options?: MajikAPICreateOptions): MajikAPI;
|
|
32
|
+
/**
|
|
33
|
+
* Reconstruct a MajikAPI instance from a serialised MajikAPIJSON object.
|
|
34
|
+
* Accepts the raw output of `toJSON()`, a Supabase row, or a Redis cache hit.
|
|
35
|
+
*
|
|
36
|
+
* `raw_api_key` is intentionally NOT restored — it is never in the JSON.
|
|
37
|
+
*/
|
|
38
|
+
static fromJSON(data: MajikAPIJSON): MajikAPI;
|
|
39
|
+
/**
|
|
40
|
+
* Serialise to a plain JSON-safe object matching MajikAPIJSON.
|
|
41
|
+
* Safe to store in Supabase or cache in Redis.
|
|
42
|
+
* raw_api_key is NEVER included.
|
|
43
|
+
*/
|
|
44
|
+
toJSON(): MajikAPIJSON;
|
|
45
|
+
/**
|
|
46
|
+
* Assert the integrity of all fields on this instance.
|
|
47
|
+
* Throws a descriptive error for the first field that fails.
|
|
48
|
+
*/
|
|
49
|
+
validate(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Hash `text` and compare against the stored api_key hash.
|
|
52
|
+
* This is the correct way to verify an incoming key at your API gateway.
|
|
53
|
+
* Returns true if the key matches.
|
|
54
|
+
*/
|
|
55
|
+
verify(text: string): boolean;
|
|
56
|
+
/** Alias for verify(). */
|
|
57
|
+
matches(text: string): boolean;
|
|
58
|
+
/** Returns true if valid_until is set and has passed. Always false if null. */
|
|
59
|
+
isExpired(): boolean;
|
|
60
|
+
/** Returns true only if the key is not expired and not restricted. */
|
|
61
|
+
isActive(): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Set the rate limit for this key.
|
|
64
|
+
*
|
|
65
|
+
* The effective rate (normalised to req/min) cannot exceed MAX_RATE_LIMIT
|
|
66
|
+
* (500 req/min). Attempting to set a higher rate will throw unless
|
|
67
|
+
* bypassSafeLimit is explicitly passed as true.
|
|
68
|
+
*
|
|
69
|
+
* @param amount - Number of allowed requests per frequency window.
|
|
70
|
+
* @param frequency - The time window unit.
|
|
71
|
+
* @param bypassSafeLimit - When true, skips the MAX_RATE_LIMIT ceiling check.
|
|
72
|
+
* Defaults to false. Use with caution.
|
|
73
|
+
*/
|
|
74
|
+
setRateLimit(amount: number, frequency: RateLimitFrequency, bypassSafeLimit?: boolean): void;
|
|
75
|
+
/** Reset the rate limit back to DEFAULT_RATE_LIMIT. */
|
|
76
|
+
resetRateLimit(): void;
|
|
77
|
+
/**
|
|
78
|
+
* Rotate the API key. Generates a new raw key (or accepts a provided one),
|
|
79
|
+
* hashes it, and replaces _api_key. The stable _id and _owner_id are
|
|
80
|
+
* untouched, so all FK references in audit/event tables remain valid.
|
|
81
|
+
*
|
|
82
|
+
* After rotation, `rawApiKey` holds the new plaintext key. This is the only
|
|
83
|
+
* moment it is accessible. The old key is immediately invalidated in-memory.
|
|
84
|
+
*
|
|
85
|
+
* IMPORTANT: After calling rotate(), you must:
|
|
86
|
+
* 1. Save the new toJSON() to Supabase (updates the api_key column).
|
|
87
|
+
* 2. Delete the old Redis cache entry (old hash key is now stale).
|
|
88
|
+
* 3. Show the caller rawApiKey before discarding this instance.
|
|
89
|
+
*
|
|
90
|
+
* @param text - Optional new raw key. If omitted, a UUIDv4 is generated.
|
|
91
|
+
*/
|
|
92
|
+
rotate(text?: string): void;
|
|
93
|
+
/** Rename this key. */
|
|
94
|
+
rename(name: string): void;
|
|
95
|
+
/**
|
|
96
|
+
* Set or clear the expiry date.
|
|
97
|
+
* Pass null to make the key never expire.
|
|
98
|
+
*/
|
|
99
|
+
setExpiry(date: Date | string | null): void;
|
|
100
|
+
/** Disable this key without deleting it. */
|
|
101
|
+
restrict(): void;
|
|
102
|
+
/** Re-enable a previously restricted key. */
|
|
103
|
+
unrestrict(): void;
|
|
104
|
+
/**
|
|
105
|
+
* Permanently revoke this key. Sets valid_until to epoch (always expired)
|
|
106
|
+
* and marks it restricted. Both flags must be cleared to undo this — prefer
|
|
107
|
+
* deleting the Supabase row and creating a new key instead.
|
|
108
|
+
*/
|
|
109
|
+
revoke(): void;
|
|
110
|
+
enableIPWhitelist(): void;
|
|
111
|
+
disableIPWhitelist(): void;
|
|
112
|
+
addIP(ip: string): void;
|
|
113
|
+
removeIP(ip: string): void;
|
|
114
|
+
setIPWhitelist(addresses: string[]): void;
|
|
115
|
+
clearIPWhitelist(): void;
|
|
116
|
+
enableDomainWhitelist(): void;
|
|
117
|
+
disableDomainWhitelist(): void;
|
|
118
|
+
addDomain(domain: string): void;
|
|
119
|
+
removeDomain(domain: string): void;
|
|
120
|
+
setDomainWhitelist(domains: string[]): void;
|
|
121
|
+
clearDomainWhitelist(): void;
|
|
122
|
+
setAllowedMethods(methods: string[]): void;
|
|
123
|
+
clearAllowedMethods(): void;
|
|
124
|
+
setMetadata(key: string, value: unknown): void;
|
|
125
|
+
getMetadata(key: string): unknown;
|
|
126
|
+
deleteMetadata(key: string): void;
|
|
127
|
+
clearMetadata(): void;
|
|
128
|
+
/** The key's own stable UUID. Primary key in Supabase. */
|
|
129
|
+
get id(): string;
|
|
130
|
+
/** UUID of the user who owns this key. FK to auth.users. */
|
|
131
|
+
get ownerId(): string;
|
|
132
|
+
get name(): string;
|
|
133
|
+
/**
|
|
134
|
+
* The SHA-256 hash of the raw key. This is what is stored in Supabase and
|
|
135
|
+
* used as the Redis cache key prefix. Never the plaintext.
|
|
136
|
+
*/
|
|
137
|
+
get apiKey(): string;
|
|
138
|
+
/**
|
|
139
|
+
* The raw plaintext key. Only defined immediately after create() or rotate().
|
|
140
|
+
* Undefined after fromJSON() or any serialise/deserialise round-trip.
|
|
141
|
+
* Treat this like a password — show it once and discard.
|
|
142
|
+
*/
|
|
143
|
+
get rawApiKey(): string | undefined;
|
|
144
|
+
get createdAt(): Date;
|
|
145
|
+
get timestamp(): string;
|
|
146
|
+
get restricted(): boolean;
|
|
147
|
+
get validUntil(): Date | null;
|
|
148
|
+
/** Returns a deep clone — mutations to the returned object have no effect. */
|
|
149
|
+
get settings(): Readonly<MajikAPISettings>;
|
|
150
|
+
get rateLimit(): Readonly<RateLimit>;
|
|
151
|
+
get ipWhitelist(): Readonly<IPWhitelist>;
|
|
152
|
+
get domainWhitelist(): Readonly<DomainWhitelist>;
|
|
153
|
+
get allowedMethods(): string[];
|
|
154
|
+
/**
|
|
155
|
+
* Milliseconds until this key expires.
|
|
156
|
+
* Returns -1 if the key never expires (valid_until is null).
|
|
157
|
+
* Returns 0 if the key has already expired.
|
|
158
|
+
*/
|
|
159
|
+
get msUntilExpiry(): number;
|
|
160
|
+
/**
|
|
161
|
+
* Human-readable lifecycle status.
|
|
162
|
+
*
|
|
163
|
+
* "active" — valid, not restricted, not expired.
|
|
164
|
+
* "restricted" — manually disabled, not expired.
|
|
165
|
+
* "expired" — past valid_until date.
|
|
166
|
+
* "revoked" — permanently invalidated via revoke().
|
|
167
|
+
*/
|
|
168
|
+
get status(): "active" | "restricted" | "expired" | "revoked";
|
|
169
|
+
private static parseDate;
|
|
170
|
+
private static validateSettings;
|
|
171
|
+
toString(): string;
|
|
172
|
+
}
|
|
173
|
+
export default MajikAPI;
|
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import { generateID, sha256 } from "./utils";
|
|
2
|
+
// ─────────────────────────────────────────────
|
|
3
|
+
// Constants
|
|
4
|
+
// ─────────────────────────────────────────────
|
|
5
|
+
export const DEFAULT_RATE_LIMIT = {
|
|
6
|
+
amount: 100,
|
|
7
|
+
frequency: "minutes",
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Hard ceiling for any rate limit set on a MajikAPI key.
|
|
11
|
+
* No key — regardless of trust level — may exceed this without bypassSafeLimit.
|
|
12
|
+
* Expressed in req/min for normalisation purposes; stored as a RateLimit for
|
|
13
|
+
* consistency with the rest of the API.
|
|
14
|
+
*/
|
|
15
|
+
export const MAX_RATE_LIMIT = {
|
|
16
|
+
amount: 500,
|
|
17
|
+
frequency: "minutes",
|
|
18
|
+
};
|
|
19
|
+
/** Multipliers to convert each frequency unit into requests-per-minute. */
|
|
20
|
+
const TO_MINUTES = {
|
|
21
|
+
seconds: 1 / 60,
|
|
22
|
+
minutes: 1,
|
|
23
|
+
hours: 60,
|
|
24
|
+
};
|
|
25
|
+
// ─────────────────────────────────────────────
|
|
26
|
+
// Validation Helpers
|
|
27
|
+
// ─────────────────────────────────────────────
|
|
28
|
+
function assertString(value, label) {
|
|
29
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
30
|
+
throw new TypeError(`[MajikAPI] "${label}" must be a non-empty string. Received: ${JSON.stringify(value)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function assertPositiveInteger(value, label) {
|
|
34
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
35
|
+
throw new RangeError(`[MajikAPI] "${label}" must be a positive integer. Received: ${JSON.stringify(value)}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function assertRateLimitFrequency(value, label) {
|
|
39
|
+
const valid = ["seconds", "minutes", "hours"];
|
|
40
|
+
if (!valid.includes(value)) {
|
|
41
|
+
throw new TypeError(`[MajikAPI] "${label}" must be one of: ${valid.join(", ")}. Received: ${JSON.stringify(value)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function assertBoolean(value, label) {
|
|
45
|
+
if (typeof value !== "boolean") {
|
|
46
|
+
throw new TypeError(`[MajikAPI] "${label}" must be a boolean. Received: ${JSON.stringify(value)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function assertStringArray(value, label) {
|
|
50
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
|
|
51
|
+
throw new TypeError(`[MajikAPI] "${label}" must be an array of strings. Received: ${JSON.stringify(value)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function isValidIPv4(ip) {
|
|
55
|
+
return (/^(\d{1,3}\.){3}\d{1,3}$/.test(ip) &&
|
|
56
|
+
ip.split(".").every((o) => parseInt(o) <= 255));
|
|
57
|
+
}
|
|
58
|
+
function isValidIPv6(ip) {
|
|
59
|
+
return /^[0-9a-fA-F:]+$/.test(ip) && ip.includes(":");
|
|
60
|
+
}
|
|
61
|
+
function isValidCIDR(cidr) {
|
|
62
|
+
const [ip, prefix] = cidr.split("/");
|
|
63
|
+
if (!prefix)
|
|
64
|
+
return false;
|
|
65
|
+
const p = parseInt(prefix);
|
|
66
|
+
return ((isValidIPv4(ip) && p >= 0 && p <= 32) ||
|
|
67
|
+
(isValidIPv6(ip) && p >= 0 && p <= 128));
|
|
68
|
+
}
|
|
69
|
+
function validateIP(ip) {
|
|
70
|
+
if (!isValidIPv4(ip) && !isValidIPv6(ip) && !isValidCIDR(ip)) {
|
|
71
|
+
throw new Error(`[MajikAPI] Invalid IP address or CIDR: "${ip}"`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function isValidDomain(domain) {
|
|
75
|
+
return (/^(\*\.)?[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]{2,})+$/.test(domain) || /^\*$/.test(domain));
|
|
76
|
+
}
|
|
77
|
+
function validateDomain(domain) {
|
|
78
|
+
if (!isValidDomain(domain)) {
|
|
79
|
+
throw new Error(`[MajikAPI] Invalid domain: "${domain}"`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function isValidISODate(value) {
|
|
83
|
+
const d = new Date(value);
|
|
84
|
+
return !isNaN(d.getTime());
|
|
85
|
+
}
|
|
86
|
+
// ─────────────────────────────────────────────
|
|
87
|
+
// Default Settings Factory
|
|
88
|
+
// ─────────────────────────────────────────────
|
|
89
|
+
function buildDefaultSettings(overrides) {
|
|
90
|
+
return {
|
|
91
|
+
rateLimit: { ...DEFAULT_RATE_LIMIT, ...(overrides?.rateLimit ?? {}) },
|
|
92
|
+
ipWhitelist: {
|
|
93
|
+
enabled: false,
|
|
94
|
+
addresses: [],
|
|
95
|
+
...(overrides?.ipWhitelist ?? {}),
|
|
96
|
+
},
|
|
97
|
+
domainWhitelist: {
|
|
98
|
+
enabled: false,
|
|
99
|
+
domains: [],
|
|
100
|
+
...(overrides?.domainWhitelist ?? {}),
|
|
101
|
+
},
|
|
102
|
+
allowedMethods: overrides?.allowedMethods ?? [],
|
|
103
|
+
metadata: overrides?.metadata ?? {},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// ─────────────────────────────────────────────
|
|
107
|
+
// MajikAPI Class
|
|
108
|
+
// ─────────────────────────────────────────────
|
|
109
|
+
export class MajikAPI {
|
|
110
|
+
// ── Private fields ───────────────────────────────────────────────────────
|
|
111
|
+
//
|
|
112
|
+
// _id — stable UUID. Primary key in Supabase. Never changes even
|
|
113
|
+
// when the key is rotated. Safe to FK against in audit tables.
|
|
114
|
+
//
|
|
115
|
+
// _owner_id — UUID of the user who owns this key. FK to auth.users.
|
|
116
|
+
//
|
|
117
|
+
// _api_key — SHA-256 hash of the raw plaintext key. Has a UNIQUE INDEX
|
|
118
|
+
// in Postgres (not the PK). Used as the Redis cache key.
|
|
119
|
+
// The raw key is never stored or logged anywhere.
|
|
120
|
+
//
|
|
121
|
+
// _raw_api_key — Only populated immediately after create(). Cleared
|
|
122
|
+
// (undefined) when reconstructed via fromJSON(). This is
|
|
123
|
+
// the one and only moment the caller can read the plaintext.
|
|
124
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
125
|
+
_id;
|
|
126
|
+
_owner_id;
|
|
127
|
+
_name;
|
|
128
|
+
_api_key;
|
|
129
|
+
_raw_api_key;
|
|
130
|
+
_timestamp;
|
|
131
|
+
_restricted;
|
|
132
|
+
_valid_until;
|
|
133
|
+
_settings;
|
|
134
|
+
// ─────────────────────────────────────────────
|
|
135
|
+
// Private Constructor
|
|
136
|
+
// ─────────────────────────────────────────────
|
|
137
|
+
constructor(id, owner_id, name, api_key, timestamp, restricted, valid_until, settings, raw_api_key) {
|
|
138
|
+
this._id = id;
|
|
139
|
+
this._owner_id = owner_id;
|
|
140
|
+
this._name = name;
|
|
141
|
+
this._api_key = api_key;
|
|
142
|
+
this._timestamp = timestamp;
|
|
143
|
+
this._restricted = restricted;
|
|
144
|
+
this._valid_until = valid_until;
|
|
145
|
+
this._settings = settings;
|
|
146
|
+
this._raw_api_key = raw_api_key;
|
|
147
|
+
}
|
|
148
|
+
// ─────────────────────────────────────────────
|
|
149
|
+
// Static Factory: create()
|
|
150
|
+
// ─────────────────────────────────────────────
|
|
151
|
+
/**
|
|
152
|
+
* Create a brand-new MajikAPI key instance.
|
|
153
|
+
*
|
|
154
|
+
* @param ownerID - UUID of the user who owns this key. Required.
|
|
155
|
+
* @param text - Optional raw key text. If omitted, a UUIDv4 is generated.
|
|
156
|
+
* @param options - Optional name, expiry, restrictions, and settings.
|
|
157
|
+
*
|
|
158
|
+
* After creation, `instance.rawApiKey` holds the plaintext key. This is the
|
|
159
|
+
* only moment it is accessible. Store it safely — it cannot be recovered.
|
|
160
|
+
*/
|
|
161
|
+
static create(ownerID, text, options = {}) {
|
|
162
|
+
assertString(ownerID, "ownerID");
|
|
163
|
+
// Resolve raw key
|
|
164
|
+
let rawKey;
|
|
165
|
+
if (text !== undefined) {
|
|
166
|
+
if (typeof text !== "string" || text.trim() === "") {
|
|
167
|
+
throw new TypeError("[MajikAPI] create(): 'text' must be a non-empty string if provided.");
|
|
168
|
+
}
|
|
169
|
+
rawKey = text.trim();
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
rawKey = generateID();
|
|
173
|
+
}
|
|
174
|
+
const name = options.name ?? "Unnamed Key";
|
|
175
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
176
|
+
throw new TypeError("[MajikAPI] create(): 'options.name' must be a non-empty string.");
|
|
177
|
+
}
|
|
178
|
+
const restricted = options.restricted ?? false;
|
|
179
|
+
assertBoolean(restricted, "options.restricted");
|
|
180
|
+
let valid_until = null;
|
|
181
|
+
if (options.valid_until !== undefined && options.valid_until !== null) {
|
|
182
|
+
valid_until = MajikAPI.parseDate(options.valid_until, "options.valid_until");
|
|
183
|
+
if (valid_until <= new Date()) {
|
|
184
|
+
throw new RangeError("[MajikAPI] create(): 'valid_until' must be a future date.");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const settings = buildDefaultSettings(options.settings);
|
|
188
|
+
MajikAPI.validateSettings(settings);
|
|
189
|
+
return new MajikAPI(generateID(), // _id — stable primary key, separate from the key hash
|
|
190
|
+
ownerID.trim(), // _owner_id
|
|
191
|
+
name.trim(), // _name
|
|
192
|
+
sha256(rawKey), // _api_key — hash only, never store raw
|
|
193
|
+
new Date(), // _timestamp
|
|
194
|
+
restricted, valid_until, settings, rawKey);
|
|
195
|
+
}
|
|
196
|
+
// ─────────────────────────────────────────────
|
|
197
|
+
// Static Factory: fromJSON()
|
|
198
|
+
// ─────────────────────────────────────────────
|
|
199
|
+
/**
|
|
200
|
+
* Reconstruct a MajikAPI instance from a serialised MajikAPIJSON object.
|
|
201
|
+
* Accepts the raw output of `toJSON()`, a Supabase row, or a Redis cache hit.
|
|
202
|
+
*
|
|
203
|
+
* `raw_api_key` is intentionally NOT restored — it is never in the JSON.
|
|
204
|
+
*/
|
|
205
|
+
static fromJSON(data) {
|
|
206
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
207
|
+
throw new TypeError("[MajikAPI] fromJSON(): Expected a plain object.");
|
|
208
|
+
}
|
|
209
|
+
assertString(data.id, "id");
|
|
210
|
+
assertString(data.owner_id, "owner_id");
|
|
211
|
+
assertString(data.name, "name");
|
|
212
|
+
assertString(data.api_key, "api_key");
|
|
213
|
+
assertString(data.timestamp, "timestamp");
|
|
214
|
+
if (!isValidISODate(data.timestamp)) {
|
|
215
|
+
throw new TypeError(`[MajikAPI] fromJSON(): 'timestamp' is not a valid ISO date: "${data.timestamp}"`);
|
|
216
|
+
}
|
|
217
|
+
if (typeof data.restricted !== "boolean") {
|
|
218
|
+
throw new TypeError("[MajikAPI] fromJSON(): 'restricted' must be a boolean.");
|
|
219
|
+
}
|
|
220
|
+
if (data.valid_until !== null && data.valid_until !== undefined) {
|
|
221
|
+
assertString(data.valid_until, "valid_until");
|
|
222
|
+
if (!isValidISODate(data.valid_until)) {
|
|
223
|
+
throw new TypeError("[MajikAPI] fromJSON(): 'valid_until' is not a valid ISO date.");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (typeof data.settings !== "object" || data.settings === null) {
|
|
227
|
+
throw new TypeError("[MajikAPI] fromJSON(): 'settings' must be an object.");
|
|
228
|
+
}
|
|
229
|
+
const settings = buildDefaultSettings(data.settings);
|
|
230
|
+
MajikAPI.validateSettings(settings);
|
|
231
|
+
return new MajikAPI(data.id, data.owner_id, data.name, data.api_key, new Date(data.timestamp), data.restricted, data.valid_until ? new Date(data.valid_until) : null, settings, undefined);
|
|
232
|
+
}
|
|
233
|
+
// ─────────────────────────────────────────────
|
|
234
|
+
// Serialisation
|
|
235
|
+
// ─────────────────────────────────────────────
|
|
236
|
+
/**
|
|
237
|
+
* Serialise to a plain JSON-safe object matching MajikAPIJSON.
|
|
238
|
+
* Safe to store in Supabase or cache in Redis.
|
|
239
|
+
* raw_api_key is NEVER included.
|
|
240
|
+
*/
|
|
241
|
+
toJSON() {
|
|
242
|
+
return {
|
|
243
|
+
id: this._id,
|
|
244
|
+
owner_id: this._owner_id,
|
|
245
|
+
name: this._name,
|
|
246
|
+
api_key: this._api_key,
|
|
247
|
+
timestamp: this._timestamp.toISOString(),
|
|
248
|
+
restricted: this._restricted,
|
|
249
|
+
valid_until: this._valid_until ? this._valid_until.toISOString() : null,
|
|
250
|
+
settings: structuredClone(this._settings),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// ─────────────────────────────────────────────
|
|
254
|
+
// Validation
|
|
255
|
+
// ─────────────────────────────────────────────
|
|
256
|
+
/**
|
|
257
|
+
* Assert the integrity of all fields on this instance.
|
|
258
|
+
* Throws a descriptive error for the first field that fails.
|
|
259
|
+
*/
|
|
260
|
+
validate() {
|
|
261
|
+
assertString(this._id, "id");
|
|
262
|
+
assertString(this._owner_id, "owner_id");
|
|
263
|
+
assertString(this._name, "name");
|
|
264
|
+
assertString(this._api_key, "api_key");
|
|
265
|
+
if (!/^[a-f0-9]{64}$/.test(this._api_key)) {
|
|
266
|
+
throw new Error("[MajikAPI] validate(): 'api_key' does not appear to be a valid SHA-256 hash.");
|
|
267
|
+
}
|
|
268
|
+
if (!(this._timestamp instanceof Date) ||
|
|
269
|
+
isNaN(this._timestamp.getTime())) {
|
|
270
|
+
throw new TypeError("[MajikAPI] validate(): 'timestamp' is not a valid Date.");
|
|
271
|
+
}
|
|
272
|
+
assertBoolean(this._restricted, "restricted");
|
|
273
|
+
if (this._valid_until !== null) {
|
|
274
|
+
if (!(this._valid_until instanceof Date) ||
|
|
275
|
+
isNaN(this._valid_until.getTime())) {
|
|
276
|
+
throw new TypeError("[MajikAPI] validate(): 'valid_until' is not a valid Date.");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
MajikAPI.validateSettings(this._settings);
|
|
280
|
+
}
|
|
281
|
+
// ─────────────────────────────────────────────
|
|
282
|
+
// Key Verification
|
|
283
|
+
// ─────────────────────────────────────────────
|
|
284
|
+
/**
|
|
285
|
+
* Hash `text` and compare against the stored api_key hash.
|
|
286
|
+
* This is the correct way to verify an incoming key at your API gateway.
|
|
287
|
+
* Returns true if the key matches.
|
|
288
|
+
*/
|
|
289
|
+
verify(text) {
|
|
290
|
+
if (typeof text !== "string" || text.trim() === "") {
|
|
291
|
+
throw new TypeError("[MajikAPI] verify(): Input must be a non-empty string.");
|
|
292
|
+
}
|
|
293
|
+
return sha256(text.trim()) === this._api_key;
|
|
294
|
+
}
|
|
295
|
+
/** Alias for verify(). */
|
|
296
|
+
matches(text) {
|
|
297
|
+
return this.verify(text);
|
|
298
|
+
}
|
|
299
|
+
// ─────────────────────────────────────────────
|
|
300
|
+
// Status Checks
|
|
301
|
+
// ─────────────────────────────────────────────
|
|
302
|
+
/** Returns true if valid_until is set and has passed. Always false if null. */
|
|
303
|
+
isExpired() {
|
|
304
|
+
if (this._valid_until === null)
|
|
305
|
+
return false;
|
|
306
|
+
return new Date() > this._valid_until;
|
|
307
|
+
}
|
|
308
|
+
/** Returns true only if the key is not expired and not restricted. */
|
|
309
|
+
isActive() {
|
|
310
|
+
return !this.isExpired() && !this._restricted;
|
|
311
|
+
}
|
|
312
|
+
// ─────────────────────────────────────────────
|
|
313
|
+
// Rate Limit
|
|
314
|
+
// ─────────────────────────────────────────────
|
|
315
|
+
/**
|
|
316
|
+
* Set the rate limit for this key.
|
|
317
|
+
*
|
|
318
|
+
* The effective rate (normalised to req/min) cannot exceed MAX_RATE_LIMIT
|
|
319
|
+
* (500 req/min). Attempting to set a higher rate will throw unless
|
|
320
|
+
* bypassSafeLimit is explicitly passed as true.
|
|
321
|
+
*
|
|
322
|
+
* @param amount - Number of allowed requests per frequency window.
|
|
323
|
+
* @param frequency - The time window unit.
|
|
324
|
+
* @param bypassSafeLimit - When true, skips the MAX_RATE_LIMIT ceiling check.
|
|
325
|
+
* Defaults to false. Use with caution.
|
|
326
|
+
*/
|
|
327
|
+
setRateLimit(amount, frequency, bypassSafeLimit = false) {
|
|
328
|
+
assertPositiveInteger(amount, "amount");
|
|
329
|
+
assertRateLimitFrequency(frequency, "frequency");
|
|
330
|
+
assertBoolean(bypassSafeLimit, "bypassSafeLimit");
|
|
331
|
+
if (!bypassSafeLimit) {
|
|
332
|
+
const incomingRpm = amount * TO_MINUTES[frequency];
|
|
333
|
+
const ceilingRpm = MAX_RATE_LIMIT.amount * TO_MINUTES[MAX_RATE_LIMIT.frequency];
|
|
334
|
+
if (incomingRpm > ceilingRpm) {
|
|
335
|
+
throw new RangeError(`[MajikAPI] setRateLimit(): The requested rate (${amount} per ${frequency} ` +
|
|
336
|
+
`\u2248 ${incomingRpm.toFixed(4)} req/min) exceeds the system ceiling of ` +
|
|
337
|
+
`${ceilingRpm.toFixed(4)} req/min (${MAX_RATE_LIMIT.amount} per ` +
|
|
338
|
+
`${MAX_RATE_LIMIT.frequency}). ` +
|
|
339
|
+
`Pass bypassSafeLimit = true to override this guard.`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
this._settings.rateLimit = { amount, frequency };
|
|
343
|
+
}
|
|
344
|
+
/** Reset the rate limit back to DEFAULT_RATE_LIMIT. */
|
|
345
|
+
resetRateLimit() {
|
|
346
|
+
this._settings.rateLimit = { ...DEFAULT_RATE_LIMIT };
|
|
347
|
+
}
|
|
348
|
+
// ─────────────────────────────────────────────
|
|
349
|
+
// Key Rotation
|
|
350
|
+
// ─────────────────────────────────────────────
|
|
351
|
+
/**
|
|
352
|
+
* Rotate the API key. Generates a new raw key (or accepts a provided one),
|
|
353
|
+
* hashes it, and replaces _api_key. The stable _id and _owner_id are
|
|
354
|
+
* untouched, so all FK references in audit/event tables remain valid.
|
|
355
|
+
*
|
|
356
|
+
* After rotation, `rawApiKey` holds the new plaintext key. This is the only
|
|
357
|
+
* moment it is accessible. The old key is immediately invalidated in-memory.
|
|
358
|
+
*
|
|
359
|
+
* IMPORTANT: After calling rotate(), you must:
|
|
360
|
+
* 1. Save the new toJSON() to Supabase (updates the api_key column).
|
|
361
|
+
* 2. Delete the old Redis cache entry (old hash key is now stale).
|
|
362
|
+
* 3. Show the caller rawApiKey before discarding this instance.
|
|
363
|
+
*
|
|
364
|
+
* @param text - Optional new raw key. If omitted, a UUIDv4 is generated.
|
|
365
|
+
*/
|
|
366
|
+
rotate(text) {
|
|
367
|
+
let rawKey;
|
|
368
|
+
if (text !== undefined) {
|
|
369
|
+
if (typeof text !== "string" || text.trim() === "") {
|
|
370
|
+
throw new TypeError("[MajikAPI] rotate(): 'text' must be a non-empty string if provided.");
|
|
371
|
+
}
|
|
372
|
+
rawKey = text.trim();
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
rawKey = generateID();
|
|
376
|
+
}
|
|
377
|
+
this._api_key = sha256(rawKey);
|
|
378
|
+
this._raw_api_key = rawKey;
|
|
379
|
+
}
|
|
380
|
+
// ─────────────────────────────────────────────
|
|
381
|
+
// Mutation Methods
|
|
382
|
+
// ─────────────────────────────────────────────
|
|
383
|
+
/** Rename this key. */
|
|
384
|
+
rename(name) {
|
|
385
|
+
assertString(name, "name");
|
|
386
|
+
this._name = name.trim();
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Set or clear the expiry date.
|
|
390
|
+
* Pass null to make the key never expire.
|
|
391
|
+
*/
|
|
392
|
+
setExpiry(date) {
|
|
393
|
+
if (date === null) {
|
|
394
|
+
this._valid_until = null;
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const parsed = MajikAPI.parseDate(date, "date");
|
|
398
|
+
if (parsed <= new Date()) {
|
|
399
|
+
throw new RangeError("[MajikAPI] setExpiry(): Expiry date must be in the future.");
|
|
400
|
+
}
|
|
401
|
+
this._valid_until = parsed;
|
|
402
|
+
}
|
|
403
|
+
/** Disable this key without deleting it. */
|
|
404
|
+
restrict() {
|
|
405
|
+
this._restricted = true;
|
|
406
|
+
}
|
|
407
|
+
/** Re-enable a previously restricted key. */
|
|
408
|
+
unrestrict() {
|
|
409
|
+
this._restricted = false;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Permanently revoke this key. Sets valid_until to epoch (always expired)
|
|
413
|
+
* and marks it restricted. Both flags must be cleared to undo this — prefer
|
|
414
|
+
* deleting the Supabase row and creating a new key instead.
|
|
415
|
+
*/
|
|
416
|
+
revoke() {
|
|
417
|
+
this._valid_until = new Date(0);
|
|
418
|
+
this._restricted = true;
|
|
419
|
+
}
|
|
420
|
+
// ─────────────────────────────────────────────
|
|
421
|
+
// IP Whitelist
|
|
422
|
+
// ─────────────────────────────────────────────
|
|
423
|
+
enableIPWhitelist() {
|
|
424
|
+
this._settings.ipWhitelist.enabled = true;
|
|
425
|
+
}
|
|
426
|
+
disableIPWhitelist() {
|
|
427
|
+
this._settings.ipWhitelist.enabled = false;
|
|
428
|
+
}
|
|
429
|
+
addIP(ip) {
|
|
430
|
+
assertString(ip, "ip");
|
|
431
|
+
validateIP(ip.trim());
|
|
432
|
+
const trimmed = ip.trim();
|
|
433
|
+
if (!this._settings.ipWhitelist.addresses.includes(trimmed)) {
|
|
434
|
+
this._settings.ipWhitelist.addresses.push(trimmed);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
removeIP(ip) {
|
|
438
|
+
assertString(ip, "ip");
|
|
439
|
+
this._settings.ipWhitelist.addresses =
|
|
440
|
+
this._settings.ipWhitelist.addresses.filter((a) => a !== ip.trim());
|
|
441
|
+
}
|
|
442
|
+
setIPWhitelist(addresses) {
|
|
443
|
+
assertStringArray(addresses, "addresses");
|
|
444
|
+
addresses.forEach((ip) => validateIP(ip.trim()));
|
|
445
|
+
this._settings.ipWhitelist.addresses = addresses.map((ip) => ip.trim());
|
|
446
|
+
}
|
|
447
|
+
clearIPWhitelist() {
|
|
448
|
+
this._settings.ipWhitelist.addresses = [];
|
|
449
|
+
}
|
|
450
|
+
// ─────────────────────────────────────────────
|
|
451
|
+
// Domain Whitelist
|
|
452
|
+
// ─────────────────────────────────────────────
|
|
453
|
+
enableDomainWhitelist() {
|
|
454
|
+
this._settings.domainWhitelist.enabled = true;
|
|
455
|
+
}
|
|
456
|
+
disableDomainWhitelist() {
|
|
457
|
+
this._settings.domainWhitelist.enabled = false;
|
|
458
|
+
}
|
|
459
|
+
addDomain(domain) {
|
|
460
|
+
assertString(domain, "domain");
|
|
461
|
+
validateDomain(domain.trim());
|
|
462
|
+
const trimmed = domain.trim();
|
|
463
|
+
if (!this._settings.domainWhitelist.domains.includes(trimmed)) {
|
|
464
|
+
this._settings.domainWhitelist.domains.push(trimmed);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
removeDomain(domain) {
|
|
468
|
+
assertString(domain, "domain");
|
|
469
|
+
this._settings.domainWhitelist.domains =
|
|
470
|
+
this._settings.domainWhitelist.domains.filter((d) => d !== domain.trim());
|
|
471
|
+
}
|
|
472
|
+
setDomainWhitelist(domains) {
|
|
473
|
+
assertStringArray(domains, "domains");
|
|
474
|
+
domains.forEach((d) => validateDomain(d.trim()));
|
|
475
|
+
this._settings.domainWhitelist.domains = domains.map((d) => d.trim());
|
|
476
|
+
}
|
|
477
|
+
clearDomainWhitelist() {
|
|
478
|
+
this._settings.domainWhitelist.domains = [];
|
|
479
|
+
}
|
|
480
|
+
// ─────────────────────────────────────────────
|
|
481
|
+
// Allowed Methods
|
|
482
|
+
// ─────────────────────────────────────────────
|
|
483
|
+
setAllowedMethods(methods) {
|
|
484
|
+
assertStringArray(methods, "methods");
|
|
485
|
+
const valid = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
486
|
+
for (const m of methods) {
|
|
487
|
+
if (!valid.includes(m.toUpperCase())) {
|
|
488
|
+
throw new Error(`[MajikAPI] setAllowedMethods(): Unknown HTTP method "${m}". Valid: ${valid.join(", ")}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
this._settings.allowedMethods = methods.map((m) => m.toUpperCase());
|
|
492
|
+
}
|
|
493
|
+
clearAllowedMethods() {
|
|
494
|
+
this._settings.allowedMethods = [];
|
|
495
|
+
}
|
|
496
|
+
// ─────────────────────────────────────────────
|
|
497
|
+
// Metadata
|
|
498
|
+
// ─────────────────────────────────────────────
|
|
499
|
+
setMetadata(key, value) {
|
|
500
|
+
assertString(key, "metadata key");
|
|
501
|
+
this._settings.metadata = this._settings.metadata ?? {};
|
|
502
|
+
this._settings.metadata[key] = value;
|
|
503
|
+
}
|
|
504
|
+
getMetadata(key) {
|
|
505
|
+
assertString(key, "metadata key");
|
|
506
|
+
return this._settings.metadata?.[key];
|
|
507
|
+
}
|
|
508
|
+
deleteMetadata(key) {
|
|
509
|
+
assertString(key, "metadata key");
|
|
510
|
+
if (this._settings.metadata) {
|
|
511
|
+
delete this._settings.metadata[key];
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
clearMetadata() {
|
|
515
|
+
this._settings.metadata = {};
|
|
516
|
+
}
|
|
517
|
+
// ─────────────────────────────────────────────
|
|
518
|
+
// Getters
|
|
519
|
+
// ─────────────────────────────────────────────
|
|
520
|
+
/** The key's own stable UUID. Primary key in Supabase. */
|
|
521
|
+
get id() {
|
|
522
|
+
return this._id;
|
|
523
|
+
}
|
|
524
|
+
/** UUID of the user who owns this key. FK to auth.users. */
|
|
525
|
+
get ownerId() {
|
|
526
|
+
return this._owner_id;
|
|
527
|
+
}
|
|
528
|
+
get name() {
|
|
529
|
+
return this._name;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* The SHA-256 hash of the raw key. This is what is stored in Supabase and
|
|
533
|
+
* used as the Redis cache key prefix. Never the plaintext.
|
|
534
|
+
*/
|
|
535
|
+
get apiKey() {
|
|
536
|
+
return this._api_key;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* The raw plaintext key. Only defined immediately after create() or rotate().
|
|
540
|
+
* Undefined after fromJSON() or any serialise/deserialise round-trip.
|
|
541
|
+
* Treat this like a password — show it once and discard.
|
|
542
|
+
*/
|
|
543
|
+
get rawApiKey() {
|
|
544
|
+
return this._raw_api_key;
|
|
545
|
+
}
|
|
546
|
+
get createdAt() {
|
|
547
|
+
return new Date(this._timestamp);
|
|
548
|
+
}
|
|
549
|
+
get timestamp() {
|
|
550
|
+
return this._timestamp.toISOString();
|
|
551
|
+
}
|
|
552
|
+
get restricted() {
|
|
553
|
+
return this._restricted;
|
|
554
|
+
}
|
|
555
|
+
get validUntil() {
|
|
556
|
+
return this._valid_until ? new Date(this._valid_until) : null;
|
|
557
|
+
}
|
|
558
|
+
/** Returns a deep clone — mutations to the returned object have no effect. */
|
|
559
|
+
get settings() {
|
|
560
|
+
return structuredClone(this._settings);
|
|
561
|
+
}
|
|
562
|
+
get rateLimit() {
|
|
563
|
+
return { ...this._settings.rateLimit };
|
|
564
|
+
}
|
|
565
|
+
get ipWhitelist() {
|
|
566
|
+
return structuredClone(this._settings.ipWhitelist);
|
|
567
|
+
}
|
|
568
|
+
get domainWhitelist() {
|
|
569
|
+
return structuredClone(this._settings.domainWhitelist);
|
|
570
|
+
}
|
|
571
|
+
get allowedMethods() {
|
|
572
|
+
return [...(this._settings.allowedMethods ?? [])];
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Milliseconds until this key expires.
|
|
576
|
+
* Returns -1 if the key never expires (valid_until is null).
|
|
577
|
+
* Returns 0 if the key has already expired.
|
|
578
|
+
*/
|
|
579
|
+
get msUntilExpiry() {
|
|
580
|
+
if (this._valid_until === null)
|
|
581
|
+
return -1;
|
|
582
|
+
return Math.max(0, this._valid_until.getTime() - Date.now());
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Human-readable lifecycle status.
|
|
586
|
+
*
|
|
587
|
+
* "active" — valid, not restricted, not expired.
|
|
588
|
+
* "restricted" — manually disabled, not expired.
|
|
589
|
+
* "expired" — past valid_until date.
|
|
590
|
+
* "revoked" — permanently invalidated via revoke().
|
|
591
|
+
*/
|
|
592
|
+
get status() {
|
|
593
|
+
if (this._valid_until?.getTime() === new Date(0).getTime())
|
|
594
|
+
return "revoked";
|
|
595
|
+
if (this.isExpired())
|
|
596
|
+
return "expired";
|
|
597
|
+
if (this._restricted)
|
|
598
|
+
return "restricted";
|
|
599
|
+
return "active";
|
|
600
|
+
}
|
|
601
|
+
// ─────────────────────────────────────────────
|
|
602
|
+
// Private Static Utilities
|
|
603
|
+
// ─────────────────────────────────────────────
|
|
604
|
+
static parseDate(value, label) {
|
|
605
|
+
if (value instanceof Date) {
|
|
606
|
+
if (isNaN(value.getTime())) {
|
|
607
|
+
throw new TypeError(`[MajikAPI] "${label}" is an invalid Date object.`);
|
|
608
|
+
}
|
|
609
|
+
return value;
|
|
610
|
+
}
|
|
611
|
+
if (typeof value === "string") {
|
|
612
|
+
if (!isValidISODate(value)) {
|
|
613
|
+
throw new TypeError(`[MajikAPI] "${label}" is not a valid ISO date string: "${value}"`);
|
|
614
|
+
}
|
|
615
|
+
return new Date(value);
|
|
616
|
+
}
|
|
617
|
+
throw new TypeError(`[MajikAPI] "${label}" must be a Date instance or an ISO date string.`);
|
|
618
|
+
}
|
|
619
|
+
static validateSettings(settings) {
|
|
620
|
+
if (typeof settings !== "object" || settings === null) {
|
|
621
|
+
throw new TypeError("[MajikAPI] 'settings' must be an object.");
|
|
622
|
+
}
|
|
623
|
+
assertPositiveInteger(settings.rateLimit?.amount, "settings.rateLimit.amount");
|
|
624
|
+
assertRateLimitFrequency(settings.rateLimit?.frequency, "settings.rateLimit.frequency");
|
|
625
|
+
assertBoolean(settings.ipWhitelist?.enabled, "settings.ipWhitelist.enabled");
|
|
626
|
+
assertStringArray(settings.ipWhitelist?.addresses, "settings.ipWhitelist.addresses");
|
|
627
|
+
settings.ipWhitelist.addresses.forEach((ip) => validateIP(ip));
|
|
628
|
+
assertBoolean(settings.domainWhitelist?.enabled, "settings.domainWhitelist.enabled");
|
|
629
|
+
assertStringArray(settings.domainWhitelist?.domains, "settings.domainWhitelist.domains");
|
|
630
|
+
settings.domainWhitelist.domains.forEach((d) => validateDomain(d));
|
|
631
|
+
}
|
|
632
|
+
// ─────────────────────────────────────────────
|
|
633
|
+
// Debug
|
|
634
|
+
// ─────────────────────────────────────────────
|
|
635
|
+
toString() {
|
|
636
|
+
return `[MajikAPI id="${this._id}" owner="${this._owner_id}" name="${this._name}" status="${this.status}"]`;
|
|
637
|
+
}
|
|
638
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
639
|
+
return this.toString();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
export default MajikAPI;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type RateLimitFrequency = "seconds" | "minutes" | "hours";
|
|
2
|
+
export interface RateLimit {
|
|
3
|
+
amount: number;
|
|
4
|
+
frequency: RateLimitFrequency;
|
|
5
|
+
}
|
|
6
|
+
export interface IPWhitelist {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
addresses: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface DomainWhitelist {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
domains: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface MajikAPISettings {
|
|
15
|
+
rateLimit: RateLimit;
|
|
16
|
+
ipWhitelist: IPWhitelist;
|
|
17
|
+
domainWhitelist: DomainWhitelist;
|
|
18
|
+
allowedMethods?: string[];
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The serialised shape stored in Supabase and cached in Redis.
|
|
23
|
+
*
|
|
24
|
+
* id — stable UUID primary key in Supabase. Never changes, even on
|
|
25
|
+
* key rotation. Safe to use as a FK in audit/event tables.
|
|
26
|
+
* owner_id — FK to auth.users. Identifies who owns this key.
|
|
27
|
+
* api_key — SHA-256 hash of the raw plaintext key. Has a UNIQUE INDEX in
|
|
28
|
+
* Postgres (not the PK). Used as the Redis cache key prefix.
|
|
29
|
+
* The raw key is never stored anywhere.
|
|
30
|
+
*/
|
|
31
|
+
export interface MajikAPIJSON {
|
|
32
|
+
id: string;
|
|
33
|
+
owner_id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
api_key: string;
|
|
36
|
+
timestamp: string;
|
|
37
|
+
restricted: boolean;
|
|
38
|
+
valid_until: string | null;
|
|
39
|
+
settings: MajikAPISettings;
|
|
40
|
+
}
|
|
41
|
+
export interface MajikAPICreateOptions {
|
|
42
|
+
name?: string;
|
|
43
|
+
restricted?: boolean;
|
|
44
|
+
valid_until?: Date | string | null;
|
|
45
|
+
settings?: Partial<MajikAPISettings>;
|
|
46
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { hash } from "@stablelib/sha256";
|
|
2
|
+
import { v4 as uuidv4 } from "uuid";
|
|
3
|
+
export function sha256(input) {
|
|
4
|
+
const hashed = hash(new TextEncoder().encode(input));
|
|
5
|
+
return arrayToBase64(hashed);
|
|
6
|
+
}
|
|
7
|
+
export function arrayToBase64(data) {
|
|
8
|
+
let binary = "";
|
|
9
|
+
const bytes = data;
|
|
10
|
+
const len = bytes.byteLength;
|
|
11
|
+
for (let i = 0; i < len; i++) {
|
|
12
|
+
binary += String.fromCharCode(bytes[i]);
|
|
13
|
+
}
|
|
14
|
+
return btoa(binary);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generate a Random v4 UUID
|
|
18
|
+
*/
|
|
19
|
+
export function generateID() {
|
|
20
|
+
try {
|
|
21
|
+
const genID = uuidv4();
|
|
22
|
+
return genID;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw new Error(`Failed to generate ID: ${error}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@majikah/majik-api",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"description": "A high-security API key management library for TypeScript. Handles generation, SHA-256 hashing, and lifecycle management including rate limiting, IP/domain whitelisting, and secure key rotation.",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"author": "Zelijah",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/Majikah/majik-api.git"
|
|
17
|
+
},
|
|
18
|
+
"funding": {
|
|
19
|
+
"type": "github",
|
|
20
|
+
"url": "https://github.com/sponsors/jedlsf"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"api-key",
|
|
24
|
+
"api-management",
|
|
25
|
+
"security",
|
|
26
|
+
"sha256",
|
|
27
|
+
"rate-limiting",
|
|
28
|
+
"whitelisting",
|
|
29
|
+
"key-rotation",
|
|
30
|
+
"ip-filtering",
|
|
31
|
+
"domain-validation",
|
|
32
|
+
"typescript",
|
|
33
|
+
"supabase",
|
|
34
|
+
"redis",
|
|
35
|
+
"majikah"
|
|
36
|
+
],
|
|
37
|
+
"homepage": "https://github.com/Majikah/majik-api#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/Majikah/majik-api/issues"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
43
|
+
"build": "tsc",
|
|
44
|
+
"prepublishOnly": "npm run build"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@stablelib/sha256": "^2.0.1",
|
|
48
|
+
"uuid": "^13.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|