@hraness/suite-accounts 0.9.20
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 +21 -0
- package/README.md +496 -0
- package/compiler-foundation.css +1 -0
- package/dist/auth-client.js +16 -0
- package/dist/auth-proxy.js +757 -0
- package/dist/bearer-verifier.js +1119 -0
- package/dist/browser-session.js +208 -0
- package/dist/client-configuration.js +470 -0
- package/dist/convex-browser-auth-browser.js +923 -0
- package/dist/convex-browser-auth-server.js +846 -0
- package/dist/convex-browser-auth.js +597 -0
- package/dist/entitlements.js +502 -0
- package/dist/identity/functions.js +44 -0
- package/dist/identity/index.js +1716 -0
- package/dist/identity/return-targets.js +33 -0
- package/dist/index.js +1133 -0
- package/dist/oidc-device-code.js +253 -0
- package/dist/oidc-rp.js +2259 -0
- package/dist/oidc-session-policy.js +5 -0
- package/dist/oidc-surface-server.js +2332 -0
- package/dist/profile-form.js +1934 -0
- package/dist/profile.js +17 -0
- package/dist/public-config.js +462 -0
- package/dist/react.js +137 -0
- package/dist/receipt-verifier.js +619 -0
- package/dist/registry.js +327 -0
- package/dist/stylex-manifest.json +1 -0
- package/dist/stylex.css +435 -0
- package/dist/urls.js +391 -0
- package/package.json +263 -0
- package/portfolio-inventory.json +37 -0
- package/src/auth-client.ts +37 -0
- package/src/auth-proxy.ts +473 -0
- package/src/bearer-verifier.ts +478 -0
- package/src/browser-session.ts +260 -0
- package/src/client-configuration.ts +172 -0
- package/src/convex-browser-auth-browser.ts +238 -0
- package/src/convex-browser-auth-server.ts +492 -0
- package/src/convex-browser-auth.ts +208 -0
- package/src/convex-url.ts +74 -0
- package/src/entitlements.ts +317 -0
- package/src/identity/catalog.ts +227 -0
- package/src/identity/consumers.ts +53 -0
- package/src/identity/functions.ts +70 -0
- package/src/identity/identifiers.ts +82 -0
- package/src/identity/index.ts +10 -0
- package/src/identity/links.ts +280 -0
- package/src/identity/membership.ts +156 -0
- package/src/identity/principals.ts +301 -0
- package/src/identity/profiles-v2.ts +330 -0
- package/src/identity/profiles.ts +524 -0
- package/src/identity/return-targets.ts +20 -0
- package/src/identity/usernames.ts +101 -0
- package/src/identity/views.ts +289 -0
- package/src/immutable.ts +25 -0
- package/src/index.ts +7 -0
- package/src/oidc-continuation.ts +59 -0
- package/src/oidc-device-code.ts +401 -0
- package/src/oidc-rp.ts +2217 -0
- package/src/oidc-session-policy.ts +6 -0
- package/src/oidc-surface-server.ts +186 -0
- package/src/profile-form.css +1 -0
- package/src/profile-form.stylex.ts +226 -0
- package/src/profile-form.tsx +314 -0
- package/src/profile.ts +33 -0
- package/src/public-config.ts +294 -0
- package/src/public-profile-form-result.ts +82 -0
- package/src/public-profile-form.tsx +319 -0
- package/src/public-profile-links.ts +100 -0
- package/src/react.tsx +196 -0
- package/src/receipt-verifier.ts +273 -0
- package/src/registry.ts +600 -0
- package/src/urls.ts +234 -0
package/src/react.tsx
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ConvexProviderWithAuth,
|
|
5
|
+
ConvexReactClient,
|
|
6
|
+
useConvexAuth,
|
|
7
|
+
} from "convex/react";
|
|
8
|
+
import {
|
|
9
|
+
createContext,
|
|
10
|
+
useCallback,
|
|
11
|
+
useContext,
|
|
12
|
+
useEffect,
|
|
13
|
+
useMemo,
|
|
14
|
+
type ReactNode,
|
|
15
|
+
} from "react";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
createSuiteAccountsAuthClient,
|
|
19
|
+
type SuiteAccountsAuthClient,
|
|
20
|
+
} from "./auth-client.js";
|
|
21
|
+
import type {
|
|
22
|
+
ReadySuiteAccountsPublicConfig,
|
|
23
|
+
SuiteAccountsPublicConfig,
|
|
24
|
+
} from "./public-config.js";
|
|
25
|
+
|
|
26
|
+
export type SuiteAccountsAuthentication =
|
|
27
|
+
| Readonly<{ kind: "loading" }>
|
|
28
|
+
| Readonly<{ kind: "signed_in" }>
|
|
29
|
+
| Readonly<{ kind: "signed_out" }>
|
|
30
|
+
| Readonly<{ kind: "unavailable"; message: string }>;
|
|
31
|
+
|
|
32
|
+
export type SuiteAccountsReactContext = Readonly<{
|
|
33
|
+
authClient: SuiteAccountsAuthClient | null;
|
|
34
|
+
authentication: SuiteAccountsAuthentication;
|
|
35
|
+
client: ConvexReactClient | null;
|
|
36
|
+
config: SuiteAccountsPublicConfig;
|
|
37
|
+
}>;
|
|
38
|
+
|
|
39
|
+
const SuiteAccountsContext =
|
|
40
|
+
createContext<SuiteAccountsReactContext | undefined>(undefined);
|
|
41
|
+
|
|
42
|
+
function unavailableMessage(config: SuiteAccountsPublicConfig): string {
|
|
43
|
+
if (config.kind === "invalid") return config.message;
|
|
44
|
+
if (config.kind === "unavailable") return config.message;
|
|
45
|
+
if (config.kind === "missing") {
|
|
46
|
+
return `Missing suite Accounts configuration: ${config.missing.join(", ")}.`;
|
|
47
|
+
}
|
|
48
|
+
if (config.authMode === "oidc-rp") {
|
|
49
|
+
return "This product uses the suite OAuth relying-party session.";
|
|
50
|
+
}
|
|
51
|
+
return "Suite Accounts are unavailable.";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function SuiteAccountsContextBridge({
|
|
55
|
+
authClient,
|
|
56
|
+
children,
|
|
57
|
+
client,
|
|
58
|
+
config,
|
|
59
|
+
}: Readonly<{
|
|
60
|
+
authClient: SuiteAccountsAuthClient;
|
|
61
|
+
children: ReactNode;
|
|
62
|
+
client: ConvexReactClient;
|
|
63
|
+
config: ReadySuiteAccountsPublicConfig;
|
|
64
|
+
}>) {
|
|
65
|
+
const convexAuth = useConvexAuth();
|
|
66
|
+
const authentication: SuiteAccountsAuthentication = convexAuth.isLoading
|
|
67
|
+
? { kind: "loading" }
|
|
68
|
+
: convexAuth.isAuthenticated
|
|
69
|
+
? { kind: "signed_in" }
|
|
70
|
+
: { kind: "signed_out" };
|
|
71
|
+
const value = useMemo(
|
|
72
|
+
() => ({ authClient, authentication, client, config }),
|
|
73
|
+
[authClient, authentication, client, config],
|
|
74
|
+
);
|
|
75
|
+
return (
|
|
76
|
+
<SuiteAccountsContext.Provider value={value}>
|
|
77
|
+
{children}
|
|
78
|
+
</SuiteAccountsContext.Provider>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ReadySuiteAccountsProvider({
|
|
83
|
+
children,
|
|
84
|
+
config,
|
|
85
|
+
}: Readonly<{
|
|
86
|
+
children: ReactNode;
|
|
87
|
+
config: Extract<
|
|
88
|
+
ReadySuiteAccountsPublicConfig,
|
|
89
|
+
{ authMode: "authority" | "proxy" }
|
|
90
|
+
>;
|
|
91
|
+
}>) {
|
|
92
|
+
const authClient = useMemo(
|
|
93
|
+
() => createSuiteAccountsAuthClient({ basePath: config.authBasePath }),
|
|
94
|
+
[config.authBasePath],
|
|
95
|
+
);
|
|
96
|
+
const client = useMemo(
|
|
97
|
+
() => new ConvexReactClient(config.convexUrl),
|
|
98
|
+
[config.convexUrl],
|
|
99
|
+
);
|
|
100
|
+
const { data: session, isPending } = authClient.useSession();
|
|
101
|
+
const sessionId = session?.session.id;
|
|
102
|
+
const fetchAccessToken = useCallback(async () => {
|
|
103
|
+
try {
|
|
104
|
+
const result = await authClient.convex.token({
|
|
105
|
+
fetchOptions: { throw: false },
|
|
106
|
+
});
|
|
107
|
+
return result.data?.token ?? null;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}, [authClient]);
|
|
112
|
+
const useSuiteAuth = useCallback(
|
|
113
|
+
() => ({
|
|
114
|
+
fetchAccessToken,
|
|
115
|
+
isAuthenticated: sessionId !== undefined,
|
|
116
|
+
isLoading: isPending,
|
|
117
|
+
}),
|
|
118
|
+
[fetchAccessToken, isPending, sessionId],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
return () => {
|
|
123
|
+
void client.close();
|
|
124
|
+
};
|
|
125
|
+
}, [client]);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<ConvexProviderWithAuth client={client} useAuth={useSuiteAuth}>
|
|
129
|
+
<SuiteAccountsContextBridge
|
|
130
|
+
authClient={authClient}
|
|
131
|
+
client={client}
|
|
132
|
+
config={config}
|
|
133
|
+
>
|
|
134
|
+
{children}
|
|
135
|
+
</SuiteAccountsContextBridge>
|
|
136
|
+
</ConvexProviderWithAuth>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Mount this provider only around the suite-owned route or component subtree.
|
|
142
|
+
*
|
|
143
|
+
* Descendant Convex hooks intentionally use the central Accounts deployment.
|
|
144
|
+
* Product-local hooks and providers must remain outside this subtree.
|
|
145
|
+
*/
|
|
146
|
+
export function SuiteAccountsProvider({
|
|
147
|
+
children,
|
|
148
|
+
config,
|
|
149
|
+
}: Readonly<{
|
|
150
|
+
children: ReactNode;
|
|
151
|
+
config: SuiteAccountsPublicConfig;
|
|
152
|
+
}>) {
|
|
153
|
+
if (
|
|
154
|
+
config.kind === "ready"
|
|
155
|
+
&& (config.authMode === "authority" || config.authMode === "proxy")
|
|
156
|
+
) {
|
|
157
|
+
return (
|
|
158
|
+
<ReadySuiteAccountsProvider config={config}>
|
|
159
|
+
{children}
|
|
160
|
+
</ReadySuiteAccountsProvider>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
const value: SuiteAccountsReactContext = {
|
|
164
|
+
authClient: null,
|
|
165
|
+
authentication: {
|
|
166
|
+
kind: "unavailable",
|
|
167
|
+
message: unavailableMessage(config),
|
|
168
|
+
},
|
|
169
|
+
client: null,
|
|
170
|
+
config,
|
|
171
|
+
};
|
|
172
|
+
return (
|
|
173
|
+
<SuiteAccountsContext.Provider value={value}>
|
|
174
|
+
{children}
|
|
175
|
+
</SuiteAccountsContext.Provider>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function useSuiteAccounts(): SuiteAccountsReactContext {
|
|
180
|
+
const value = useContext(SuiteAccountsContext);
|
|
181
|
+
if (value === undefined) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
"useSuiteAccounts must be used inside SuiteAccountsProvider.",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function useSuiteAccountsClient(): ConvexReactClient | null {
|
|
190
|
+
return useSuiteAccounts().client;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function useSuiteAccountsAuthentication():
|
|
194
|
+
SuiteAccountsAuthentication {
|
|
195
|
+
return useSuiteAccounts().authentication;
|
|
196
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import {
|
|
2
|
+
productLinkProofMessage,
|
|
3
|
+
parseSuiteLinkProduct,
|
|
4
|
+
suiteEntitlementReceiptMessage,
|
|
5
|
+
suiteLinkReceiptMessage,
|
|
6
|
+
validateProductLinkProof,
|
|
7
|
+
validateSuiteEntitlementReceipt,
|
|
8
|
+
validateSuiteLinkReceipt,
|
|
9
|
+
type ProductLinkProof,
|
|
10
|
+
type SignedSuiteLinkProduct,
|
|
11
|
+
type SuiteEntitlementReceipt,
|
|
12
|
+
type SuiteLinkProduct,
|
|
13
|
+
type SuiteLinkReceipt,
|
|
14
|
+
} from "./identity/links.js";
|
|
15
|
+
import {
|
|
16
|
+
isSuiteIssuableEnvironment,
|
|
17
|
+
parseSuiteEnvironment,
|
|
18
|
+
type SuiteEnvironment,
|
|
19
|
+
} from "./identity/principals.js";
|
|
20
|
+
import { deepFreeze } from "./immutable.js";
|
|
21
|
+
|
|
22
|
+
export type SuiteReceiptKey = Readonly<{
|
|
23
|
+
environment: SuiteEnvironment;
|
|
24
|
+
keyVersion: string;
|
|
25
|
+
product: SuiteLinkProduct;
|
|
26
|
+
secret: string;
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
export type SuiteReceiptKeyring = Readonly<{
|
|
30
|
+
keys: readonly SuiteReceiptKey[];
|
|
31
|
+
version: 1;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
export type SuiteReceiptConfiguration = Readonly<{
|
|
35
|
+
key: SuiteReceiptKey;
|
|
36
|
+
keyring: SuiteReceiptKeyring;
|
|
37
|
+
}>;
|
|
38
|
+
|
|
39
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
40
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function parseSuiteReceiptKeyring(
|
|
44
|
+
value: unknown,
|
|
45
|
+
): SuiteReceiptKeyring | null {
|
|
46
|
+
let decoded: unknown = value;
|
|
47
|
+
if (typeof value === "string") {
|
|
48
|
+
if (value.length > 32_768) return null;
|
|
49
|
+
try {
|
|
50
|
+
decoded = JSON.parse(value) as unknown;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (
|
|
56
|
+
!isRecord(decoded)
|
|
57
|
+
|| decoded["version"] !== 1
|
|
58
|
+
|| !Array.isArray(decoded["keys"])
|
|
59
|
+
|| decoded["keys"].length < 1
|
|
60
|
+
|| decoded["keys"].length > 20
|
|
61
|
+
) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const keys: SuiteReceiptKey[] = [];
|
|
65
|
+
const identities = new Set<string>();
|
|
66
|
+
for (const rawKey of decoded["keys"]) {
|
|
67
|
+
if (!isRecord(rawKey)) return null;
|
|
68
|
+
const { environment, keyVersion, product, secret } = rawKey;
|
|
69
|
+
const parsedEnvironment = parseSuiteEnvironment(environment);
|
|
70
|
+
const parsedProduct = parseSuiteLinkProduct(product);
|
|
71
|
+
if (
|
|
72
|
+
!parsedEnvironment.ok
|
|
73
|
+
|| !parsedProduct.ok
|
|
74
|
+
|| !/^[a-z0-9][a-z0-9._-]{0,31}$/u.test(String(keyVersion))
|
|
75
|
+
|| typeof secret !== "string"
|
|
76
|
+
|| new TextEncoder().encode(secret).byteLength < 32
|
|
77
|
+
|| secret.length > 1_024
|
|
78
|
+
) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const identity = `${parsedProduct.value}:${parsedEnvironment.value}:${String(
|
|
82
|
+
keyVersion,
|
|
83
|
+
)}`;
|
|
84
|
+
if (identities.has(identity)) return null;
|
|
85
|
+
identities.add(identity);
|
|
86
|
+
keys.push({
|
|
87
|
+
environment: parsedEnvironment.value,
|
|
88
|
+
keyVersion: String(keyVersion),
|
|
89
|
+
product: parsedProduct.value,
|
|
90
|
+
secret,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return deepFreeze({ keys, version: 1 });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Select the one active issuance key while retaining prior versions for
|
|
98
|
+
* bounded verification during rotation.
|
|
99
|
+
*
|
|
100
|
+
* Staging keys remain parseable only to verify bounded historical evidence.
|
|
101
|
+
* They can never become the active key for new proof or receipt issuance.
|
|
102
|
+
*/
|
|
103
|
+
export function selectSuiteReceiptConfiguration(
|
|
104
|
+
value: unknown,
|
|
105
|
+
product: SignedSuiteLinkProduct,
|
|
106
|
+
activeKeyVersion: unknown,
|
|
107
|
+
): SuiteReceiptConfiguration | null {
|
|
108
|
+
const canonicalProduct = parseSuiteLinkProduct(product);
|
|
109
|
+
if (
|
|
110
|
+
!canonicalProduct.ok
|
|
111
|
+
|| typeof activeKeyVersion !== "string"
|
|
112
|
+
|| !/^[a-z0-9][a-z0-9._-]{0,31}$/u.test(activeKeyVersion)
|
|
113
|
+
) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const keyring = parseSuiteReceiptKeyring(value);
|
|
117
|
+
if (keyring === null) return null;
|
|
118
|
+
const active = keyring.keys.filter(key =>
|
|
119
|
+
key.product === canonicalProduct.value
|
|
120
|
+
&& key.keyVersion === activeKeyVersion
|
|
121
|
+
&& isSuiteIssuableEnvironment(key.environment)
|
|
122
|
+
);
|
|
123
|
+
if (active.length !== 1) return null;
|
|
124
|
+
const key = active[0]!;
|
|
125
|
+
const verificationKeys = keyring.keys.filter(candidate =>
|
|
126
|
+
candidate.product === canonicalProduct.value
|
|
127
|
+
&& candidate.environment === key.environment
|
|
128
|
+
);
|
|
129
|
+
return deepFreeze({
|
|
130
|
+
key,
|
|
131
|
+
keyring: { keys: verificationKeys, version: 1 },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function keyFor(
|
|
136
|
+
keyring: SuiteReceiptKeyring,
|
|
137
|
+
product: SignedSuiteLinkProduct,
|
|
138
|
+
environment: SuiteEnvironment,
|
|
139
|
+
keyVersion: string,
|
|
140
|
+
): SuiteReceiptKey | null {
|
|
141
|
+
const canonicalProduct = parseSuiteLinkProduct(product);
|
|
142
|
+
if (!canonicalProduct.ok) return null;
|
|
143
|
+
return keyring.keys.find(key =>
|
|
144
|
+
key.product === canonicalProduct.value
|
|
145
|
+
&& key.environment === environment
|
|
146
|
+
&& key.keyVersion === keyVersion
|
|
147
|
+
) ?? null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function decodeSignature(value: string): Uint8Array<ArrayBuffer> | null {
|
|
151
|
+
if (!/^[A-Za-z0-9_-]{43}$/u.test(value)) return null;
|
|
152
|
+
try {
|
|
153
|
+
const binary = atob(
|
|
154
|
+
value.replaceAll("-", "+").replaceAll("_", "/") + "=",
|
|
155
|
+
);
|
|
156
|
+
const result = new Uint8Array(new ArrayBuffer(binary.length));
|
|
157
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
158
|
+
result[index] = binary.charCodeAt(index);
|
|
159
|
+
}
|
|
160
|
+
return result.byteLength === 32 ? result : null;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function hmac(
|
|
167
|
+
secret: string,
|
|
168
|
+
message: string,
|
|
169
|
+
): Promise<ArrayBuffer> {
|
|
170
|
+
const key = await crypto.subtle.importKey(
|
|
171
|
+
"raw",
|
|
172
|
+
new TextEncoder().encode(secret),
|
|
173
|
+
{ hash: "SHA-256", name: "HMAC" },
|
|
174
|
+
false,
|
|
175
|
+
["sign"],
|
|
176
|
+
);
|
|
177
|
+
return await crypto.subtle.sign(
|
|
178
|
+
"HMAC",
|
|
179
|
+
key,
|
|
180
|
+
new TextEncoder().encode(message),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function verifySignature(
|
|
185
|
+
secret: string,
|
|
186
|
+
message: string,
|
|
187
|
+
signature: string,
|
|
188
|
+
): Promise<boolean> {
|
|
189
|
+
const received = decodeSignature(signature);
|
|
190
|
+
if (received === null) return false;
|
|
191
|
+
const key = await crypto.subtle.importKey(
|
|
192
|
+
"raw",
|
|
193
|
+
new TextEncoder().encode(secret),
|
|
194
|
+
{ hash: "SHA-256", name: "HMAC" },
|
|
195
|
+
false,
|
|
196
|
+
["verify"],
|
|
197
|
+
);
|
|
198
|
+
return await crypto.subtle.verify(
|
|
199
|
+
"HMAC",
|
|
200
|
+
key,
|
|
201
|
+
received,
|
|
202
|
+
new TextEncoder().encode(message),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function encodeBase64Url(bytes: Uint8Array): string {
|
|
207
|
+
let binary = "";
|
|
208
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
209
|
+
return btoa(binary)
|
|
210
|
+
.replaceAll("+", "-")
|
|
211
|
+
.replaceAll("/", "_")
|
|
212
|
+
.replace(/=+$/u, "");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function signSuiteProductLinkProof(
|
|
216
|
+
proof: ProductLinkProof,
|
|
217
|
+
keyring: SuiteReceiptKeyring,
|
|
218
|
+
nowMs: number,
|
|
219
|
+
): Promise<string | null> {
|
|
220
|
+
if (
|
|
221
|
+
validateProductLinkProof(proof, nowMs) !== null
|
|
222
|
+
|| !isSuiteIssuableEnvironment(proof.environment)
|
|
223
|
+
) return null;
|
|
224
|
+
const key = keyFor(
|
|
225
|
+
keyring,
|
|
226
|
+
proof.product,
|
|
227
|
+
proof.environment,
|
|
228
|
+
proof.keyVersion,
|
|
229
|
+
);
|
|
230
|
+
return key === null
|
|
231
|
+
? null
|
|
232
|
+
: encodeBase64Url(
|
|
233
|
+
new Uint8Array(await hmac(key.secret, productLinkProofMessage(proof))),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function verifySuiteLinkReceiptSignature(
|
|
238
|
+
receipt: SuiteLinkReceipt,
|
|
239
|
+
keyring: SuiteReceiptKeyring,
|
|
240
|
+
nowMs: number,
|
|
241
|
+
): Promise<boolean> {
|
|
242
|
+
if (validateSuiteLinkReceipt(receipt, nowMs) !== null) return false;
|
|
243
|
+
const key = keyFor(
|
|
244
|
+
keyring,
|
|
245
|
+
receipt.product,
|
|
246
|
+
receipt.environment,
|
|
247
|
+
receipt.keyVersion,
|
|
248
|
+
);
|
|
249
|
+
return key !== null && await verifySignature(
|
|
250
|
+
key.secret,
|
|
251
|
+
suiteLinkReceiptMessage(receipt),
|
|
252
|
+
receipt.signature,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function verifySuiteEntitlementReceiptSignature(
|
|
257
|
+
receipt: SuiteEntitlementReceipt,
|
|
258
|
+
keyring: SuiteReceiptKeyring,
|
|
259
|
+
nowMs: number,
|
|
260
|
+
): Promise<boolean> {
|
|
261
|
+
if (validateSuiteEntitlementReceipt(receipt, nowMs) !== null) return false;
|
|
262
|
+
const key = keyFor(
|
|
263
|
+
keyring,
|
|
264
|
+
receipt.product,
|
|
265
|
+
receipt.environment,
|
|
266
|
+
receipt.keyVersion,
|
|
267
|
+
);
|
|
268
|
+
return key !== null && await verifySignature(
|
|
269
|
+
key.secret,
|
|
270
|
+
suiteEntitlementReceiptMessage(receipt),
|
|
271
|
+
receipt.signature,
|
|
272
|
+
);
|
|
273
|
+
}
|