@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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { makeFunctionReference } from "convex/server";
|
|
2
|
+
import { deepFreeze } from "../immutable.js";
|
|
3
|
+
|
|
4
|
+
import type { SuitePlanId } from "./catalog.js";
|
|
5
|
+
import type { SuiteInvoiceRef } from "./identifiers.js";
|
|
6
|
+
import type { SuiteReturnTarget } from "./return-targets.js";
|
|
7
|
+
import type { SuiteAccountView } from "./views.js";
|
|
8
|
+
|
|
9
|
+
export type { SuiteReturnTarget } from "./return-targets.js";
|
|
10
|
+
|
|
11
|
+
export type StartSuiteCheckoutArgs = {
|
|
12
|
+
readonly plan: SuitePlanId;
|
|
13
|
+
readonly returnTarget: SuiteReturnTarget;
|
|
14
|
+
};
|
|
15
|
+
export type StartSuiteCheckoutResult =
|
|
16
|
+
| { readonly kind: "account" }
|
|
17
|
+
| { readonly kind: "redirect"; readonly url: string };
|
|
18
|
+
|
|
19
|
+
export type CreateSuitePortalSessionArgs = {
|
|
20
|
+
readonly returnTarget: SuiteReturnTarget;
|
|
21
|
+
};
|
|
22
|
+
export type CreateSuitePortalSessionResult = {
|
|
23
|
+
readonly kind: "redirect";
|
|
24
|
+
readonly url: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type OpenSuiteInvoiceArgs = {
|
|
28
|
+
readonly invoiceRef: SuiteInvoiceRef;
|
|
29
|
+
};
|
|
30
|
+
export type OpenSuiteInvoiceResult = {
|
|
31
|
+
readonly kind: "redirect";
|
|
32
|
+
readonly url: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Query the currently authenticated suite account and its safe projections. */
|
|
36
|
+
export const accountsCurrent = makeFunctionReference<
|
|
37
|
+
"query",
|
|
38
|
+
Record<never, never>,
|
|
39
|
+
SuiteAccountView | null
|
|
40
|
+
>("accounts:current");
|
|
41
|
+
|
|
42
|
+
/** Start hosted Checkout for one exact suite plan. Return URLs are server-owned. */
|
|
43
|
+
export const billingStartCheckout = makeFunctionReference<
|
|
44
|
+
"action",
|
|
45
|
+
StartSuiteCheckoutArgs,
|
|
46
|
+
StartSuiteCheckoutResult
|
|
47
|
+
>("billing:startCheckout");
|
|
48
|
+
|
|
49
|
+
/** Open the current account's hosted Customer Portal. Its return URL is server-owned. */
|
|
50
|
+
export const billingCreatePortalSession = makeFunctionReference<
|
|
51
|
+
"action",
|
|
52
|
+
CreateSuitePortalSessionArgs,
|
|
53
|
+
CreateSuitePortalSessionResult
|
|
54
|
+
>("billing:createPortalSession");
|
|
55
|
+
|
|
56
|
+
/** Resolve one owned invoice reference to a validated hosted invoice URL. */
|
|
57
|
+
export const billingOpenInvoice = makeFunctionReference<
|
|
58
|
+
"action",
|
|
59
|
+
OpenSuiteInvoiceArgs,
|
|
60
|
+
OpenSuiteInvoiceResult
|
|
61
|
+
>("billing:openInvoice");
|
|
62
|
+
|
|
63
|
+
export const suiteAccountsApi = deepFreeze({
|
|
64
|
+
accounts: { current: accountsCurrent },
|
|
65
|
+
billing: {
|
|
66
|
+
createPortalSession: billingCreatePortalSession,
|
|
67
|
+
openInvoice: billingOpenInvoice,
|
|
68
|
+
startCheckout: billingStartCheckout,
|
|
69
|
+
},
|
|
70
|
+
} as const);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { err, ok, type Result } from "@hraness/result";
|
|
2
|
+
|
|
3
|
+
declare const suiteAccountIdBrand: unique symbol;
|
|
4
|
+
declare const suiteInvoiceRefBrand: unique symbol;
|
|
5
|
+
|
|
6
|
+
/** An opaque, public account identifier owned by the suite accounts service. */
|
|
7
|
+
export type SuiteAccountId = string & {
|
|
8
|
+
readonly [suiteAccountIdBrand]: "SuiteAccountId";
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** An opaque, unguessable invoice reference owned by the suite accounts service. */
|
|
12
|
+
export type SuiteInvoiceRef = string & {
|
|
13
|
+
readonly [suiteInvoiceRefBrand]: "SuiteInvoiceRef";
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type SuiteAccountIdIssue = "invalid-suite-account-id";
|
|
17
|
+
export type SuiteInvoiceRefIssue = "invalid-suite-invoice-ref";
|
|
18
|
+
|
|
19
|
+
const suiteAccountIdPattern = /^acct_[0-9a-f]{32}$/u;
|
|
20
|
+
const suiteInvoiceRefPattern = /^invref_[0-9a-f]{32}$/u;
|
|
21
|
+
const uuidPattern =
|
|
22
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
23
|
+
|
|
24
|
+
/** Parse a foreign value into the only supported suite account ID format. */
|
|
25
|
+
export function parseSuiteAccountId(
|
|
26
|
+
value: unknown,
|
|
27
|
+
): Result<SuiteAccountId, SuiteAccountIdIssue> {
|
|
28
|
+
return typeof value === "string" && suiteAccountIdPattern.test(value)
|
|
29
|
+
? ok(value as SuiteAccountId)
|
|
30
|
+
: err("invalid-suite-account-id");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Parse a foreign value without exposing a provider invoice identifier. */
|
|
34
|
+
export function parseSuiteInvoiceRef(
|
|
35
|
+
value: unknown,
|
|
36
|
+
): Result<SuiteInvoiceRef, SuiteInvoiceRefIssue> {
|
|
37
|
+
return typeof value === "string" && suiteInvoiceRefPattern.test(value)
|
|
38
|
+
? ok(value as SuiteInvoiceRef)
|
|
39
|
+
: err("invalid-suite-invoice-ref");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate a suite account ID from a cryptographically random UUID.
|
|
44
|
+
*
|
|
45
|
+
* The injected seam lets deterministic tests prove formatting. Production callers
|
|
46
|
+
* should keep the default Web Crypto implementation.
|
|
47
|
+
*/
|
|
48
|
+
export function generateSuiteAccountId(
|
|
49
|
+
randomUuid: () => string = () => crypto.randomUUID(),
|
|
50
|
+
): SuiteAccountId {
|
|
51
|
+
const uuid = randomUuid();
|
|
52
|
+
if (!uuidPattern.test(uuid)) {
|
|
53
|
+
throw new TypeError("The suite account ID source did not return a UUID.");
|
|
54
|
+
}
|
|
55
|
+
const candidate = `acct_${uuid.replaceAll("-", "").toLowerCase()}`;
|
|
56
|
+
const parsed = parseSuiteAccountId(candidate);
|
|
57
|
+
if (!parsed.ok) {
|
|
58
|
+
throw new TypeError("The suite account ID source produced an invalid value.");
|
|
59
|
+
}
|
|
60
|
+
return parsed.value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generate a provider-neutral invoice reference from cryptographic randomness.
|
|
65
|
+
*
|
|
66
|
+
* The reference is safe to project to a browser only after the accounts service
|
|
67
|
+
* associates it with an invoice and resolves it under the authenticated account.
|
|
68
|
+
*/
|
|
69
|
+
export function generateSuiteInvoiceRef(
|
|
70
|
+
randomUuid: () => string = () => crypto.randomUUID(),
|
|
71
|
+
): SuiteInvoiceRef {
|
|
72
|
+
const uuid = randomUuid();
|
|
73
|
+
if (!uuidPattern.test(uuid)) {
|
|
74
|
+
throw new TypeError("The suite invoice reference source did not return a UUID.");
|
|
75
|
+
}
|
|
76
|
+
const candidate = `invref_${uuid.replaceAll("-", "").toLowerCase()}`;
|
|
77
|
+
const parsed = parseSuiteInvoiceRef(candidate);
|
|
78
|
+
if (!parsed.ok) {
|
|
79
|
+
throw new TypeError("The suite invoice reference source produced an invalid value.");
|
|
80
|
+
}
|
|
81
|
+
return parsed.value;
|
|
82
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./catalog.js";
|
|
2
|
+
export * from "./consumers.js";
|
|
3
|
+
export * from "./identifiers.js";
|
|
4
|
+
export * from "./links.js";
|
|
5
|
+
export * from "./membership.js";
|
|
6
|
+
export * from "./principals.js";
|
|
7
|
+
export * from "./profiles.js";
|
|
8
|
+
export * from "./profiles-v2.js";
|
|
9
|
+
export * from "./usernames.js";
|
|
10
|
+
export * from "./views.js";
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { ok, type Result } from "@hraness/result";
|
|
2
|
+
import { deepFreeze } from "../immutable.js";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
grantedSuiteFeatureSets,
|
|
6
|
+
parseSuiteCatalogRevision,
|
|
7
|
+
parseSuiteFeatureId,
|
|
8
|
+
SUITE_FEATURE_IDS,
|
|
9
|
+
type SuiteCatalogRevision,
|
|
10
|
+
type SuiteFeatureId,
|
|
11
|
+
} from "./catalog.js";
|
|
12
|
+
import { parseSuiteAccountId, type SuiteAccountId } from "./identifiers.js";
|
|
13
|
+
import {
|
|
14
|
+
parseIdentitySubject,
|
|
15
|
+
parseSuiteEnvironment,
|
|
16
|
+
parseSuiteProduct,
|
|
17
|
+
type LegacySuiteProductId,
|
|
18
|
+
type SuiteEnvironment,
|
|
19
|
+
type SuiteProduct,
|
|
20
|
+
} from "./principals.js";
|
|
21
|
+
|
|
22
|
+
export const IDENTITY_LINK_PROOF_VERSION =
|
|
23
|
+
"suite-product-link-proof-v1" as const;
|
|
24
|
+
export const IDENTITY_LINK_RECEIPT_VERSION =
|
|
25
|
+
"suite-link-receipt-v1" as const;
|
|
26
|
+
export const SUITE_ENTITLEMENTS_CLAIM_VERSION =
|
|
27
|
+
"suite-entitlements-v1" as const;
|
|
28
|
+
export const SUITE_ENTITLEMENT_RECEIPT_VERSION =
|
|
29
|
+
"suite-entitlement-receipt-v1" as const;
|
|
30
|
+
export const IDENTITY_LINK_MAX_TTL_MS = 5 * 60_000;
|
|
31
|
+
export const SUITE_ENTITLEMENT_RECEIPT_MAX_TTL_MS = 5 * 60_000;
|
|
32
|
+
export const IDENTITY_LINK_CLOCK_SKEW_MS = 30_000;
|
|
33
|
+
|
|
34
|
+
export const SUITE_LINK_PRODUCTS = deepFreeze([
|
|
35
|
+
"soundfish",
|
|
36
|
+
"hra",
|
|
37
|
+
"peopleblade",
|
|
38
|
+
"ghostget",
|
|
39
|
+
// Compatibility-only readers for already-issued publication receipts and links.
|
|
40
|
+
"crclte",
|
|
41
|
+
"pub",
|
|
42
|
+
] as const satisfies readonly SuiteProduct[]);
|
|
43
|
+
export const LEGACY_SUITE_LINK_PRODUCTS = deepFreeze([
|
|
44
|
+
"oprte",
|
|
45
|
+
"kitchen",
|
|
46
|
+
] as const satisfies readonly LegacySuiteProductId[]);
|
|
47
|
+
|
|
48
|
+
export type SuiteLinkProduct = (typeof SUITE_LINK_PRODUCTS)[number];
|
|
49
|
+
export type LegacySuiteLinkProduct =
|
|
50
|
+
(typeof LEGACY_SUITE_LINK_PRODUCTS)[number];
|
|
51
|
+
export type SignedSuiteLinkProduct =
|
|
52
|
+
| LegacySuiteLinkProduct
|
|
53
|
+
| SuiteLinkProduct;
|
|
54
|
+
|
|
55
|
+
export function parseSuiteLinkProduct(
|
|
56
|
+
value: unknown,
|
|
57
|
+
): Result<SuiteLinkProduct, "invalid-product"> {
|
|
58
|
+
const parsed = parseSuiteProduct(value);
|
|
59
|
+
if (!parsed.ok) return parsed;
|
|
60
|
+
switch (parsed.value) {
|
|
61
|
+
case "soundfish":
|
|
62
|
+
case "hra":
|
|
63
|
+
case "peopleblade":
|
|
64
|
+
case "ghostget":
|
|
65
|
+
case "crclte":
|
|
66
|
+
case "pub":
|
|
67
|
+
return ok(parsed.value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type ProductLinkProof = Readonly<{
|
|
72
|
+
challengeId: string;
|
|
73
|
+
environment: SuiteEnvironment;
|
|
74
|
+
expiresAtMs: number;
|
|
75
|
+
issuedAtMs: number;
|
|
76
|
+
keyVersion: string;
|
|
77
|
+
localSubject: string;
|
|
78
|
+
product: SignedSuiteLinkProduct;
|
|
79
|
+
}>;
|
|
80
|
+
|
|
81
|
+
export type SuiteLinkReceiptPayload = ProductLinkProof & Readonly<{
|
|
82
|
+
suiteAccountId: SuiteAccountId | string;
|
|
83
|
+
}>;
|
|
84
|
+
|
|
85
|
+
export type SuiteLinkReceipt = SuiteLinkReceiptPayload & Readonly<{
|
|
86
|
+
signature: string;
|
|
87
|
+
version: typeof IDENTITY_LINK_RECEIPT_VERSION;
|
|
88
|
+
}>;
|
|
89
|
+
|
|
90
|
+
export type SuiteEntitlementsClaim = Readonly<{
|
|
91
|
+
catalogRevision: SuiteCatalogRevision;
|
|
92
|
+
expiresAtMs: number;
|
|
93
|
+
features: readonly SuiteFeatureId[];
|
|
94
|
+
observedAtMs: number;
|
|
95
|
+
projectionRevision: number;
|
|
96
|
+
version: typeof SUITE_ENTITLEMENTS_CLAIM_VERSION;
|
|
97
|
+
}>;
|
|
98
|
+
|
|
99
|
+
export type SuiteEntitlementReceiptPayload = Readonly<{
|
|
100
|
+
entitlements: SuiteEntitlementsClaim;
|
|
101
|
+
environment: SuiteEnvironment;
|
|
102
|
+
expiresAtMs: number;
|
|
103
|
+
issuedAtMs: number;
|
|
104
|
+
keyVersion: string;
|
|
105
|
+
product: SignedSuiteLinkProduct;
|
|
106
|
+
suiteAccountId: SuiteAccountId | string;
|
|
107
|
+
}>;
|
|
108
|
+
|
|
109
|
+
export type SuiteEntitlementReceipt =
|
|
110
|
+
SuiteEntitlementReceiptPayload & Readonly<{
|
|
111
|
+
signature: string;
|
|
112
|
+
version: typeof SUITE_ENTITLEMENT_RECEIPT_VERSION;
|
|
113
|
+
}>;
|
|
114
|
+
|
|
115
|
+
export type IdentityLinkInputIssue =
|
|
116
|
+
| "expired"
|
|
117
|
+
| "invalid"
|
|
118
|
+
| "not-yet-valid";
|
|
119
|
+
|
|
120
|
+
function safeInteger(value: number): boolean {
|
|
121
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function validateProductLinkProof(
|
|
125
|
+
input: ProductLinkProof,
|
|
126
|
+
now: number,
|
|
127
|
+
): IdentityLinkInputIssue | null {
|
|
128
|
+
const product = parseSuiteLinkProduct(input.product);
|
|
129
|
+
const environment = parseSuiteEnvironment(input.environment);
|
|
130
|
+
const localSubject = parseIdentitySubject(input.localSubject);
|
|
131
|
+
if (
|
|
132
|
+
!safeInteger(now)
|
|
133
|
+
|| !product.ok
|
|
134
|
+
|| !environment.ok
|
|
135
|
+
|| !localSubject.ok
|
|
136
|
+
|| !/^[A-Za-z0-9_-]{22,128}$/u.test(input.challengeId)
|
|
137
|
+
|| !/^[a-z0-9][a-z0-9._-]{0,31}$/u.test(input.keyVersion)
|
|
138
|
+
|| !safeInteger(input.issuedAtMs)
|
|
139
|
+
|| !safeInteger(input.expiresAtMs)
|
|
140
|
+
|| input.expiresAtMs <= input.issuedAtMs
|
|
141
|
+
|| input.expiresAtMs - input.issuedAtMs > IDENTITY_LINK_MAX_TTL_MS
|
|
142
|
+
) {
|
|
143
|
+
return "invalid";
|
|
144
|
+
}
|
|
145
|
+
if (input.issuedAtMs > now + IDENTITY_LINK_CLOCK_SKEW_MS) {
|
|
146
|
+
return "not-yet-valid";
|
|
147
|
+
}
|
|
148
|
+
return input.expiresAtMs <= now ? "expired" : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function productLinkProofMessage(input: ProductLinkProof): string {
|
|
152
|
+
return JSON.stringify([
|
|
153
|
+
IDENTITY_LINK_PROOF_VERSION,
|
|
154
|
+
input.product,
|
|
155
|
+
input.environment,
|
|
156
|
+
input.localSubject,
|
|
157
|
+
input.challengeId,
|
|
158
|
+
input.issuedAtMs,
|
|
159
|
+
input.expiresAtMs,
|
|
160
|
+
input.keyVersion,
|
|
161
|
+
]);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function suiteLinkReceiptMessage(
|
|
165
|
+
input: SuiteLinkReceiptPayload,
|
|
166
|
+
): string {
|
|
167
|
+
return JSON.stringify([
|
|
168
|
+
IDENTITY_LINK_RECEIPT_VERSION,
|
|
169
|
+
input.product,
|
|
170
|
+
input.environment,
|
|
171
|
+
input.localSubject,
|
|
172
|
+
input.suiteAccountId,
|
|
173
|
+
input.challengeId,
|
|
174
|
+
input.issuedAtMs,
|
|
175
|
+
input.expiresAtMs,
|
|
176
|
+
input.keyVersion,
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function validateSuiteLinkReceipt(
|
|
181
|
+
input: SuiteLinkReceipt,
|
|
182
|
+
now: number,
|
|
183
|
+
): IdentityLinkInputIssue | null {
|
|
184
|
+
const proofIssue = validateProductLinkProof(input, now);
|
|
185
|
+
if (
|
|
186
|
+
proofIssue !== null
|
|
187
|
+
|| input.version !== IDENTITY_LINK_RECEIPT_VERSION
|
|
188
|
+
|| !parseSuiteAccountId(input.suiteAccountId).ok
|
|
189
|
+
|| !/^[A-Za-z0-9_-]{43}$/u.test(input.signature)
|
|
190
|
+
) {
|
|
191
|
+
return proofIssue ?? "invalid";
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function exactGrantedFeatures(
|
|
197
|
+
revision: SuiteCatalogRevision,
|
|
198
|
+
values: readonly SuiteFeatureId[],
|
|
199
|
+
): boolean {
|
|
200
|
+
if (values.length > SUITE_FEATURE_IDS.length) return false;
|
|
201
|
+
const parsed: SuiteFeatureId[] = [];
|
|
202
|
+
for (const value of values) {
|
|
203
|
+
const feature = parseSuiteFeatureId(value);
|
|
204
|
+
if (!feature.ok || parsed.includes(feature.value)) return false;
|
|
205
|
+
parsed.push(feature.value);
|
|
206
|
+
}
|
|
207
|
+
return grantedSuiteFeatureSets(revision).some(set =>
|
|
208
|
+
set.length === parsed.length
|
|
209
|
+
&& set.every((feature, index) => feature === parsed[index])
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function validateSuiteEntitlementsClaim(
|
|
214
|
+
input: SuiteEntitlementsClaim,
|
|
215
|
+
): boolean {
|
|
216
|
+
const catalogRevision = parseSuiteCatalogRevision(input.catalogRevision);
|
|
217
|
+
return (
|
|
218
|
+
input.version === SUITE_ENTITLEMENTS_CLAIM_VERSION
|
|
219
|
+
&& catalogRevision.ok
|
|
220
|
+
&& safeInteger(input.observedAtMs)
|
|
221
|
+
&& safeInteger(input.expiresAtMs)
|
|
222
|
+
&& input.expiresAtMs > input.observedAtMs
|
|
223
|
+
&& safeInteger(input.projectionRevision)
|
|
224
|
+
&& Array.isArray(input.features)
|
|
225
|
+
&& exactGrantedFeatures(catalogRevision.value, input.features)
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function suiteEntitlementReceiptMessage(
|
|
230
|
+
input: SuiteEntitlementReceiptPayload,
|
|
231
|
+
): string {
|
|
232
|
+
return JSON.stringify([
|
|
233
|
+
SUITE_ENTITLEMENT_RECEIPT_VERSION,
|
|
234
|
+
input.product,
|
|
235
|
+
input.environment,
|
|
236
|
+
input.suiteAccountId,
|
|
237
|
+
input.keyVersion,
|
|
238
|
+
input.issuedAtMs,
|
|
239
|
+
input.expiresAtMs,
|
|
240
|
+
input.entitlements.version,
|
|
241
|
+
input.entitlements.catalogRevision,
|
|
242
|
+
input.entitlements.observedAtMs,
|
|
243
|
+
input.entitlements.expiresAtMs,
|
|
244
|
+
input.entitlements.projectionRevision,
|
|
245
|
+
input.entitlements.features,
|
|
246
|
+
]);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function validateSuiteEntitlementReceipt(
|
|
250
|
+
input: SuiteEntitlementReceipt,
|
|
251
|
+
now: number,
|
|
252
|
+
): IdentityLinkInputIssue | null {
|
|
253
|
+
const product = parseSuiteLinkProduct(input.product);
|
|
254
|
+
const environment = parseSuiteEnvironment(input.environment);
|
|
255
|
+
if (
|
|
256
|
+
!safeInteger(now)
|
|
257
|
+
|| input.version !== SUITE_ENTITLEMENT_RECEIPT_VERSION
|
|
258
|
+
|| !product.ok
|
|
259
|
+
|| !environment.ok
|
|
260
|
+
|| !parseSuiteAccountId(input.suiteAccountId).ok
|
|
261
|
+
|| !/^[a-z0-9][a-z0-9._-]{0,31}$/u.test(input.keyVersion)
|
|
262
|
+
|| !safeInteger(input.issuedAtMs)
|
|
263
|
+
|| !safeInteger(input.expiresAtMs)
|
|
264
|
+
|| input.expiresAtMs <= input.issuedAtMs
|
|
265
|
+
|| input.expiresAtMs - input.issuedAtMs
|
|
266
|
+
> SUITE_ENTITLEMENT_RECEIPT_MAX_TTL_MS
|
|
267
|
+
|| !validateSuiteEntitlementsClaim(input.entitlements)
|
|
268
|
+
|| input.expiresAtMs > input.entitlements.expiresAtMs
|
|
269
|
+
|| !/^[A-Za-z0-9_-]{43}$/u.test(input.signature)
|
|
270
|
+
) {
|
|
271
|
+
return "invalid";
|
|
272
|
+
}
|
|
273
|
+
if (
|
|
274
|
+
input.issuedAtMs > now + IDENTITY_LINK_CLOCK_SKEW_MS
|
|
275
|
+
|| input.entitlements.observedAtMs > now + IDENTITY_LINK_CLOCK_SKEW_MS
|
|
276
|
+
) {
|
|
277
|
+
return "not-yet-valid";
|
|
278
|
+
}
|
|
279
|
+
return input.expiresAtMs <= now ? "expired" : null;
|
|
280
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { deepFreeze } from "../immutable.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public membership product shown on the shared subscription surfaces.
|
|
5
|
+
*
|
|
6
|
+
* Only display metadata lives here: the stable product ID, emoji, name,
|
|
7
|
+
* canonical link, and short description. Accounts and the hraness.com plan
|
|
8
|
+
* page render the list verbatim; the portfolio site keeps it convergent with
|
|
9
|
+
* the authored portfolio catalog.
|
|
10
|
+
*/
|
|
11
|
+
export type SuiteMembershipProduct = Readonly<{
|
|
12
|
+
description: string;
|
|
13
|
+
emoji: string;
|
|
14
|
+
href: `https://${string}`;
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
export const SUITE_MEMBERSHIP_PRODUCTS = deepFreeze([
|
|
20
|
+
{
|
|
21
|
+
description: "model benchmark scores plotted against cost and tokens per task",
|
|
22
|
+
emoji: "📈",
|
|
23
|
+
href: "https://aicharts.io",
|
|
24
|
+
id: "aicharts",
|
|
25
|
+
name: "AI CHARTS",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
description: "markdown knowledge base that gives agents the decisions behind code",
|
|
29
|
+
emoji: "📝",
|
|
30
|
+
href: "https://wordcell.io",
|
|
31
|
+
id: "kb",
|
|
32
|
+
name: "WORDCELL",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
description: "memory for agents that stores each fact with its sources and history",
|
|
36
|
+
emoji: "📚",
|
|
37
|
+
href: "https://oh.computer",
|
|
38
|
+
id: "oh-computer",
|
|
39
|
+
name: "OH",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
description: "private library for what you read, with notes your agent can cite",
|
|
43
|
+
emoji: "🧽",
|
|
44
|
+
href: "https://sponge.computer",
|
|
45
|
+
id: "sponge",
|
|
46
|
+
name: "SPONGE",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
description: "local personal crm for everyone you know, built for your agent",
|
|
50
|
+
emoji: "📇",
|
|
51
|
+
href: "https://peopleblade.com",
|
|
52
|
+
id: "peopleblade",
|
|
53
|
+
name: "PEOPLEBLADE",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
description: "named web actions for ai agents: read pages, save media, use connected accounts",
|
|
57
|
+
emoji: "👻",
|
|
58
|
+
href: "https://ghostget.com",
|
|
59
|
+
id: "ghostget",
|
|
60
|
+
name: "GHOSTGET",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
description: "free agent skill that writes dated dossiers on people, sources cited",
|
|
64
|
+
emoji: "🦾",
|
|
65
|
+
href: "https://soulscrape.com",
|
|
66
|
+
id: "soulscrape",
|
|
67
|
+
name: "SOULSCRAPE",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
description: "media studio for agents: images, 3d, animation, and video to revise",
|
|
71
|
+
emoji: "📸",
|
|
72
|
+
href: "https://slopcamera.com",
|
|
73
|
+
id: "slopcamera",
|
|
74
|
+
name: "SLOPCAMERA",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
description: "build prompt that turns a coding agent into a trading-system designer",
|
|
78
|
+
emoji: "📈",
|
|
79
|
+
href: "https://sloptrade.com",
|
|
80
|
+
id: "sloptrade",
|
|
81
|
+
name: "SLOPTRADE",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
description: "software studio making tools for ai agents and for people",
|
|
85
|
+
emoji: "🪢",
|
|
86
|
+
href: "https://hraness.com",
|
|
87
|
+
id: "hraness",
|
|
88
|
+
name: "HRANESS",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
description: "free sleep sounds made in your browser, with sourced sleep guides",
|
|
92
|
+
emoji: "💤",
|
|
93
|
+
href: "https://sleepy.land",
|
|
94
|
+
id: "sleepyland",
|
|
95
|
+
name: "SLEEPYLAND",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
description: "independent history of stripe where every event is dated and sourced",
|
|
99
|
+
emoji: "🦓",
|
|
100
|
+
href: "https://hraness.com/stripe",
|
|
101
|
+
id: "stripe-history",
|
|
102
|
+
name: "STRIPE HISTORY",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
description: "ehlers-danlos evidence stratified by kind, every record linked to sources",
|
|
106
|
+
emoji: "🧬",
|
|
107
|
+
href: "https://hraness.com/eds",
|
|
108
|
+
id: "eds-research",
|
|
109
|
+
name: "EDS RESEARCH INDEX",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
description: "estimate act 60 savings after real costs, with sourced guides and a day tracker",
|
|
113
|
+
emoji: "⏱️",
|
|
114
|
+
href: "https://act60.me",
|
|
115
|
+
id: "act60",
|
|
116
|
+
name: "ACT60",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
description: "browser experiments for a game of algorithmic organisms",
|
|
120
|
+
emoji: "🦠",
|
|
121
|
+
href: "https://platonik.space",
|
|
122
|
+
id: "platonik",
|
|
123
|
+
name: "PLATONIK",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
description: "repeatable app states for browser agents, opened by url",
|
|
127
|
+
emoji: "🎯",
|
|
128
|
+
href: "https://hraness.com/direct",
|
|
129
|
+
id: "direct",
|
|
130
|
+
name: "DIRECT",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
description: "fresh puzzles for ai agents, scored exactly, with a signed receipt",
|
|
134
|
+
emoji: "📡",
|
|
135
|
+
href: "https://clankdar.com",
|
|
136
|
+
id: "clankdar",
|
|
137
|
+
name: "CLANKDAR",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
description: "turn the chapters of your life into one timeline you can share",
|
|
141
|
+
emoji: "📊",
|
|
142
|
+
href: "https://lifecharts.io",
|
|
143
|
+
id: "lifedaysleft",
|
|
144
|
+
name: "LIFECHARTS",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
description: "free publication about how companies put ai agents to work",
|
|
148
|
+
emoji: "🏭",
|
|
149
|
+
href: "https://swft.io",
|
|
150
|
+
id: "swft",
|
|
151
|
+
name: "SWFT",
|
|
152
|
+
},
|
|
153
|
+
] as const satisfies readonly SuiteMembershipProduct[]);
|
|
154
|
+
|
|
155
|
+
export type SuiteMembershipProductId =
|
|
156
|
+
(typeof SUITE_MEMBERSHIP_PRODUCTS)[number]["id"];
|