@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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +496 -0
  3. package/compiler-foundation.css +1 -0
  4. package/dist/auth-client.js +16 -0
  5. package/dist/auth-proxy.js +757 -0
  6. package/dist/bearer-verifier.js +1119 -0
  7. package/dist/browser-session.js +208 -0
  8. package/dist/client-configuration.js +470 -0
  9. package/dist/convex-browser-auth-browser.js +923 -0
  10. package/dist/convex-browser-auth-server.js +846 -0
  11. package/dist/convex-browser-auth.js +597 -0
  12. package/dist/entitlements.js +502 -0
  13. package/dist/identity/functions.js +44 -0
  14. package/dist/identity/index.js +1716 -0
  15. package/dist/identity/return-targets.js +33 -0
  16. package/dist/index.js +1133 -0
  17. package/dist/oidc-device-code.js +253 -0
  18. package/dist/oidc-rp.js +2259 -0
  19. package/dist/oidc-session-policy.js +5 -0
  20. package/dist/oidc-surface-server.js +2332 -0
  21. package/dist/profile-form.js +1934 -0
  22. package/dist/profile.js +17 -0
  23. package/dist/public-config.js +462 -0
  24. package/dist/react.js +137 -0
  25. package/dist/receipt-verifier.js +619 -0
  26. package/dist/registry.js +327 -0
  27. package/dist/stylex-manifest.json +1 -0
  28. package/dist/stylex.css +435 -0
  29. package/dist/urls.js +391 -0
  30. package/package.json +263 -0
  31. package/portfolio-inventory.json +37 -0
  32. package/src/auth-client.ts +37 -0
  33. package/src/auth-proxy.ts +473 -0
  34. package/src/bearer-verifier.ts +478 -0
  35. package/src/browser-session.ts +260 -0
  36. package/src/client-configuration.ts +172 -0
  37. package/src/convex-browser-auth-browser.ts +238 -0
  38. package/src/convex-browser-auth-server.ts +492 -0
  39. package/src/convex-browser-auth.ts +208 -0
  40. package/src/convex-url.ts +74 -0
  41. package/src/entitlements.ts +317 -0
  42. package/src/identity/catalog.ts +227 -0
  43. package/src/identity/consumers.ts +53 -0
  44. package/src/identity/functions.ts +70 -0
  45. package/src/identity/identifiers.ts +82 -0
  46. package/src/identity/index.ts +10 -0
  47. package/src/identity/links.ts +280 -0
  48. package/src/identity/membership.ts +156 -0
  49. package/src/identity/principals.ts +301 -0
  50. package/src/identity/profiles-v2.ts +330 -0
  51. package/src/identity/profiles.ts +524 -0
  52. package/src/identity/return-targets.ts +20 -0
  53. package/src/identity/usernames.ts +101 -0
  54. package/src/identity/views.ts +289 -0
  55. package/src/immutable.ts +25 -0
  56. package/src/index.ts +7 -0
  57. package/src/oidc-continuation.ts +59 -0
  58. package/src/oidc-device-code.ts +401 -0
  59. package/src/oidc-rp.ts +2217 -0
  60. package/src/oidc-session-policy.ts +6 -0
  61. package/src/oidc-surface-server.ts +186 -0
  62. package/src/profile-form.css +1 -0
  63. package/src/profile-form.stylex.ts +226 -0
  64. package/src/profile-form.tsx +314 -0
  65. package/src/profile.ts +33 -0
  66. package/src/public-config.ts +294 -0
  67. package/src/public-profile-form-result.ts +82 -0
  68. package/src/public-profile-form.tsx +319 -0
  69. package/src/public-profile-links.ts +100 -0
  70. package/src/react.tsx +196 -0
  71. package/src/receipt-verifier.ts +273 -0
  72. package/src/registry.ts +600 -0
  73. 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"];