@southwind-ai/license 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.
@@ -0,0 +1,199 @@
1
+ import type {
2
+ LegacyPrototypeLicenseClaims,
3
+ LegacyPrototypeLicenseVerificationResult,
4
+ LegacyPrototypeSignedLicense,
5
+ LicenseVerificationResult,
6
+ SignedLicense,
7
+ } from "@southwind-ai/shared";
8
+ import { createLicenseSigningPayload } from "./payload.js";
9
+
10
+ export type LicensePublicKeyMap = Record<string, string>;
11
+
12
+ export type LicensePublicKeyResolver = (
13
+ publicKeyId: string,
14
+ ) => Promise<string | undefined> | string | undefined;
15
+
16
+ export type LicensePublicKeySource =
17
+ | LicensePublicKeyMap
18
+ | LicensePublicKeyResolver;
19
+
20
+ export async function verifySignedLicense(
21
+ license: SignedLicense,
22
+ publicKeySource: LicensePublicKeySource,
23
+ ): Promise<LicenseVerificationResult> {
24
+ if (license.formatVersion !== 1) {
25
+ return { valid: false, failures: ["unsupported-format"] };
26
+ }
27
+ if (license.algorithm !== "ED25519") {
28
+ return { valid: false, failures: ["invalid-format"] };
29
+ }
30
+ const publicKeyPem = await resolvePublicKey(
31
+ publicKeySource,
32
+ license.publicKeyId,
33
+ );
34
+ if (!publicKeyPem) {
35
+ return { valid: false, failures: ["unknown-public-key"] };
36
+ }
37
+ const valid = await verifyLicenseSignature(license, publicKeyPem);
38
+ return valid
39
+ ? { valid: true, claims: license.claims, failures: [] }
40
+ : { valid: false, failures: ["invalid-signature"] };
41
+ }
42
+
43
+ export async function verifyLicenseSignature(
44
+ license: SignedLicense,
45
+ publicKeyPem: string,
46
+ ): Promise<boolean> {
47
+ try {
48
+ return verifyLicensePayloadSignature(
49
+ createLicenseSigningPayload({
50
+ formatVersion: 1,
51
+ algorithm: license.algorithm,
52
+ publicKeyId: license.publicKeyId,
53
+ claims: license.claims,
54
+ }),
55
+ license.signature,
56
+ publicKeyPem,
57
+ );
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /** Explicit read-only compatibility path. It must never be used to issue. */
64
+ export const legacyPrototypeLicensePayloadReader = {
65
+ parse(payload: string): LegacyPrototypeLicenseClaims {
66
+ return JSON.parse(payload) as LegacyPrototypeLicenseClaims;
67
+ },
68
+ serializeForVerification(claims: LegacyPrototypeLicenseClaims): string {
69
+ return JSON.stringify(claims);
70
+ },
71
+ } as const;
72
+
73
+ export async function verifyLegacyPrototypeSignedLicense(
74
+ license: LegacyPrototypeSignedLicense,
75
+ publicKeySource: LicensePublicKeySource,
76
+ ): Promise<LegacyPrototypeLicenseVerificationResult> {
77
+ if ((license.algorithm as string) !== "ED25519") {
78
+ return { valid: false, failures: ["invalid-signature"] };
79
+ }
80
+ const publicKeyPem = await resolvePublicKey(
81
+ publicKeySource,
82
+ license.publicKeyId,
83
+ );
84
+ if (!publicKeyPem) {
85
+ return { valid: false, failures: ["invalid-signature"] };
86
+ }
87
+ const valid = await verifyLicensePayloadSignature(
88
+ legacyPrototypeLicensePayloadReader.serializeForVerification(
89
+ license.claims,
90
+ ),
91
+ license.signature,
92
+ publicKeyPem,
93
+ );
94
+ return valid
95
+ ? { valid: true, claims: license.claims, failures: [] }
96
+ : { valid: false, failures: ["invalid-signature"] };
97
+ }
98
+
99
+ export async function verifyLicensePayloadSignature(
100
+ payload: string,
101
+ signatureBase64: string,
102
+ publicKeyPem: string,
103
+ ): Promise<boolean> {
104
+ let payloadBytes: Uint8Array;
105
+ let signature: Uint8Array;
106
+ try {
107
+ payloadBytes = new TextEncoder().encode(payload);
108
+ signature = base64ToBytes(signatureBase64);
109
+ } catch {
110
+ return false;
111
+ }
112
+ const webCryptoResult = await verifyWithWebCrypto(
113
+ publicKeyPem,
114
+ signature,
115
+ payloadBytes,
116
+ );
117
+ if (webCryptoResult !== undefined) return webCryptoResult;
118
+ return verifyWithNodeCrypto(publicKeyPem, signature, payloadBytes);
119
+ }
120
+
121
+ async function resolvePublicKey(
122
+ source: LicensePublicKeySource,
123
+ publicKeyId: string,
124
+ ): Promise<string | undefined> {
125
+ return typeof source === "function"
126
+ ? source(publicKeyId)
127
+ : source[publicKeyId];
128
+ }
129
+
130
+ async function verifyWithWebCrypto(
131
+ publicKeyPem: string,
132
+ signature: Uint8Array,
133
+ payload: Uint8Array,
134
+ ): Promise<boolean | undefined> {
135
+ const subtle = globalThis.crypto?.subtle;
136
+ if (!subtle) return undefined;
137
+ try {
138
+ const algorithm: AlgorithmIdentifier = { name: "Ed25519" };
139
+ const publicKey = await subtle.importKey(
140
+ "spki",
141
+ toArrayBuffer(pemToDer(publicKeyPem)),
142
+ algorithm,
143
+ false,
144
+ ["verify"],
145
+ );
146
+ return subtle.verify(
147
+ algorithm,
148
+ publicKey,
149
+ toArrayBuffer(signature),
150
+ toArrayBuffer(payload),
151
+ );
152
+ } catch {
153
+ return undefined;
154
+ }
155
+ }
156
+
157
+ async function verifyWithNodeCrypto(
158
+ publicKeyPem: string,
159
+ signature: Uint8Array,
160
+ payload: Uint8Array,
161
+ ): Promise<boolean> {
162
+ try {
163
+ const { createPublicKey, verify } = await import("node:crypto");
164
+ return verify(null, payload, createPublicKey(publicKeyPem), signature);
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+
170
+ function pemToDer(pem: string): Uint8Array {
171
+ return base64ToBytes(
172
+ pem
173
+ .replace(/-----BEGIN PUBLIC KEY-----/g, "")
174
+ .replace(/-----END PUBLIC KEY-----/g, "")
175
+ .replace(/\s+/g, ""),
176
+ );
177
+ }
178
+
179
+ function base64ToBytes(value: string): Uint8Array {
180
+ if (
181
+ !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
182
+ value,
183
+ )
184
+ ) {
185
+ throw new Error("无效 Base64");
186
+ }
187
+ if (typeof atob === "function") {
188
+ const binary = atob(value);
189
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
190
+ }
191
+ return new Uint8Array(Buffer.from(value, "base64"));
192
+ }
193
+
194
+ function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
195
+ return bytes.buffer.slice(
196
+ bytes.byteOffset,
197
+ bytes.byteOffset + bytes.byteLength,
198
+ ) as ArrayBuffer;
199
+ }