@majikah/majik-slink 0.0.1 → 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 CHANGED
@@ -1,67 +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
-
1
+ Copyright (c) 2026 Majikah Solutions OPC
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
67
  END OF TERMS AND CONDITIONS
package/README.md CHANGED
@@ -1,74 +1,119 @@
1
1
  # Majik SLink
2
2
 
3
- [![Developed by Zelijah](https://img.shields.io/badge/Developed%20by-Zelijah-red?logo=github&logoColor=white)](https://thezelijah.world) ![GitHub Sponsors](https://img.shields.io/github/sponsors/jedlsf?style=plastic&label=Sponsors&link=https%3A%2F%2Fgithub.com%2Fsponsors%2Fjedlsf)
4
-
5
- **MajikSLink** is a specialized TypeScript library designed for URL ownership verification and cryptographic identity linking. It provides a robust mechanism to prove that a specific digital identity (a MajikKey) controls a publicly accessible web resource (such as a YouTube channel, social media profile, or personal blog).
6
-
7
- By combining traditional **Ed25519** signatures with post-quantum **ML-DSA-87** (Dilithium) hybrid cryptography, MajikSLink ensures that your identity-to-URL associations are tamper-proof and future-proof.
8
-
9
-
10
-
11
- Identities can be created and managed via the web application at **[https://id.majikah.solutions](https://id.majikah.solutions)** using your own Majik Keys.
12
-
3
+ [![Developed by Zelijah](https://img.shields.io/badge/Developed%20by-Zelijah-red?logo=github&logoColor=white)](https://www.thezelijah.world) ![GitHub Sponsors](https://img.shields.io/github/sponsors/jedlsf?style=plastic&label=Sponsors&link=https%3A%2F%2Fgithub.com%2Fsponsors%2Fjedlsf)
13
4
  ![npm](https://img.shields.io/npm/v/@majikah/majik-slink) ![npm downloads](https://img.shields.io/npm/dm/@majikah/majik-slink) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue)
14
5
 
6
+ **MajikSLink** is a specialized TypeScript library designed for URL ownership verification and cryptographic identity linking. It provides a robust, zero-trust mechanism to prove that a specific digital identity (a **MajikKey**) controls a publicly accessible web resource (such as a YouTube channel, social media profile, DNS record, or personal blog).
15
7
 
8
+ By combining traditional **Ed25519** signatures with post-quantum **ML-DSA-87 (Dilithium)** hybrid cryptography via `@majikah/majik-signature`, MajikSLink ensures that your identity-to-URL associations are tamper-proof and resilient against future quantum threats.
16
9
 
10
+ Identities can be created and managed via the web application at **[https://id.majikah.solutions](https://id.majikah.solutions)** using your own Majik Keys.
17
11
 
18
- ## Key Features:
19
- 1. **Deterministic URL Normalization:** Automatically converts URLs into a canonical format (stripping query params and fragments) so that the same resource always generates the same verification code.
12
+ ---
20
13
 
21
- 2. **Challenge-Response Verification:** Generates short, embeddable verification codes (v_code) that users can place in their bios or page metadata for automated scrapers to detect.
14
+ ## Key Features
22
15
 
23
- 3. **Hybrid Signing:** Leverages [@majikah/majik-signature](https://www.npmjs.com/package/@majikah/majik-signature) to sign canonical URL data with both classical and post-quantum algorithms.
16
+ 1. **Deterministic URL Normalization:** Automatically converts URLs into a canonical `cleanUrl` format (stripping tracking queries, fragments, and trailing slashes) so that the same core resource always generates a consistent verification code and hash.
17
+ 2. **Challenge-Response Verification:** Generates short, OTP-style verification codes (`vCode`) that users can place in DNS TXT records, bios, or page metadata for automated verifiers to detect.
18
+ 3. **Post-Quantum Hybrid Signing:** Leverages `MajikSignature` to sign the canonical URL data, guaranteeing non-repudiation.
19
+ 4. **Trustless Proof of Ownership:** Enables a decentralized workflow where a web crawler verifies the presence of a challenge code, and the library independently verifies the cryptographic signature against the owner's public keys.
20
+ 5. **Flexible Serialization:** Built-in support for flawless JSON rehydration (`toJSON` / `fromJSON`) and Base64 serialization (`serialize` / `deserialize`) for seamless database storage and API transmission.
24
21
 
25
- 4. **Proof of Ownership:** Enables a trustless workflow where a crawler can verify the presence of a code, and the library verifies the cryptographic signature against the owner's public keys.
22
+ ---
26
23
 
27
- 5. **Seamless Serialization:** Includes built-in support for JSON rehydration and Base64 serialization for easy storage in databases or transmission via QR codes.
24
+ ## Installation
28
25
 
26
+ ```bash
27
+ npm install @majikah/majik-slink @majikah/majik-key @majikah/majik-signature
28
+ ```
29
29
 
30
- ## Related Projects
30
+ ## Quick Start & Usage
31
31
 
32
+ ### 1. Creating and Signing a New SLink
33
+ To link an identity to a URL, create a `MajikSLink` instance using a user's `MajikKey`.
32
34
 
33
- ### [Majik Signature](https://www.npmjs.com/package/@majikah/majik-signature)
34
- Hybrid post-quantum content signing the signing engine used by `signContent()` and `signFile()`.
35
+ ```typescript
36
+ import { MajikSLink } from "@majikah/majik-slink";
37
+ import { MajikKey } from "@majikah/majik-key";
35
38
 
36
- [Read Docs](https://majikah.solutions/products/majik-signature/docs) · [Microsoft Store](https://apps.microsoft.com/detail/9pl9g3xzvd1x)
39
+ // 1. Initialize your identity (MajikKey)
40
+ const key = await MajikKey.fromSeedPhrase("your seed phrase here...");
41
+ const userId = "user_abc123";
42
+ const muid = "muid_xyz987";
37
43
 
38
- [![Majik Signature Microsoft App Store](https://get.microsoft.com/images/en-us%20light.svg)](https://apps.microsoft.com/detail/9pl9g3xzvd1x)
44
+ // 2. Create the SLink payload
45
+ const targetUrl = "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=42s";
46
+ const slink = await MajikSLink.create(targetUrl, key, userId, muid, {
47
+ claimType: "ownership" // Defaults to "ownership" and verificationMethod: "dns_txt"
48
+ });
39
49
 
50
+ console.log("Canonical URL:", slink.cleanUrl); // https://www.youtube.com/watch
51
+ console.log("Verification Code:", slink.vCode); // majik-slink:xxxx-xxxx-xxxx...
52
+ ```
40
53
 
54
+ ### 2. Presenting the Challenge (For Verifiers)
55
+ If you are building an application that needs to ask a user to verify a URL, you can generate a challenge without a private key.
41
56
 
42
- ### [Majik Message](https://message.majikah.solutions)
43
- Secure messaging platform using Majik Keys and Majik Signatures for identity-bound communication.
57
+ ```typescript
58
+ const challenge = await MajikSLink.generateChallenge("https://github.com/majikah/repo");
44
59
 
45
- [Read Docs](https://majikah.solutions/products/majik-message/docs) · [Microsoft Store](https://apps.microsoft.com/detail/9pmjgvzzjspn)
60
+ if (challenge) {
61
+ console.log("Ask the user to place this code in their bio:", challenge.v_code);
62
+ console.log("Expected resource hash:", challenge.hash);
63
+ }
64
+ ```
46
65
 
47
- [![Majik Message Microsoft App Store](https://get.microsoft.com/images/en-us%20light.svg)](https://apps.microsoft.com/detail/9pmjgvzzjspn)
66
+ ### 3. DNS TXT Verification Formatting
67
+ For domain ownership claims, `MajikSLink` automatically formats the required DNS properties.
48
68
 
69
+ ```typescript
70
+ console.log("DNS Record Name:", slink.dnsRecordName); // e.g., _majik-challenge.github.com
71
+ console.log("DNS Record Value:", slink.dnsRecordValue); // e.g., majik-slink-verify=majik-slink:...
72
+ ```
49
73
 
50
- ### [Majik Key](https://www.npmjs.com/package/@majikah/majik-key)
51
- Seed phrase account library required peer dependency for signing and encryption.
74
+ ### 4. Verifying Cryptographic Signatures
75
+ When validating an SLink supplied by a user, use their public keys to verify the payload hasn't been tampered with.
52
76
 
53
- [Read More Information](https://majikah.solutions/sdk/majik-key)
77
+ ```typescript
78
+ import { MajikSignature } from "@majikah/majik-signature";
54
79
 
55
- ### [Majik Envelope](https://www.npmjs.com/package/@majikah/majik-envelope)
56
- Post-quantum group encryption — used to encrypt and share private personal info.
80
+ // Retrieve public keys (usually fetched from a trusted identity registry)
81
+ const publicKeys = {
82
+ ed25519: "base64-ed25519-pubkey",
83
+ mldsa: "base64-mldsa-pubkey"
84
+ };
57
85
 
58
- [Read More Information](https://majikah.solutions/sdk/majik-envelope)
86
+ // Verify the hydrated SLink instance
87
+ const verificationResult = slink.verify(publicKeys);
59
88
 
89
+ if (verificationResult.valid) {
90
+ console.log("Valid signature from:", verificationResult.signerId);
91
+ slink.markVerified(); // Update state to verified
92
+ } else {
93
+ slink.markSignatureInvalid();
94
+ }
95
+ ```
60
96
 
61
- ### [Majik Universal ID](https://id.majikah.solutions)
62
- A cryptographically anchored identity layer for the Majikah ecosystem.
97
+ ### 5. Serialization and Deserialization
98
+ Easily store and retrieve `MajikSLink` objects from your database.
63
99
 
64
- [Read More Information](https://majikah.solutions/sdk/majik-universal-id)
100
+ ```typescript
101
+ // Export to JSON for DB storage
102
+ const jsonPayload = slink.toJSON();
65
103
 
104
+ // Hydrate from DB
105
+ const hydratedSLink = MajikSLink.fromJSON(jsonPayload);
106
+ ```
66
107
 
67
108
  ---
68
109
 
69
- ## Contributing
110
+ ## Related Ecosystem Projects
70
111
 
71
- If you want to contribute or help extend support, reach out via email. All contributions are welcome!
112
+ * **[Majik Signature](https://www.npmjs.com/package/@majikah/majik-signature)**: Hybrid post-quantum content signing engine. ([Microsoft Store](https://apps.microsoft.com/detail/9pl9g3xzvd1x))
113
+ * **[Majik Message](https://message.majikah.solutions)**: Secure messaging platform utilizing Majik Keys and hybrid ML-KEM-768/X25519 protocols for identity-bound communication. ([Microsoft Store](https://apps.microsoft.com/detail/9pmjgvzzjspn))
114
+ * **[Majik Key](https://www.npmjs.com/package/@majikah/majik-key)**: Foundational seed phrase account library.
115
+ * **[Majik Universal ID](https://id.majikah.solutions)**: A cryptographically anchored identity layer for the Majikah ecosystem.
116
+ * **[Majik Envelope](https://www.npmjs.com/package/@majikah/majik-envelope)**: Post-quantum group encryption.
72
117
 
73
118
  ---
74
119
 
@@ -80,9 +125,10 @@ If you want to contribute or help extend support, reach out via email. All contr
80
125
 
81
126
  ## Author
82
127
 
83
- Made with 💙 by [@thezelijah](https://github.com/jedlsf)
128
+ Made with 💙 by **Zelijah**
84
129
 
85
- **Developer**: Josef Elijah Fabian
130
+ **Developer**: Josef Elijah Delos Santos Fabian
131
+ **Organization**: Majikah Solutions OPC
86
132
  **GitHub**: [https://github.com/jedlsf](https://github.com/jedlsf)
87
133
  **Project Repository**: [https://github.com/Majikah/majik-slink](https://github.com/Majikah/majik-slink)
88
134
 
@@ -90,6 +136,6 @@ Made with 💙 by [@thezelijah](https://github.com/jedlsf)
90
136
 
91
137
  ## Contact
92
138
 
93
- - **Business Email**: [business@thezelijah.world](mailto:business@thezelijah.world)
94
- - **Official Website**: [https://www.thezelijah.world](https://www.thezelijah.world)
95
- - **ID Web App**: [https://id.majikah.solutions](https://id.majikah.solutions)
139
+ * **Business Email**: [business@thezelijah.world](mailto:business@thezelijah.world)
140
+ * **Official Website**: [https://www.thezelijah.world](https://www.thezelijah.world)
141
+ * **ID Web App**: [https://id.majikah.solutions](https://id.majikah.solutions)
@@ -1,30 +1,27 @@
1
- import { MajikSignatureJSON } from "@majikah/majik-signature";
2
- /** Source platform of the signed URL */
3
- export type SLinkSource = "youtube" | "x" | "instagram" | "tiktok" | "github" | "website" | "other";
4
- /** Structured URL parts extracted during normalisation */
5
- export interface UrlInfo {
6
- /** Full registered domain e.g. "youtube.com" */
7
- domain: string;
8
- /** Second-level domain only e.g. "youtube" */
9
- sld: string;
10
- /** Top-level domain e.g. "com" */
11
- tld: string;
12
- /** Subdomain or null e.g. "www" | null */
13
- subdomain: string | null;
14
- /** Normalised pathname e.g. "/watch/dQw4w9WgXcQ" */
15
- path: string;
16
- /** Reconstructed clean URL (no params, no fragment) */
17
- cleanUrl: string;
18
- /** The string that is actually hashed + signed */
19
- canonical: string;
20
- }
1
+ import { MajikSignatureCompactJSON } from "@majikah/majik-signature";
2
+ /**
3
+ * What kind of relationship the user claims to have with this URL.
4
+ * - "ownership": user controls the domain (can add a DNS TXT record).
5
+ * Strongest proof; independent of any single page's content.
6
+ * - "attribution": user controls the *content* at this URL (a profile bio,
7
+ * video description, repo README) but not the domain/DNS
8
+ * itself e.g. a YouTube channel, a GitHub profile.
9
+ * Provable only via page-embed code, never DNS.
10
+ * - "reference": user controls neither the domain nor the content.
11
+ * A pure attestation ("this URL is about me / relevant to
12
+ * me") with no independent proof possible. Should never
13
+ * be presented to end users as "verified" — surface it
14
+ * as "attested by <signer>" instead.
15
+ */
16
+ export type SLinkClaimType = "ownership" | "attribution" | "reference";
17
+ export type SLinkVerificationMethod = "dns_txt" | "page_content" | null;
21
18
  /** Verification status of a MajikSLink */
22
19
  export type SLinkVerificationStatus = "pending" | "unverified" | "verified" | "failed" | "sig_invalid";
23
- /** Shape persisted to / rehydrated from a database */
24
20
  export interface MajikSLinkJSON {
25
21
  version: 1;
26
22
  id: string;
27
23
  user_id: string;
24
+ muid: string;
28
25
  /** Full registered domain e.g. "youtube.com" */
29
26
  domain: string;
30
27
  /** Subdomain or null */
@@ -39,14 +36,35 @@ export interface MajikSLinkJSON {
39
36
  v_code: string;
40
37
  /** Detected source platform */
41
38
  source: SLinkSource;
39
+ claim_type: SLinkClaimType;
40
+ verification_method: SLinkVerificationMethod;
42
41
  /** Verification status */
43
42
  status: SLinkVerificationStatus;
44
43
  /** ISO timestamp of when this SLink was created */
45
44
  timestamp: string;
46
45
  /** ISO timestamp of the last verification attempt, or null */
47
46
  verified_at: string | null;
48
- /** The MajikSignature envelope covering the canonical URL */
49
- signature: MajikSignatureJSON;
47
+ /** Compact envelope no embedded public keys. Resolve via muid/signerId at verify time. */
48
+ signature: MajikSignatureCompactJSON;
49
+ }
50
+ /** Source platform of the signed URL */
51
+ export type SLinkSource = "youtube" | "x" | "instagram" | "tiktok" | "github" | "website" | "other";
52
+ /** Structured URL parts extracted during normalisation */
53
+ export interface UrlInfo {
54
+ /** Full registered domain e.g. "youtube.com" */
55
+ domain: string;
56
+ /** Second-level domain only e.g. "youtube" */
57
+ sld: string;
58
+ /** Top-level domain e.g. "com" */
59
+ tld: string;
60
+ /** Subdomain or null e.g. "www" | null */
61
+ subdomain: string | null;
62
+ /** Normalised pathname e.g. "/watch/dQw4w9WgXcQ" */
63
+ path: string;
64
+ /** Reconstructed clean URL (no params, no fragment) */
65
+ cleanUrl: string;
66
+ /** The string that is actually hashed + signed */
67
+ canonical: string;
50
68
  }
51
69
  /** Lightweight code-only payload — useful for the UI before full create() */
52
70
  export interface SLinkCodePreview {
@@ -59,3 +77,25 @@ export interface SLinkCodePreview {
59
77
  /** Parsed URL parts */
60
78
  urlInfo: UrlInfo;
61
79
  }
80
+ export interface MajikSLinkConstructorOptions {
81
+ version: 1;
82
+ id: string;
83
+ user_id: string;
84
+ muid: string;
85
+ domain: string;
86
+ sld: string;
87
+ tld: string;
88
+ subdomain: string | null;
89
+ path: string;
90
+ url: string;
91
+ clean_url: string;
92
+ hash: string;
93
+ v_code: string;
94
+ source: SLinkSource;
95
+ claim_type?: SLinkClaimType;
96
+ verification_method?: SLinkVerificationMethod;
97
+ status: SLinkVerificationStatus;
98
+ verified_at: Date | null;
99
+ signature: MajikSignatureCompactJSON;
100
+ timestamp: Date;
101
+ }
@@ -1,5 +1,5 @@
1
1
  import { MajikKey } from "@majikah/majik-key";
2
- import { SLinkSource, UrlInfo } from "./types";
2
+ import { SLinkClaimType, SLinkSource, SLinkVerificationMethod, UrlInfo } from "./types";
3
3
  export declare function sha256Hex(input: string): Promise<string>;
4
4
  export declare function generateId(): string;
5
5
  /**
@@ -22,3 +22,11 @@ export declare function parseUrlInfo(rawUrl: string): UrlInfo | null;
22
22
  * Used during fromJSON rehydration to avoid re-running PSL.
23
23
  */
24
24
  export declare function buildCanonical(subdomain: string | null, sld: string, tld: string, path: string): string;
25
+ /**
26
+ * Derive the natural default verification method for a claim type when the
27
+ * caller doesn't specify one explicitly.
28
+ * - "ownership" → "dns_txt" (strongest available proof for a domain the signer controls)
29
+ * - "attribution" → "page_content" (only proof possible without domain control)
30
+ * - "reference" → null (no independent proof is possible at all)
31
+ */
32
+ export declare function defaultVerificationMethod(claimType: SLinkClaimType): SLinkVerificationMethod;
@@ -118,3 +118,20 @@ export function parseUrlInfo(rawUrl) {
118
118
  export function buildCanonical(subdomain, sld, tld, path) {
119
119
  return `${CANONICAL_PREFIX}${subdomain ?? "root"}::${sld}::${tld}::${path}`;
120
120
  }
121
+ /**
122
+ * Derive the natural default verification method for a claim type when the
123
+ * caller doesn't specify one explicitly.
124
+ * - "ownership" → "dns_txt" (strongest available proof for a domain the signer controls)
125
+ * - "attribution" → "page_content" (only proof possible without domain control)
126
+ * - "reference" → null (no independent proof is possible at all)
127
+ */
128
+ export function defaultVerificationMethod(claimType) {
129
+ switch (claimType) {
130
+ case "ownership":
131
+ return "dns_txt";
132
+ case "attribution":
133
+ return "page_content";
134
+ case "reference":
135
+ return null;
136
+ }
137
+ }
package/dist/index.d.ts CHANGED
@@ -6,4 +6,4 @@ export { MajikSLink } from "./majik-slink";
6
6
  export type * from "./core/types";
7
7
  export * from "./core/errors";
8
8
  export * from "./core/constants";
9
- export { parseUrlInfo, buildCanonical, detectSource } from "./core/utils";
9
+ export { parseUrlInfo, buildCanonical, detectSource, defaultVerificationMethod, } from "./core/utils";
package/dist/index.js CHANGED
@@ -9,4 +9,4 @@ export * from "./core/errors";
9
9
  // ── Constants ─────────────────────────────────────────────────────────────────
10
10
  export * from "./core/constants";
11
11
  // ── Low-level utilities (opt-in) ──────────────────────────────────────────────
12
- export { parseUrlInfo, buildCanonical, detectSource } from "./core/utils";
12
+ export { parseUrlInfo, buildCanonical, detectSource, defaultVerificationMethod, } from "./core/utils";
@@ -2,13 +2,13 @@
2
2
  * majik-slink.ts
3
3
  */
4
4
  import type { MajikKey } from "@majikah/majik-key";
5
- import { MajikSignature, type MajikSignatureJSON } from "@majikah/majik-signature";
6
- import { MajikSLinkJSON, SLinkCodePreview, SLinkSource, SLinkVerificationStatus, UrlInfo } from "./core/types";
5
+ import { MajikSignature, MajikSignatureCompactJSON, MajikSignerPublicKeys, VerificationResult } from "@majikah/majik-signature";
6
+ import { MajikSLinkJSON, SLinkClaimType, SLinkCodePreview, SLinkSource, SLinkVerificationMethod, SLinkVerificationStatus, UrlInfo } from "./core/types";
7
7
  /**
8
8
  * MajikSLink
9
9
  * ---------------------
10
- * MajikSLink — URL ownership signing and verification.
11
- * A MajikSLink proves that the holder of a MajikKey owns (or controls) a
10
+ * MajikSLink — URL binding, ownership, and attribution signing/verification.
11
+ * A MajikSLink cryptographically associates a MUID/MajikKey with a
12
12
  * publicly-accessible URL by:
13
13
  *
14
14
  * 1. Normalising the URL to a canonical form (scheme + subdomain + domain +
@@ -16,13 +16,28 @@ import { MajikSLinkJSON, SLinkCodePreview, SLinkSource, SLinkVerificationStatus,
16
16
  * 2. Deriving a short, deterministic verification code from a SHA-256 digest
17
17
  * of the canonical URL parts — so the same resource always produces the
18
18
  * same code regardless of query params or tracking tokens.
19
- * 3. Signing the canonical URL string with the owner's MajikKey via
20
- * MajikSignature (Ed25519 + ML-DSA-87 hybrid).
19
+ * 3. Signing the canonical URL string with the signer's MajikKey via
20
+ * MajikSignature (Ed25519 + ML-DSA-87 hybrid), stored in compact form
21
+ * (no embedded public keys — resolved externally at verify time via
22
+ * muid / signerId).
21
23
  *
22
- * The owner embeds the verification code (`v_code`) anywhere in the public
23
- * page (description, bio, comment, HTML body) and a scraper verifies its
24
- * presence. Once found, the MajikSignature certifies the association between
25
- * the signer's identity and the URL.
24
+ * The *strength* of the claim is described by `claimType`:
25
+ * - "ownership": the signer controls the domain. Verifiable via DNS TXT
26
+ * (strongest survives page changes) or page content.
27
+ * - "attribution": the signer controls the content at this URL (a channel
28
+ * bio, repo README) but not the domain/DNS. Verifiable
29
+ * only via page content.
30
+ * - "reference": the signer controls neither. A signed attestation with
31
+ * no independent proof possible — never present this to
32
+ * end users as "verified"; surface it as "attested by
33
+ * <signer>" instead.
34
+ * See `SLinkClaimType` / `SLinkVerificationMethod` for the full contract.
35
+ *
36
+ * For "dns_txt" verification, the owner publishes `dnsRecordValue` at
37
+ * `dnsRecordName`. For "page_content", the owner embeds `vCode` anywhere in
38
+ * the public page (description, bio, comment, HTML body) and a scraper
39
+ * verifies its presence. Once found, the MajikSignature certifies the
40
+ * association between the signer's identity and the URL.
26
41
  *
27
42
  * Canonical URL format (what is signed):
28
43
  * "majik-slink-v1:<subdomain>::<sld>::<tld>::<path>"
@@ -36,6 +51,7 @@ export declare class MajikSLink {
36
51
  private readonly _version;
37
52
  private readonly _id;
38
53
  private readonly _user_id;
54
+ private readonly _muid;
39
55
  private readonly _domain;
40
56
  private readonly _sld;
41
57
  private readonly _tld;
@@ -48,13 +64,16 @@ export declare class MajikSLink {
48
64
  private readonly _source;
49
65
  private _status;
50
66
  private _verified_at;
67
+ private readonly _claim_type;
68
+ private readonly _verification_method;
51
69
  private readonly _signature;
52
- private _signatureInstance;
70
+ private _resolvedSignature;
53
71
  private readonly _timestamp;
54
72
  private constructor();
55
73
  get version(): 1;
56
74
  get id(): string;
57
75
  get userId(): string;
76
+ get muid(): string;
58
77
  /** Full registered domain e.g. "youtube.com" */
59
78
  get domain(): string;
60
79
  /** Second-level domain e.g. "youtube" */
@@ -77,20 +96,59 @@ export declare class MajikSLink {
77
96
  get status(): SLinkVerificationStatus;
78
97
  get verifiedAt(): Date | null;
79
98
  /**
80
- * Get the signature as a MajikSignature instance.
81
- * The instance is cached to avoid re-parsing on every access.
99
+ * The resolved MajikSignature instance, if `resolveSignature()` has been
100
+ * called. Null until then MajikSLink stores only the compact envelope
101
+ * (no embedded public keys), so there is nothing to eagerly resolve.
102
+ */
103
+ get signature(): MajikSignature | null;
104
+ /**
105
+ * Get the raw compact signature JSON as stored/persisted.
106
+ * No public keys are embedded — resolve them externally (e.g. by
107
+ * `signerId` / `muid` from your key registry) before verifying.
82
108
  */
83
- get signature(): MajikSignature;
109
+ get signatureJSON(): MajikSignatureCompactJSON;
84
110
  /**
85
- * Get the raw signature JSON.
86
- * Useful when you need the serializable form without parsing.
111
+ * Resolve the compact signature into a full MajikSignature instance, using
112
+ * externally-supplied public keys (looked up by `this.muid` or
113
+ * `this.signatureJSON.signerId` from your key registry).
114
+ *
115
+ * There is nothing to fall back to by design — MajikSLink never stores
116
+ * public keys inline, so `publicKeys` is mandatory. The result is cached;
117
+ * subsequent calls with the same (correct) keys are free.
118
+ *
119
+ * @throws {MajikSLinkValidationError} if `publicKeys.signerId` does not
120
+ * match the signerId recorded on this SLink's signature.
121
+ *
122
+ * @example
123
+ * const keys = await resolvePublicKeysForMuid(slink.muid);
124
+ * const sig = slink.resolveSignature(keys);
125
+ * console.log(sig.hasTSA);
87
126
  */
88
- get signatureJSON(): MajikSignatureJSON;
127
+ resolveSignature(publicKeys: MajikSignerPublicKeys): MajikSignature;
128
+ get claimType(): SLinkClaimType;
129
+ get verificationMethod(): SLinkVerificationMethod;
89
130
  get timestamp(): Date;
90
131
  /** True when a successful scrape + signature verification has been recorded */
91
132
  get isVerified(): boolean;
92
133
  /** The OTP-display chunks (groups of 4 hex chars from the hash) */
93
134
  get codeChunks(): string[];
135
+ /**
136
+ * The DNS TXT record name to publish for `"dns_txt"` verification.
137
+ * Only meaningful when `verificationMethod === "dns_txt"` — for
138
+ * `"page_content"` or `"reference"` SLinks this value is simply
139
+ * irrelevant (not an error), so check `verificationMethod` before using it.
140
+ *
141
+ * @example "_majik-challenge.example.com"
142
+ */
143
+ get dnsRecordName(): string;
144
+ /**
145
+ * The DNS TXT record value to publish for `"dns_txt"` verification.
146
+ * See `dnsRecordName` — only meaningful when
147
+ * `verificationMethod === "dns_txt"`.
148
+ *
149
+ * @example "majik-slink-verify=majik-slink:a1b2c3d4..."
150
+ */
151
+ get dnsRecordValue(): string;
94
152
  /**
95
153
  * Create and sign a new MajikSLink.
96
154
  *
@@ -113,10 +171,12 @@ export declare class MajikSLink {
113
171
  * );
114
172
  * console.log(slink.vCode); // "majik-slink:a1b2c3d4…"
115
173
  */
116
- static create(rawUrl: string, key: MajikKey, userId: string, options?: {
174
+ static create(rawUrl: string, key: MajikKey, userId: string, muid: string, options?: {
117
175
  id?: string;
118
176
  timestamp?: Date;
119
177
  status?: SLinkVerificationStatus;
178
+ claimType?: SLinkClaimType;
179
+ verificationMethod?: SLinkVerificationMethod;
120
180
  }): Promise<MajikSLink>;
121
181
  /**
122
182
  * Derive the verification code and URL info for a raw URL *without* signing.
@@ -133,43 +193,53 @@ export declare class MajikSLink {
133
193
  */
134
194
  static generateChallenge(rawUrl: string): Promise<SLinkCodePreview | null>;
135
195
  /**
136
- * Verify the embedded MajikSignature of a persisted MajikSLink.
196
+ * Verify the stored MajikSignature of a persisted MajikSLink against
197
+ * externally-supplied public keys.
137
198
  *
138
- * This does NOT scrape the target page — it only cryptographically checks
139
- * that the signature covers the canonical URL and was issued by the stated
140
- * signer.
199
+ * This does NOT scrape the target page or check DNS — it only
200
+ * cryptographically checks that the signature covers the canonical URL
201
+ * and was issued by the holder of `publicKeys`. No public keys are
202
+ * embedded in the stored SLink; you must resolve them yourself (e.g. by
203
+ * `signerId` / `muid` from your key registry) before calling this.
141
204
  *
142
- * Use in conjunction with your scraper to fully verify ownership:
143
- * 1. Scrape the page and confirm `v_code` is present.
205
+ * Use in conjunction with your DNS/scrape check to fully verify a claim:
206
+ * 1. Look up (dns_txt) or scrape (page_content) per `verificationMethod`
207
+ * and confirm `v_code` is present.
144
208
  * 2. Call `verifySignature()` to confirm the cryptographic provenance.
145
209
  *
146
210
  * @example
147
- * const result = MajikSLink.verifySignature(slink, signerPublicKeys);
148
- * if (!result.valid) console.warn("Signature tampered:", result.reason);
211
+ * const keys = await resolvePublicKeysForMuid(slink.muid);
212
+ * const result = MajikSLink.verifySignature(slink, keys);
213
+ * if (!result.valid) console.warn("Signature invalid:", result.reason);
149
214
  */
150
- static verifySignature(slink: MajikSLink | MajikSLinkJSON, publicKeys: Parameters<typeof MajikSignature.verify>[2]): ReturnType<typeof MajikSignature.verify>;
215
+ static verifySignature(slink: MajikSLink | MajikSLinkJSON, publicKeys: MajikSignerPublicKeys): VerificationResult;
151
216
  /**
152
- * Verify this SLink's embedded signature against the provided public keys.
217
+ * Verify this SLink's stored signature against externally-supplied
218
+ * public keys.
153
219
  *
154
220
  * This validates that:
155
221
  * 1. The signature cryptographically matches the canonical URL
156
222
  * 2. The signature was created by the holder of the private key
223
+ * corresponding to `publicKeys`
157
224
  *
158
- * Note: This does NOT scrape the page. Use with `markVerified()` after
159
- * a successful scrape to complete the verification flow.
225
+ * Note: This does NOT scrape the page or check DNS. Use with
226
+ * `markVerified()` after a successful DNS lookup / scrape (per
227
+ * `verificationMethod`) to complete the verification flow.
160
228
  *
161
- * @param publicKeys The signer's public keys (classic Ed25519 + ML-DSA-87)
229
+ * @param publicKeys The signer's public keys (classic Ed25519 + ML-DSA-87),
230
+ * resolved externally — e.g. by `this.muid`.
162
231
  * @returns Verification result with valid/invalid status and optional reason
163
232
  *
164
233
  * @example
165
- * // After scraping and finding v_code on the page:
166
- * const result = slink.verify(user.publicKeys);
234
+ * // After confirming v_code via DNS or page scrape:
235
+ * const keys = await resolvePublicKeysForMuid(slink.muid);
236
+ * const result = slink.verify(keys);
167
237
  * if (result.valid) {
168
238
  * slink.markVerified();
169
239
  * await db.save(slink);
170
240
  * }
171
241
  */
172
- verify(publicKeys?: Parameters<typeof MajikSignature.verify>[2]): ReturnType<typeof MajikSignature.verify>;
242
+ verify(publicKeys: MajikSignerPublicKeys): VerificationResult;
173
243
  /**
174
244
  * Record a successful scrape verification.
175
245
  * Should be called by your server route after a positive scrape result.
@@ -206,7 +276,8 @@ export declare class MajikSLink {
206
276
  toJSON(): MajikSLinkJSON;
207
277
  /**
208
278
  * Rehydrate a MajikSLink from a plain JSON object (e.g. from a database row).
209
- * Does NOT re-verify the signature — call `verify()` explicitly.
279
+ * Does NOT re-verify the signature — call `verify()` explicitly with
280
+ * externally-resolved public keys.
210
281
  *
211
282
  * @throws {MajikSLinkSerializationError} if any required field is missing or malformed.
212
283
  */
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * majik-slink.ts
3
3
  */
4
- import { base64ToBytes, MajikSignature, } from "@majikah/majik-signature";
4
+ import { MajikSignature, } from "@majikah/majik-signature";
5
5
  import { MajikSLinkError, MajikSLinkSerializationError, MajikSLinkSigningError, MajikSLinkValidationError, } from "./core/errors";
6
6
  import { CODE_HEX_LENGTH, CODE_PREFIX, SLINK_VERSION } from "./core/constants";
7
- import { assertMajikKey, assertNonEmptyString, assertUnlockedKey, assertValidHttpUrl, buildCanonical, detectSource, generateId, parseUrlInfo, sha256Hex, } from "./core/utils";
7
+ import { assertMajikKey, assertNonEmptyString, assertUnlockedKey, assertValidHttpUrl, buildCanonical, defaultVerificationMethod, detectSource, generateId, parseUrlInfo, sha256Hex, } from "./core/utils";
8
8
  // ─── MajikSLink ───────────────────────────────────────────────────────────────
9
9
  /**
10
10
  * MajikSLink
11
11
  * ---------------------
12
- * MajikSLink — URL ownership signing and verification.
13
- * A MajikSLink proves that the holder of a MajikKey owns (or controls) a
12
+ * MajikSLink — URL binding, ownership, and attribution signing/verification.
13
+ * A MajikSLink cryptographically associates a MUID/MajikKey with a
14
14
  * publicly-accessible URL by:
15
15
  *
16
16
  * 1. Normalising the URL to a canonical form (scheme + subdomain + domain +
@@ -18,13 +18,28 @@ import { assertMajikKey, assertNonEmptyString, assertUnlockedKey, assertValidHtt
18
18
  * 2. Deriving a short, deterministic verification code from a SHA-256 digest
19
19
  * of the canonical URL parts — so the same resource always produces the
20
20
  * same code regardless of query params or tracking tokens.
21
- * 3. Signing the canonical URL string with the owner's MajikKey via
22
- * MajikSignature (Ed25519 + ML-DSA-87 hybrid).
21
+ * 3. Signing the canonical URL string with the signer's MajikKey via
22
+ * MajikSignature (Ed25519 + ML-DSA-87 hybrid), stored in compact form
23
+ * (no embedded public keys — resolved externally at verify time via
24
+ * muid / signerId).
23
25
  *
24
- * The owner embeds the verification code (`v_code`) anywhere in the public
25
- * page (description, bio, comment, HTML body) and a scraper verifies its
26
- * presence. Once found, the MajikSignature certifies the association between
27
- * the signer's identity and the URL.
26
+ * The *strength* of the claim is described by `claimType`:
27
+ * - "ownership": the signer controls the domain. Verifiable via DNS TXT
28
+ * (strongest survives page changes) or page content.
29
+ * - "attribution": the signer controls the content at this URL (a channel
30
+ * bio, repo README) but not the domain/DNS. Verifiable
31
+ * only via page content.
32
+ * - "reference": the signer controls neither. A signed attestation with
33
+ * no independent proof possible — never present this to
34
+ * end users as "verified"; surface it as "attested by
35
+ * <signer>" instead.
36
+ * See `SLinkClaimType` / `SLinkVerificationMethod` for the full contract.
37
+ *
38
+ * For "dns_txt" verification, the owner publishes `dnsRecordValue` at
39
+ * `dnsRecordName`. For "page_content", the owner embeds `vCode` anywhere in
40
+ * the public page (description, bio, comment, HTML body) and a scraper
41
+ * verifies its presence. Once found, the MajikSignature certifies the
42
+ * association between the signer's identity and the URL.
28
43
  *
29
44
  * Canonical URL format (what is signed):
30
45
  * "majik-slink-v1:<subdomain>::<sld>::<tld>::<path>"
@@ -38,6 +53,7 @@ export class MajikSLink {
38
53
  _version;
39
54
  _id;
40
55
  _user_id;
56
+ _muid;
41
57
  // URL parts (normalised)
42
58
  _domain; // "youtube.com"
43
59
  _sld; // "youtube"
@@ -51,14 +67,17 @@ export class MajikSLink {
51
67
  _source;
52
68
  _status;
53
69
  _verified_at;
70
+ _claim_type;
71
+ _verification_method;
54
72
  _signature;
55
- _signatureInstance = null; // Cached instance
73
+ _resolvedSignature = null;
56
74
  _timestamp;
57
75
  // ── Private constructor — use MajikSLink.create() ────────────────────────
58
76
  constructor(data) {
59
77
  this._version = data.version;
60
78
  this._id = data.id;
61
79
  this._user_id = data.user_id;
80
+ this._muid = data.muid;
62
81
  this._domain = data.domain;
63
82
  this._sld = data.sld;
64
83
  this._tld = data.tld;
@@ -69,6 +88,12 @@ export class MajikSLink {
69
88
  this._hash = data.hash;
70
89
  this._v_code = data.v_code;
71
90
  this._source = data.source;
91
+ // `??` — not `||` — so an explicit `null` (a deliberate "no verification
92
+ // possible" for claim_type "reference") is preserved rather than being
93
+ // silently overwritten by the default.
94
+ this._claim_type = data.claim_type ?? "ownership";
95
+ this._verification_method =
96
+ data.verification_method ?? defaultVerificationMethod(this._claim_type);
72
97
  this._status = data.status;
73
98
  this._verified_at = data.verified_at;
74
99
  this._signature = data.signature;
@@ -84,6 +109,9 @@ export class MajikSLink {
84
109
  get userId() {
85
110
  return this._user_id;
86
111
  }
112
+ get muid() {
113
+ return this._muid;
114
+ }
87
115
  /** Full registered domain e.g. "youtube.com" */
88
116
  get domain() {
89
117
  return this._domain;
@@ -130,22 +158,53 @@ export class MajikSLink {
130
158
  return this._verified_at;
131
159
  }
132
160
  /**
133
- * Get the signature as a MajikSignature instance.
134
- * The instance is cached to avoid re-parsing on every access.
161
+ * The resolved MajikSignature instance, if `resolveSignature()` has been
162
+ * called. Null until then MajikSLink stores only the compact envelope
163
+ * (no embedded public keys), so there is nothing to eagerly resolve.
135
164
  */
136
165
  get signature() {
137
- if (!this._signatureInstance) {
138
- this._signatureInstance = MajikSignature.fromJSON(this._signature);
139
- }
140
- return this._signatureInstance;
166
+ return this._resolvedSignature;
141
167
  }
142
168
  /**
143
- * Get the raw signature JSON.
144
- * Useful when you need the serializable form without parsing.
169
+ * Get the raw compact signature JSON as stored/persisted.
170
+ * No public keys are embedded resolve them externally (e.g. by
171
+ * `signerId` / `muid` from your key registry) before verifying.
145
172
  */
146
173
  get signatureJSON() {
147
174
  return this._signature;
148
175
  }
176
+ /**
177
+ * Resolve the compact signature into a full MajikSignature instance, using
178
+ * externally-supplied public keys (looked up by `this.muid` or
179
+ * `this.signatureJSON.signerId` from your key registry).
180
+ *
181
+ * There is nothing to fall back to by design — MajikSLink never stores
182
+ * public keys inline, so `publicKeys` is mandatory. The result is cached;
183
+ * subsequent calls with the same (correct) keys are free.
184
+ *
185
+ * @throws {MajikSLinkValidationError} if `publicKeys.signerId` does not
186
+ * match the signerId recorded on this SLink's signature.
187
+ *
188
+ * @example
189
+ * const keys = await resolvePublicKeysForMuid(slink.muid);
190
+ * const sig = slink.resolveSignature(keys);
191
+ * console.log(sig.hasTSA);
192
+ */
193
+ resolveSignature(publicKeys) {
194
+ if (!this._resolvedSignature) {
195
+ if (publicKeys.signerId !== this._signature.signerId) {
196
+ throw new MajikSLinkValidationError(`publicKeys are for signer "${publicKeys.signerId}" but this SLink was signed by "${this._signature.signerId}".`);
197
+ }
198
+ this._resolvedSignature = MajikSignature.fromCompact(this._signature, publicKeys);
199
+ }
200
+ return this._resolvedSignature;
201
+ }
202
+ get claimType() {
203
+ return this._claim_type;
204
+ }
205
+ get verificationMethod() {
206
+ return this._verification_method;
207
+ }
149
208
  get timestamp() {
150
209
  return this._timestamp;
151
210
  }
@@ -162,6 +221,27 @@ export class MajikSLink {
162
221
  }
163
222
  return chunks;
164
223
  }
224
+ /**
225
+ * The DNS TXT record name to publish for `"dns_txt"` verification.
226
+ * Only meaningful when `verificationMethod === "dns_txt"` — for
227
+ * `"page_content"` or `"reference"` SLinks this value is simply
228
+ * irrelevant (not an error), so check `verificationMethod` before using it.
229
+ *
230
+ * @example "_majik-challenge.example.com"
231
+ */
232
+ get dnsRecordName() {
233
+ return `_majik-challenge.${this._domain}`;
234
+ }
235
+ /**
236
+ * The DNS TXT record value to publish for `"dns_txt"` verification.
237
+ * See `dnsRecordName` — only meaningful when
238
+ * `verificationMethod === "dns_txt"`.
239
+ *
240
+ * @example "majik-slink-verify=majik-slink:a1b2c3d4..."
241
+ */
242
+ get dnsRecordValue() {
243
+ return `majik-slink-verify=${this._v_code}`;
244
+ }
165
245
  // ── Static: create ────────────────────────────────────────────────────────
166
246
  /**
167
247
  * Create and sign a new MajikSLink.
@@ -185,10 +265,11 @@ export class MajikSLink {
185
265
  * );
186
266
  * console.log(slink.vCode); // "majik-slink:a1b2c3d4…"
187
267
  */
188
- static async create(rawUrl, key, userId, options) {
268
+ static async create(rawUrl, key, userId, muid, options) {
189
269
  // ── Validate inputs ──────────────────────────────────────────────────
190
270
  assertNonEmptyString(rawUrl, "url");
191
271
  assertNonEmptyString(userId, "userId");
272
+ assertNonEmptyString(muid, "Majik Universal ID");
192
273
  assertMajikKey(key);
193
274
  assertUnlockedKey(key);
194
275
  assertValidHttpUrl(rawUrl);
@@ -196,16 +277,14 @@ export class MajikSLink {
196
277
  if (!urlInfo) {
197
278
  throw new MajikSLinkValidationError(`Could not parse URL into a valid domain: "${rawUrl}". Ensure it is a public https:// URL.`);
198
279
  }
199
- // ── Hash the canonical string ────────────────────────────────────────
280
+ const ts = options?.timestamp ?? new Date();
200
281
  const hash = await sha256Hex(urlInfo.canonical);
201
282
  const v_code = `${CODE_PREFIX}${hash.slice(0, CODE_HEX_LENGTH)}`;
202
- // ── Sign with MajikSignature ─────────────────────────────────────────
203
283
  let signature;
204
284
  try {
205
- signature = await MajikSignature.sign(urlInfo.canonical, // what we're signing
206
- key, {
285
+ signature = await MajikSignature.sign(urlInfo.canonical, key, {
207
286
  contentType: "majik-slink/url",
208
- timestamp: (options?.timestamp ?? new Date()).toISOString(),
287
+ timestamp: ts.toISOString(),
209
288
  });
210
289
  }
211
290
  catch (err) {
@@ -215,6 +294,7 @@ export class MajikSLink {
215
294
  version: SLINK_VERSION,
216
295
  id: options?.id ?? generateId(),
217
296
  user_id: userId,
297
+ muid,
218
298
  domain: urlInfo.domain,
219
299
  sld: urlInfo.sld,
220
300
  tld: urlInfo.tld,
@@ -225,10 +305,12 @@ export class MajikSLink {
225
305
  hash,
226
306
  v_code,
227
307
  source: detectSource(urlInfo.sld),
308
+ claim_type: options?.claimType ?? "ownership",
309
+ verification_method: options?.verificationMethod ?? null,
228
310
  status: options?.status ?? "unverified",
229
311
  verified_at: null,
230
- signature: signature.toJSON(),
231
- timestamp: options?.timestamp ?? new Date(),
312
+ signature: signature.toCompact(),
313
+ timestamp: ts,
232
314
  });
233
315
  }
234
316
  // ── Static: generateChallenge ─────────────────────────────────────────────
@@ -261,65 +343,63 @@ export class MajikSLink {
261
343
  }
262
344
  // ── Static: verifySignature ───────────────────────────────────────────────
263
345
  /**
264
- * Verify the embedded MajikSignature of a persisted MajikSLink.
346
+ * Verify the stored MajikSignature of a persisted MajikSLink against
347
+ * externally-supplied public keys.
265
348
  *
266
- * This does NOT scrape the target page — it only cryptographically checks
267
- * that the signature covers the canonical URL and was issued by the stated
268
- * signer.
349
+ * This does NOT scrape the target page or check DNS — it only
350
+ * cryptographically checks that the signature covers the canonical URL
351
+ * and was issued by the holder of `publicKeys`. No public keys are
352
+ * embedded in the stored SLink; you must resolve them yourself (e.g. by
353
+ * `signerId` / `muid` from your key registry) before calling this.
269
354
  *
270
- * Use in conjunction with your scraper to fully verify ownership:
271
- * 1. Scrape the page and confirm `v_code` is present.
355
+ * Use in conjunction with your DNS/scrape check to fully verify a claim:
356
+ * 1. Look up (dns_txt) or scrape (page_content) per `verificationMethod`
357
+ * and confirm `v_code` is present.
272
358
  * 2. Call `verifySignature()` to confirm the cryptographic provenance.
273
359
  *
274
360
  * @example
275
- * const result = MajikSLink.verifySignature(slink, signerPublicKeys);
276
- * if (!result.valid) console.warn("Signature tampered:", result.reason);
361
+ * const keys = await resolvePublicKeysForMuid(slink.muid);
362
+ * const result = MajikSLink.verifySignature(slink, keys);
363
+ * if (!result.valid) console.warn("Signature invalid:", result.reason);
277
364
  */
278
365
  static verifySignature(slink, publicKeys) {
279
366
  const json = slink instanceof MajikSLink ? slink.toJSON() : slink;
280
- // Reconstruct the canonical string from stored parts
281
- // We need to extract sld and tld from the domain field
282
367
  const parts = json.domain.split(".");
283
368
  const tld = parts[parts.length - 1] ?? "";
284
369
  const sld = parts.slice(0, -1).join(".") || json.domain;
285
370
  const canonical = buildCanonical(json.subdomain, sld, tld, json.path);
286
- return MajikSignature.verify(canonical, json.signature, publicKeys);
371
+ return MajikSignature.verifyCompact(canonical, json.signature, publicKeys);
287
372
  }
288
373
  // ── Instance: verify ──────────────────────────────────────────────────────
289
374
  /**
290
- * Verify this SLink's embedded signature against the provided public keys.
375
+ * Verify this SLink's stored signature against externally-supplied
376
+ * public keys.
291
377
  *
292
378
  * This validates that:
293
379
  * 1. The signature cryptographically matches the canonical URL
294
380
  * 2. The signature was created by the holder of the private key
381
+ * corresponding to `publicKeys`
295
382
  *
296
- * Note: This does NOT scrape the page. Use with `markVerified()` after
297
- * a successful scrape to complete the verification flow.
383
+ * Note: This does NOT scrape the page or check DNS. Use with
384
+ * `markVerified()` after a successful DNS lookup / scrape (per
385
+ * `verificationMethod`) to complete the verification flow.
298
386
  *
299
- * @param publicKeys The signer's public keys (classic Ed25519 + ML-DSA-87)
387
+ * @param publicKeys The signer's public keys (classic Ed25519 + ML-DSA-87),
388
+ * resolved externally — e.g. by `this.muid`.
300
389
  * @returns Verification result with valid/invalid status and optional reason
301
390
  *
302
391
  * @example
303
- * // After scraping and finding v_code on the page:
304
- * const result = slink.verify(user.publicKeys);
392
+ * // After confirming v_code via DNS or page scrape:
393
+ * const keys = await resolvePublicKeysForMuid(slink.muid);
394
+ * const result = slink.verify(keys);
305
395
  * if (result.valid) {
306
396
  * slink.markVerified();
307
397
  * await db.save(slink);
308
398
  * }
309
399
  */
310
400
  verify(publicKeys) {
311
- const signerKeys = {
312
- edPublicKey: publicKeys?.edPublicKey ||
313
- base64ToBytes(this.signature.signerEdPublicKey),
314
- mlDsaPublicKey: publicKeys?.mlDsaPublicKey ||
315
- base64ToBytes(this.signature.signerMlDsaPublicKey),
316
- signerId: publicKeys?.signerId || this.signature.signerId,
317
- };
318
- // Rebuild canonical string from stored parts
319
401
  const canonical = buildCanonical(this._subdomain, this._sld, this._tld, this._path);
320
- this.signature.validate();
321
- // Verify the signature against the canonical URL
322
- return MajikSignature.verify(canonical, this._signature, signerKeys);
402
+ return MajikSignature.verifyCompact(canonical, this._signature, publicKeys);
323
403
  }
324
404
  // ── Status mutation ───────────────────────────────────────────────────────
325
405
  /**
@@ -388,6 +468,7 @@ export class MajikSLink {
388
468
  version: this._version,
389
469
  id: this._id,
390
470
  user_id: this._user_id,
471
+ muid: this._muid,
391
472
  domain: this._domain,
392
473
  subdomain: this._subdomain,
393
474
  path: this._path,
@@ -395,6 +476,8 @@ export class MajikSLink {
395
476
  hash: this._hash,
396
477
  v_code: this._v_code,
397
478
  source: this._source,
479
+ claim_type: this._claim_type,
480
+ verification_method: this._verification_method,
398
481
  status: this._status,
399
482
  timestamp: this._timestamp.toISOString(),
400
483
  verified_at: this._verified_at?.toISOString() ?? null,
@@ -403,7 +486,8 @@ export class MajikSLink {
403
486
  }
404
487
  /**
405
488
  * Rehydrate a MajikSLink from a plain JSON object (e.g. from a database row).
406
- * Does NOT re-verify the signature — call `verify()` explicitly.
489
+ * Does NOT re-verify the signature — call `verify()` explicitly with
490
+ * externally-resolved public keys.
407
491
  *
408
492
  * @throws {MajikSLinkSerializationError} if any required field is missing or malformed.
409
493
  */
@@ -419,10 +503,23 @@ export class MajikSLink {
419
503
  assertNonEmptyString(json?.hash, "hash");
420
504
  assertNonEmptyString(json?.v_code, "v_code");
421
505
  assertNonEmptyString(json?.source, "source");
506
+ assertNonEmptyString(json?.claim_type, "claim_type");
422
507
  assertNonEmptyString(json?.status, "status");
423
508
  assertNonEmptyString(json?.timestamp, "timestamp");
509
+ if (json?.claim_type !== "ownership" &&
510
+ json?.claim_type !== "attribution" &&
511
+ json?.claim_type !== "reference") {
512
+ throw new MajikSLinkSerializationError(`"claim_type" must be "ownership", "attribution", or "reference". Got: ${String(json?.claim_type)}`);
513
+ }
514
+ // verification_method is nullable by design — check it's a valid
515
+ // value rather than asserting non-empty.
516
+ if (json?.verification_method !== null &&
517
+ json?.verification_method !== "dns_txt" &&
518
+ json?.verification_method !== "page_content") {
519
+ throw new MajikSLinkSerializationError(`"verification_method" must be "dns_txt", "page_content", or null. Got: ${String(json?.verification_method)}`);
520
+ }
424
521
  if (!json?.signature || typeof json.signature !== "object") {
425
- throw new MajikSLinkSerializationError('"signature" must be a MajikSignatureJSON object.');
522
+ throw new MajikSLinkSerializationError('"signature" must be a MajikSignatureCompactJSON object.');
426
523
  }
427
524
  // Derive sld / tld from stored domain ("youtube.com" → "youtube", "com")
428
525
  // Note: This is a simplified approach. For production use with multi-part
@@ -440,6 +537,7 @@ export class MajikSLink {
440
537
  version: SLINK_VERSION,
441
538
  id: json.id,
442
539
  user_id: json.user_id,
540
+ muid: json.muid,
443
541
  domain: json.domain,
444
542
  sld,
445
543
  tld,
@@ -450,6 +548,8 @@ export class MajikSLink {
450
548
  hash: json.hash,
451
549
  v_code: json.v_code,
452
550
  source: json.source,
551
+ claim_type: json.claim_type,
552
+ verification_method: json.verification_method,
453
553
  status: json.status,
454
554
  verified_at: json.verified_at ? new Date(json.verified_at) : null,
455
555
  signature: json.signature,
@@ -510,6 +610,7 @@ export class MajikSLink {
510
610
  ` user: ${this._user_id}`,
511
611
  ` url: ${this._clean_url}`,
512
612
  ` v_code: ${this._v_code}`,
613
+ ` claim: ${this._claim_type} (${this._verification_method ?? "no verification possible"})`,
513
614
  ` status: ${this._status}`,
514
615
  ` source: ${this._source}`,
515
616
  ` signed: ${this._timestamp.toISOString()}`,
@@ -517,3 +618,7 @@ export class MajikSLink {
517
618
  ].join("\n");
518
619
  }
519
620
  }
621
+ // Freeze static methods
622
+ Object.freeze(MajikSLink);
623
+ // Freeze instance methods
624
+ Object.freeze(MajikSLink.prototype);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@majikah/majik-slink",
3
3
  "type": "module",
4
4
  "description": "Majik SLink is a specialized TypeScript library designed for URL ownership verification and cryptographic identity linking. It provides a robust mechanism to prove that a specific digital identity (a MajikKey) controls a publicly accessible web resource (such as a YouTube channel, social media profile, or personal blog).",
5
- "version": "0.0.1",
5
+ "version": "0.1.0",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Zelijah",
8
8
  "main": "./dist/index.js",
@@ -41,17 +41,22 @@
41
41
  "url": "https://github.com/Majikah/majik-slink/issues"
42
42
  },
43
43
  "scripts": {
44
- "test": "echo \"Error: no test specified\" && exit 1",
45
44
  "build": "tsc",
46
- "prepublishOnly": "npm run build"
45
+ "typecheck": "tsc --noEmit",
46
+ "prepublishOnly": "npm run build",
47
+ "package": "npm run build && npm version patch && git push && git push --tags",
48
+ "test": "vitest run",
49
+ "test:watch": "vitest",
50
+ "update": "npm i @majikah/majik-key@latest @majikah/majik-signature@latest"
47
51
  },
48
52
  "dependencies": {
49
- "@majikah/majik-key": "^0.2.3",
50
- "@majikah/majik-signature": "^0.0.15",
53
+ "@majikah/majik-key": "^0.3.4",
54
+ "@majikah/majik-signature": "^0.2.8",
51
55
  "@types/psl": "^1.1.3",
52
56
  "psl": "^1.15.0"
53
57
  },
54
58
  "devDependencies": {
55
- "@types/node": "^25.5.0"
59
+ "@types/node": "^26.1.2",
60
+ "vitest": "^4.1.10"
56
61
  }
57
62
  }