@chatarmin/os 1.0.0 → 1.0.2
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/README.md +750 -80
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,167 +1,837 @@
|
|
|
1
|
-
# @chatarmin/os
|
|
1
|
+
# @chatarmin/os
|
|
2
2
|
|
|
3
|
-
Type-safe SDK for ChatarminOS -
|
|
3
|
+
> Type-safe SDK for ChatarminOS — the all-in-one platform for B2B SaaS to manage customers, subscriptions, feature access, and billing.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@chatarmin/os)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
## Features
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
- 🏢 **Company Management** — Create, link, and manage customer companies
|
|
11
|
+
- 🎫 **Subscription Tiers** — Define tiers with features and Stripe pricing
|
|
12
|
+
- ✨ **Feature Access** — Check entitlements and enforce usage limits
|
|
13
|
+
- 📊 **Usage Tracking** — Metered billing with idempotency support
|
|
14
|
+
- 💳 **Stripe Integration** — Claim checkout sessions, link subscriptions
|
|
15
|
+
- 🔗 **Smart Linking** — Intelligent company matching by domain/name/email
|
|
16
|
+
- 👥 **Contact Sync** — Bulk upsert contacts for each company
|
|
17
|
+
- 🚀 **Unified Onboarding** — Complete setup in a single API call
|
|
10
18
|
|
|
11
|
-
|
|
19
|
+
---
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
# .npmrc
|
|
15
|
-
@chatarmin:registry=https://registry.npmjs.org/
|
|
16
|
-
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
|
17
|
-
```
|
|
21
|
+
## Table of Contents
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
- [Installation](#installation)
|
|
24
|
+
- [Quick Start](#quick-start)
|
|
25
|
+
- [API Reference](#api-reference)
|
|
26
|
+
- [Companies](#companies)
|
|
27
|
+
- [Contacts](#contacts)
|
|
28
|
+
- [Features](#features)
|
|
29
|
+
- [Billing](#billing)
|
|
30
|
+
- [Subscriptions & Tiers](#subscriptions--tiers)
|
|
31
|
+
- [Links](#links)
|
|
32
|
+
- [Onboarding](#onboarding)
|
|
33
|
+
- [Common Patterns](#common-patterns)
|
|
34
|
+
- [TypeScript Support](#typescript-support)
|
|
35
|
+
- [Configuration](#configuration)
|
|
36
|
+
- [Error Handling](#error-handling)
|
|
20
37
|
|
|
21
|
-
|
|
22
|
-
# .env (for local development)
|
|
23
|
-
NPM_TOKEN=npm_yourtokenhere
|
|
38
|
+
---
|
|
24
39
|
|
|
25
|
-
|
|
26
|
-
export NPM_TOKEN=npm_yourtokenhere
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
**For CI/CD**: Add `NPM_TOKEN` as a secret in your deployment platform (GitHub Actions, Vercel, etc.)
|
|
30
|
-
|
|
31
|
-
### 2. Install the Package
|
|
40
|
+
## Installation
|
|
32
41
|
|
|
33
42
|
```bash
|
|
34
|
-
|
|
43
|
+
pnpm add @chatarmin/os
|
|
35
44
|
# or
|
|
36
|
-
|
|
45
|
+
npm install @chatarmin/os
|
|
37
46
|
# or
|
|
38
|
-
yarn add @chatarmin/os
|
|
47
|
+
yarn add @chatarmin/os
|
|
39
48
|
```
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
---
|
|
42
51
|
|
|
43
52
|
## Quick Start
|
|
44
53
|
|
|
45
54
|
```typescript
|
|
46
|
-
import { ChatarminOS } from "@chatarmin/os
|
|
55
|
+
import { ChatarminOS } from "@chatarmin/os"
|
|
47
56
|
|
|
48
57
|
const os = new ChatarminOS({
|
|
49
|
-
apiKey: process.env.
|
|
58
|
+
apiKey: process.env.OS_API_KEY!,
|
|
50
59
|
// Optional: custom base URL for self-hosted
|
|
51
60
|
// baseUrl: 'https://your-os.example.com/api/v1'
|
|
52
61
|
})
|
|
62
|
+
|
|
63
|
+
// Onboard a new customer (single call does everything!)
|
|
64
|
+
const result = await os.onboard({
|
|
65
|
+
externalOrgId: "org_abc123", // Your product's org ID
|
|
66
|
+
hints: {
|
|
67
|
+
companyName: "Acme Inc",
|
|
68
|
+
domain: "acme.com",
|
|
69
|
+
},
|
|
70
|
+
tierCode: "free",
|
|
71
|
+
contactEmail: "admin@acme.com",
|
|
72
|
+
contactName: "John Doe",
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
console.log(result.companyId) // Use this for all future API calls
|
|
76
|
+
console.log(result.linkStatus) // 'created' | 'linked' | 'already_linked'
|
|
53
77
|
```
|
|
54
78
|
|
|
55
|
-
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## API Reference
|
|
56
82
|
|
|
57
83
|
### Companies
|
|
58
84
|
|
|
85
|
+
Manage customer companies and their links to your product.
|
|
86
|
+
|
|
87
|
+
#### `companies.getByProductLink(input)`
|
|
88
|
+
|
|
89
|
+
Look up a company by your external organization ID.
|
|
90
|
+
|
|
59
91
|
```typescript
|
|
60
|
-
// Find company by your external org ID
|
|
61
92
|
const company = await os.companies.getByProductLink({
|
|
62
|
-
externalOrgId: "
|
|
93
|
+
externalOrgId: "org_abc123",
|
|
63
94
|
})
|
|
64
95
|
|
|
65
|
-
|
|
66
|
-
|
|
96
|
+
if (company) {
|
|
97
|
+
console.log(`Found: ${company.name} (${company.id})`)
|
|
98
|
+
} else {
|
|
99
|
+
console.log("Company not linked yet")
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### `companies.create(input)`
|
|
104
|
+
|
|
105
|
+
Create a new company and link it to your product.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
const company = await os.companies.create({
|
|
67
109
|
name: "Acme Inc",
|
|
68
110
|
domain: "acme.com",
|
|
69
|
-
externalOrgId: "
|
|
70
|
-
contactEmail: "
|
|
111
|
+
externalOrgId: "org_abc123",
|
|
112
|
+
contactEmail: "admin@acme.com",
|
|
71
113
|
contactName: "John Doe",
|
|
114
|
+
createdAt: "2024-01-15T10:30:00.000Z", // Optional: historical date
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
console.log(company.id) // UUID
|
|
118
|
+
console.log(company.shortId) // e.g., "acme-inc"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### `companies.update(input)`
|
|
122
|
+
|
|
123
|
+
Update company metadata (e.g., backfill historical dates).
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
await os.companies.update({
|
|
127
|
+
companyId: "uuid-here",
|
|
128
|
+
createdAt: "2023-06-01T00:00:00.000Z",
|
|
72
129
|
})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### `companies.smartLink(input)`
|
|
73
133
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
134
|
+
Intelligently find and link a company using hints.
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const result = await os.companies.smartLink({
|
|
138
|
+
externalOrgId: "org_abc123",
|
|
77
139
|
hints: {
|
|
78
|
-
companyName: "Acme",
|
|
140
|
+
companyName: "Acme Inc",
|
|
79
141
|
domain: "acme.com",
|
|
80
142
|
emails: ["john@acme.com", "jane@acme.com"],
|
|
81
143
|
},
|
|
82
|
-
minConfidence: 0.
|
|
144
|
+
minConfidence: 0.7, // Default: 0.6
|
|
83
145
|
})
|
|
146
|
+
|
|
147
|
+
switch (result.status) {
|
|
148
|
+
case "already_linked":
|
|
149
|
+
console.log("Already linked to your product")
|
|
150
|
+
break
|
|
151
|
+
case "linked":
|
|
152
|
+
console.log(
|
|
153
|
+
`Auto-linked to ${result.customerName} (${result.confidence}% confidence)`
|
|
154
|
+
)
|
|
155
|
+
break
|
|
156
|
+
case "suggestions":
|
|
157
|
+
console.log("Multiple matches found:", result.suggestions)
|
|
158
|
+
break
|
|
159
|
+
case "no_match":
|
|
160
|
+
console.log("No matching company found")
|
|
161
|
+
break
|
|
162
|
+
}
|
|
84
163
|
```
|
|
85
164
|
|
|
86
|
-
|
|
165
|
+
**Matching priority:**
|
|
166
|
+
|
|
167
|
+
1. Domain match → 95% confidence
|
|
168
|
+
2. Name match → up to 80% confidence
|
|
169
|
+
3. Email domain match → 70% confidence
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### Contacts
|
|
174
|
+
|
|
175
|
+
Manage people associated with companies.
|
|
176
|
+
|
|
177
|
+
#### `contacts.create(input)`
|
|
178
|
+
|
|
179
|
+
Create or update a single contact.
|
|
87
180
|
|
|
88
181
|
```typescript
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
182
|
+
const contact = await os.contacts.create({
|
|
183
|
+
companyId: "comp_xxx",
|
|
184
|
+
email: "john@acme.com",
|
|
185
|
+
name: "John Doe",
|
|
186
|
+
role: "CEO",
|
|
187
|
+
isPrimary: true,
|
|
188
|
+
avatarUrl: "https://example.com/avatar.jpg",
|
|
189
|
+
metadata: { slackId: "U123" },
|
|
92
190
|
})
|
|
93
191
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
192
|
+
if (contact.isNew) {
|
|
193
|
+
console.log("Created new contact")
|
|
194
|
+
} else {
|
|
195
|
+
console.log("Updated existing contact")
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### `contacts.bulkUpsert(input)`
|
|
200
|
+
|
|
201
|
+
Bulk create or update multiple contacts. **Recommended for onboarding.**
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const result = await os.contacts.bulkUpsert({
|
|
205
|
+
companyId: "comp_xxx",
|
|
206
|
+
contacts: [
|
|
207
|
+
{ email: "ceo@acme.com", name: "Alice", role: "CEO", isPrimary: true },
|
|
208
|
+
{ email: "cto@acme.com", name: "Bob", role: "CTO" },
|
|
209
|
+
{ email: "dev@acme.com", name: "Charlie", role: "Engineer" },
|
|
210
|
+
],
|
|
100
211
|
})
|
|
101
212
|
|
|
102
|
-
|
|
103
|
-
|
|
213
|
+
console.log(`Created: ${result.created}, Updated: ${result.updated}`)
|
|
214
|
+
// result.contacts contains individual results with IDs
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### `contacts.list(companyId, primaryOnly?)`
|
|
104
218
|
|
|
105
|
-
|
|
106
|
-
|
|
219
|
+
List contacts for a company.
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// Get all contacts
|
|
223
|
+
const contacts = await os.contacts.list("comp_xxx")
|
|
224
|
+
|
|
225
|
+
// Get only the primary contact
|
|
226
|
+
const [primary] = await os.contacts.list("comp_xxx", true)
|
|
107
227
|
```
|
|
108
228
|
|
|
109
|
-
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
### Features
|
|
232
|
+
|
|
233
|
+
Check and manage feature access for companies.
|
|
234
|
+
|
|
235
|
+
#### `features.check(input)`
|
|
236
|
+
|
|
237
|
+
Check if a company has access to a specific feature.
|
|
110
238
|
|
|
111
239
|
```typescript
|
|
112
|
-
// Check if company has access to a feature
|
|
113
240
|
const access = await os.features.check({
|
|
114
|
-
companyId:
|
|
115
|
-
featureCode: "
|
|
241
|
+
companyId: "comp_xxx",
|
|
242
|
+
featureCode: "ai_credit",
|
|
116
243
|
})
|
|
117
244
|
|
|
118
|
-
|
|
119
|
-
|
|
245
|
+
console.log({
|
|
246
|
+
enabled: access.isEnabled, // Feature is turned on
|
|
247
|
+
canUse: access.canUse, // Can use right now (not at limit)
|
|
248
|
+
hasQuantity: access.hasQuantity, // Is a quantity-based feature
|
|
249
|
+
usage: access.currentUsage, // Current usage count
|
|
250
|
+
limit: access.quantityLimit, // Max allowed (null = unlimited)
|
|
251
|
+
included: access.includedQuantity, // Free tier before billing
|
|
252
|
+
remaining: access.remaining, // How many left (null = unlimited)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
if (!access.canUse) {
|
|
256
|
+
throw new Error("Upgrade required")
|
|
120
257
|
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### `features.listAccess(companyId)`
|
|
261
|
+
|
|
262
|
+
List all features and their access status for a company.
|
|
121
263
|
|
|
122
|
-
|
|
123
|
-
const
|
|
264
|
+
```typescript
|
|
265
|
+
const features = await os.features.listAccess("comp_xxx")
|
|
266
|
+
|
|
267
|
+
for (const f of features) {
|
|
268
|
+
console.log(`${f.featureName}: ${f.isEnabled ? "✓" : "✗"}`)
|
|
269
|
+
if (f.quantityLimit) {
|
|
270
|
+
console.log(` Usage: ${f.currentUsage} / ${f.quantityLimit}`)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
124
273
|
```
|
|
125
274
|
|
|
126
|
-
|
|
275
|
+
#### `features.setAccess(input)`
|
|
276
|
+
|
|
277
|
+
Update feature access with **partial config merge**.
|
|
127
278
|
|
|
128
279
|
```typescript
|
|
129
|
-
//
|
|
130
|
-
await os.
|
|
131
|
-
companyId:
|
|
132
|
-
featureCode: "
|
|
280
|
+
// Update by company ID
|
|
281
|
+
await os.features.setAccess({
|
|
282
|
+
companyId: "comp_xxx",
|
|
283
|
+
featureCode: "ai_credit",
|
|
284
|
+
isEnabled: true,
|
|
285
|
+
quantityLimit: 1000,
|
|
286
|
+
includedQuantity: 100,
|
|
287
|
+
config: {
|
|
288
|
+
model: "gpt-4",
|
|
289
|
+
maxTokens: 4096,
|
|
290
|
+
},
|
|
291
|
+
source: "manual", // 'subscription' | 'manual' | 'trial' | 'api'
|
|
292
|
+
validUntil: new Date("2025-12-31"), // null for no expiration
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
// Update by external org ID
|
|
296
|
+
await os.features.setAccess({
|
|
297
|
+
externalOrgId: "org_abc123",
|
|
298
|
+
featureCode: "team_seats",
|
|
299
|
+
config: { maxSeats: 50 }, // Only updates maxSeats, preserves other config
|
|
300
|
+
})
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
#### `features.getAccessByExternalOrgId(externalOrgId)`
|
|
304
|
+
|
|
305
|
+
Get all features using your product's external org ID.
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
const result = await os.features.getAccessByExternalOrgId("org_abc123")
|
|
309
|
+
|
|
310
|
+
console.log(`Company: ${result.companyId}`)
|
|
311
|
+
for (const f of result.features) {
|
|
312
|
+
console.log(`${f.featureCode}: ${f.isEnabled}`)
|
|
313
|
+
console.log(" Config:", f.configValues)
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### `features.checkByExternalOrgId(externalOrgId, featureCode)`
|
|
318
|
+
|
|
319
|
+
Check a specific feature using your external org ID.
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
const access = await os.features.checkByExternalOrgId("org_abc123", "ai_credit")
|
|
323
|
+
|
|
324
|
+
if (access.canUse) {
|
|
325
|
+
console.log(`Remaining: ${access.remaining}`)
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
### Billing
|
|
332
|
+
|
|
333
|
+
Track usage, claim subscriptions, and manage billing.
|
|
334
|
+
|
|
335
|
+
#### `billing.trackUsage(input)`
|
|
336
|
+
|
|
337
|
+
Track usage for a metered feature.
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
const result = await os.billing.trackUsage({
|
|
341
|
+
companyId: "comp_xxx",
|
|
342
|
+
featureCode: "ai_credit",
|
|
133
343
|
quantity: 5,
|
|
134
|
-
idempotencyKey:
|
|
135
|
-
metadata: { model: "gpt-4" },
|
|
344
|
+
idempotencyKey: `request_${requestId}`, // Prevents double-counting
|
|
345
|
+
metadata: { model: "gpt-4", tokens: 1500 },
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
console.log({
|
|
349
|
+
currentUsage: result.currentUsage,
|
|
350
|
+
remaining: result.remaining,
|
|
351
|
+
billable: result.billableQuantity, // Amount beyond included tier
|
|
352
|
+
isAtLimit: result.isAtLimit,
|
|
136
353
|
})
|
|
137
354
|
```
|
|
138
355
|
|
|
139
|
-
|
|
356
|
+
#### `billing.claimCheckout(input)`
|
|
357
|
+
|
|
358
|
+
Claim a subscription from a completed Stripe Checkout Session.
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// In your Stripe checkout success handler
|
|
362
|
+
const result = await os.billing.claimCheckout({
|
|
363
|
+
companyId: "comp_xxx",
|
|
364
|
+
checkoutSessionId: "cs_test_xxx", // From Stripe callback
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
if (result.alreadyClaimed) {
|
|
368
|
+
console.log("Subscription was already claimed")
|
|
369
|
+
} else {
|
|
370
|
+
console.log(`Claimed subscription: ${result.subscriptionId}`)
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
#### `billing.linkSubscription(input)`
|
|
375
|
+
|
|
376
|
+
Link an existing Stripe subscription to a company.
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
// When you create subscriptions on YOUR Stripe account
|
|
380
|
+
const result = await os.billing.linkSubscription({
|
|
381
|
+
companyId: "comp_xxx",
|
|
382
|
+
stripeSubscriptionId: "sub_xxx",
|
|
383
|
+
stripeCustomerId: "cus_xxx", // Optional: fetched from subscription if omitted
|
|
384
|
+
tierCode: "pro", // Apply tier features
|
|
385
|
+
displayName: "Pro Plan",
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
console.log({
|
|
389
|
+
subscriptionId: result.subscriptionId,
|
|
390
|
+
isNew: !result.alreadyLinked,
|
|
391
|
+
featuresApplied: result.featuresApplied,
|
|
392
|
+
})
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
#### `billing.getStatus(companyId)`
|
|
396
|
+
|
|
397
|
+
Get billing status overview for a company.
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
const status = await os.billing.getStatus("comp_xxx")
|
|
401
|
+
|
|
402
|
+
console.log({
|
|
403
|
+
hasBilling: status.hasBillingProfile,
|
|
404
|
+
subscriptions: status.activeSubscriptions.length,
|
|
405
|
+
features: status.enabledFeaturesCount,
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
// Check active tier
|
|
409
|
+
if (status.activeSubscriptions[0]?.tier) {
|
|
410
|
+
console.log(`Current tier: ${status.activeSubscriptions[0].tier.name}`)
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
### Subscriptions & Tiers
|
|
417
|
+
|
|
418
|
+
Manage subscription tiers and apply features.
|
|
419
|
+
|
|
420
|
+
#### `tiers.list()`
|
|
421
|
+
|
|
422
|
+
List all available tiers for your product.
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
const tiers = await os.tiers.list()
|
|
426
|
+
|
|
427
|
+
for (const tier of tiers) {
|
|
428
|
+
console.log(`${tier.name} (${tier.code})`)
|
|
429
|
+
}
|
|
430
|
+
// ['Free', 'Pro', 'Enterprise']
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
#### `tiers.get(tierCode)`
|
|
434
|
+
|
|
435
|
+
Get detailed tier information including Stripe prices.
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
const tier = await os.tiers.get("pro")
|
|
439
|
+
|
|
440
|
+
// Check base fee
|
|
441
|
+
if (tier.baseFee) {
|
|
442
|
+
console.log(`Base fee: ${tier.baseFee.stripe.unitAmount / 100}€/mo`)
|
|
443
|
+
console.log(`Price ID: ${tier.baseFee.stripe.priceId}`)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Access features
|
|
447
|
+
for (const feature of tier.features) {
|
|
448
|
+
console.log(`${feature.code}:`, feature.configValues)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Get all Stripe price IDs (base fee + feature prices)
|
|
452
|
+
console.log("Stripe prices:", tier.stripePriceIds)
|
|
453
|
+
// ['price_base', 'price_feature1', 'price_feature2']
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
#### `tiers.apply(tierCode, options)`
|
|
457
|
+
|
|
458
|
+
Apply a tier's features to a company (without Stripe).
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
// Apply free tier
|
|
462
|
+
await os.tiers.apply("free", {
|
|
463
|
+
companyId: "comp_xxx",
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
// Apply with custom limits
|
|
467
|
+
await os.tiers.apply("trial", {
|
|
468
|
+
companyId: "comp_xxx",
|
|
469
|
+
features: {
|
|
470
|
+
ai_credit: { included_quantity: 100 },
|
|
471
|
+
seats: { max_quantity: 10 },
|
|
472
|
+
},
|
|
473
|
+
})
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
#### `subscriptions.create(tierCode, options)`
|
|
477
|
+
|
|
478
|
+
Alias for `tiers.apply()` — apply a subscription tier.
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
await os.subscriptions.create("pro", {
|
|
482
|
+
companyId: "comp_xxx",
|
|
483
|
+
features: {
|
|
484
|
+
ai_credit: { included_quantity: 500 },
|
|
485
|
+
},
|
|
486
|
+
})
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
### Links
|
|
492
|
+
|
|
493
|
+
Manually manage product links.
|
|
494
|
+
|
|
495
|
+
#### `links.create(input)`
|
|
496
|
+
|
|
497
|
+
Create a manual link between your external org ID and an existing company.
|
|
140
498
|
|
|
141
499
|
```typescript
|
|
142
|
-
// Link an external org ID to an existing company
|
|
143
500
|
await os.links.create({
|
|
501
|
+
companyId: "comp_xxx",
|
|
502
|
+
externalOrgId: "org_abc123",
|
|
503
|
+
label: "Store Vienna", // Optional: display label
|
|
504
|
+
externalUserId: "user_xyz", // Optional: user who created the link
|
|
505
|
+
})
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
#### `links.reset(options?)`
|
|
509
|
+
|
|
510
|
+
Delete all product links for your product. Use before re-importing.
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
// Basic reset (links only)
|
|
514
|
+
const result = await os.links.reset()
|
|
515
|
+
console.log(`Deleted ${result.linksDeleted} links`)
|
|
516
|
+
|
|
517
|
+
// Full reset (links + contacts + MRR)
|
|
518
|
+
const result = await os.links.reset({
|
|
519
|
+
deleteContacts: true,
|
|
520
|
+
resetMrr: true,
|
|
521
|
+
})
|
|
522
|
+
console.log(
|
|
523
|
+
`Deleted ${result.linksDeleted} links, ${result.contactsDeleted} contacts`
|
|
524
|
+
)
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
### Onboarding
|
|
530
|
+
|
|
531
|
+
Complete customer onboarding in a single API call.
|
|
532
|
+
|
|
533
|
+
The `onboard()` method handles:
|
|
534
|
+
|
|
535
|
+
1. Smart company matching/creation
|
|
536
|
+
2. Product linking
|
|
537
|
+
3. Subscription claiming/linking
|
|
538
|
+
4. Tier feature application
|
|
539
|
+
|
|
540
|
+
#### Basic Usage
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
const result = await os.onboard({
|
|
544
|
+
externalOrgId: "org_abc123", // Your product's org ID (required)
|
|
545
|
+
hints: {
|
|
546
|
+
companyName: "Acme Inc",
|
|
547
|
+
domain: "acme.com",
|
|
548
|
+
emails: ["john@acme.com"],
|
|
549
|
+
},
|
|
550
|
+
tierCode: "free",
|
|
551
|
+
contactEmail: "admin@acme.com",
|
|
552
|
+
contactName: "John Doe",
|
|
553
|
+
createdAt: "2024-01-15T10:30:00.000Z", // Optional: historical date
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
console.log({
|
|
557
|
+
companyId: result.companyId,
|
|
558
|
+
companyName: result.companyName,
|
|
559
|
+
linkStatus: result.linkStatus, // 'created' | 'linked' | 'already_linked'
|
|
560
|
+
tierApplied: result.billingStatus?.tierApplied,
|
|
561
|
+
})
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
#### Scenario 1: User Completed Stripe Checkout
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
const result = await os.onboard({
|
|
568
|
+
externalOrgId: "org_abc123",
|
|
569
|
+
hints: { companyName: "Acme Inc", domain: "acme.com" },
|
|
570
|
+
checkoutSessionId: "cs_test_xxx", // From Stripe checkout callback
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
// result.billingStatus.claimed = true
|
|
574
|
+
// result.billingStatus.subscriptionId = 'sub_xxx'
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
#### Scenario 2: Free Tier Signup
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
const result = await os.onboard({
|
|
581
|
+
externalOrgId: "org_abc123",
|
|
582
|
+
hints: { companyName: "Acme Inc" },
|
|
583
|
+
tierCode: "free",
|
|
584
|
+
contactEmail: "admin@acme.com",
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
// result.billingStatus.tierApplied = true
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
#### Scenario 3: You Create Stripe Subscription
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
// 1. Get tier prices
|
|
594
|
+
const tier = await os.tiers.get("pro")
|
|
595
|
+
|
|
596
|
+
// 2. Create subscription on YOUR Stripe account
|
|
597
|
+
const stripeSub = await stripe.subscriptions.create({
|
|
598
|
+
customer: stripeCustomerId,
|
|
599
|
+
items: tier.stripePriceIds.map((price) => ({ price })),
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
// 3. Onboard with the subscription
|
|
603
|
+
const result = await os.onboard({
|
|
604
|
+
externalOrgId: "org_abc123",
|
|
605
|
+
hints: { companyName: "Acme Inc" },
|
|
606
|
+
stripeSubscriptionId: stripeSub.id,
|
|
607
|
+
stripeCustomerId: stripeCustomerId,
|
|
608
|
+
tierCode: "pro",
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
// result.billingStatus.linked = true
|
|
612
|
+
// result.billingStatus.tierApplied = true
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
#### Handling Existing Customers
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
const result = await os.onboard({
|
|
619
|
+
externalOrgId: "org_abc123",
|
|
620
|
+
hints: { companyName: "Acme Inc" },
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
switch (result.linkStatus) {
|
|
624
|
+
case "already_linked":
|
|
625
|
+
console.log("Welcome back!")
|
|
626
|
+
break
|
|
627
|
+
case "linked":
|
|
628
|
+
console.log("Found existing company, linked!")
|
|
629
|
+
break
|
|
630
|
+
case "created":
|
|
631
|
+
console.log("New company created!")
|
|
632
|
+
break
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
## Common Patterns
|
|
639
|
+
|
|
640
|
+
### Feature Gating
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
async function checkFeatureAccess(orgId: string, feature: string) {
|
|
644
|
+
const access = await os.features.checkByExternalOrgId(orgId, feature)
|
|
645
|
+
|
|
646
|
+
if (!access.isEnabled) {
|
|
647
|
+
throw new Error(`Feature ${feature} is not available on your plan`)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (!access.canUse) {
|
|
651
|
+
throw new Error(`Usage limit reached for ${feature}. Please upgrade.`)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return access
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Usage
|
|
658
|
+
await checkFeatureAccess("org_abc123", "ai_credit")
|
|
659
|
+
await performAIOperation()
|
|
660
|
+
await os.billing.trackUsage({
|
|
144
661
|
companyId: company.id,
|
|
145
|
-
|
|
146
|
-
|
|
662
|
+
featureCode: "ai_credit",
|
|
663
|
+
quantity: 1,
|
|
147
664
|
})
|
|
148
665
|
```
|
|
149
666
|
|
|
150
|
-
|
|
667
|
+
### Stripe Subscription Flow
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
// 1. User selects a plan → Get tier prices
|
|
671
|
+
const tier = await os.tiers.get("pro")
|
|
672
|
+
|
|
673
|
+
// 2. Create Stripe Checkout Session with tier prices
|
|
674
|
+
const session = await stripe.checkout.sessions.create({
|
|
675
|
+
customer: customerId,
|
|
676
|
+
line_items: tier.stripePriceIds.map((price) => ({
|
|
677
|
+
price,
|
|
678
|
+
quantity: 1,
|
|
679
|
+
})),
|
|
680
|
+
success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
681
|
+
cancel_url: `${baseUrl}/cancel`,
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
// 3. After checkout completes → Claim subscription
|
|
685
|
+
const result = await os.onboard({
|
|
686
|
+
externalOrgId: "org_abc123",
|
|
687
|
+
checkoutSessionId: session.id,
|
|
688
|
+
})
|
|
689
|
+
```
|
|
151
690
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
691
|
+
### Backfilling Historical Data
|
|
692
|
+
|
|
693
|
+
```typescript
|
|
694
|
+
import { ChatarminOS } from "@chatarmin/os"
|
|
695
|
+
import historicalData from "./migration-data.json"
|
|
696
|
+
|
|
697
|
+
const os = new ChatarminOS({ apiKey: process.env.OS_API_KEY! })
|
|
698
|
+
|
|
699
|
+
for (const org of historicalData) {
|
|
700
|
+
// Create company with historical date
|
|
701
|
+
const result = await os.onboard({
|
|
702
|
+
externalOrgId: org.id,
|
|
703
|
+
hints: {
|
|
704
|
+
companyName: org.name,
|
|
705
|
+
domain: org.domain,
|
|
706
|
+
},
|
|
707
|
+
tierCode: org.plan,
|
|
708
|
+
contactEmail: org.adminEmail,
|
|
709
|
+
createdAt: org.createdAt, // Historical timestamp
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
// Sync contacts
|
|
713
|
+
await os.contacts.bulkUpsert({
|
|
714
|
+
companyId: result.companyId,
|
|
715
|
+
contacts: org.users.map((u) => ({
|
|
716
|
+
email: u.email,
|
|
717
|
+
name: u.name,
|
|
718
|
+
role: u.role,
|
|
719
|
+
})),
|
|
720
|
+
})
|
|
721
|
+
}
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
---
|
|
156
725
|
|
|
157
726
|
## TypeScript Support
|
|
158
727
|
|
|
159
|
-
The SDK is fully typed
|
|
728
|
+
The SDK is fully typed with comprehensive TypeScript definitions.
|
|
729
|
+
|
|
730
|
+
### Importing Types
|
|
731
|
+
|
|
732
|
+
```typescript
|
|
733
|
+
import { ChatarminOS } from "@chatarmin/os"
|
|
734
|
+
import type {
|
|
735
|
+
ChatarminOSConfig,
|
|
736
|
+
CreateCompanyInput,
|
|
737
|
+
SmartLinkInput,
|
|
738
|
+
SmartLinkResult,
|
|
739
|
+
ContactInput,
|
|
740
|
+
FeatureCheckInput,
|
|
741
|
+
FeatureSetAccessInput,
|
|
742
|
+
TrackUsageInput,
|
|
743
|
+
OnboardInput,
|
|
744
|
+
OnboardResult,
|
|
745
|
+
ApplyTierInput,
|
|
746
|
+
ClaimCheckoutInput,
|
|
747
|
+
LinkSubscriptionInput,
|
|
748
|
+
} from "@chatarmin/os"
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
### Router Types
|
|
752
|
+
|
|
753
|
+
```typescript
|
|
754
|
+
import type { AppRouter } from "@chatarmin/os"
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
---
|
|
758
|
+
|
|
759
|
+
## Configuration
|
|
760
|
+
|
|
761
|
+
| Option | Type | Required | Default | Description |
|
|
762
|
+
| --------- | -------- | -------- | --------------------------------- | --------------------------- |
|
|
763
|
+
| `apiKey` | `string` | ✅ | — | API key from OS admin panel |
|
|
764
|
+
| `baseUrl` | `string` | — | `https://os.chatarmin.com/api/v1` | API endpoint |
|
|
160
765
|
|
|
161
766
|
```typescript
|
|
162
|
-
|
|
767
|
+
const os = new ChatarminOS({
|
|
768
|
+
apiKey: process.env.OS_API_KEY!,
|
|
769
|
+
baseUrl: "https://os.chatarmin.com/api/v1", // Default
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
// Local development
|
|
773
|
+
const osLocal = new ChatarminOS({
|
|
774
|
+
apiKey: process.env.OS_API_KEY!,
|
|
775
|
+
baseUrl: "http://localhost:3001/api/v1",
|
|
776
|
+
})
|
|
163
777
|
```
|
|
164
778
|
|
|
779
|
+
### Getting Your API Key
|
|
780
|
+
|
|
781
|
+
1. Go to ChatarminOS → **Settings** → **Developers** → **API Keys**
|
|
782
|
+
2. Click "Create API Key"
|
|
783
|
+
3. Copy the key (format: `os_sk_xxxxxxxxxxxx`)
|
|
784
|
+
4. Store in environment variables
|
|
785
|
+
|
|
786
|
+
---
|
|
787
|
+
|
|
788
|
+
## Error Handling
|
|
789
|
+
|
|
790
|
+
The SDK uses tRPC under the hood. Errors include structured information:
|
|
791
|
+
|
|
792
|
+
```typescript
|
|
793
|
+
try {
|
|
794
|
+
await os.billing.trackUsage({
|
|
795
|
+
companyId: "comp_xxx",
|
|
796
|
+
featureCode: "ai_credit",
|
|
797
|
+
quantity: 1000,
|
|
798
|
+
})
|
|
799
|
+
} catch (error) {
|
|
800
|
+
if (error instanceof TRPCClientError) {
|
|
801
|
+
console.error("Code:", error.data?.code) // e.g., 'FORBIDDEN'
|
|
802
|
+
console.error("Message:", error.message) // Human-readable message
|
|
803
|
+
console.error("HTTP Status:", error.data?.httpStatus)
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### Common Error Codes
|
|
809
|
+
|
|
810
|
+
| Code | Description |
|
|
811
|
+
| --------------------- | -------------------------- |
|
|
812
|
+
| `UNAUTHORIZED` | Invalid or missing API key |
|
|
813
|
+
| `FORBIDDEN` | No access to this resource |
|
|
814
|
+
| `NOT_FOUND` | Company/feature not found |
|
|
815
|
+
| `BAD_REQUEST` | Invalid input parameters |
|
|
816
|
+
| `PRECONDITION_FAILED` | Usage limit exceeded |
|
|
817
|
+
|
|
818
|
+
---
|
|
819
|
+
|
|
820
|
+
## Related Documentation
|
|
821
|
+
|
|
822
|
+
- **[QUICKSTART.md](./QUICKSTART.md)** — Quick publishing & installation guide
|
|
823
|
+
- **[PUBLISHING.md](./PUBLISHING.md)** — Detailed publishing instructions
|
|
824
|
+
- **[test-sdk.ts](./test-sdk.ts)** — Complete usage examples
|
|
825
|
+
|
|
826
|
+
---
|
|
827
|
+
|
|
828
|
+
## Support
|
|
829
|
+
|
|
830
|
+
- **Email**: hello@chatarmin.com
|
|
831
|
+
- **Documentation**: https://os.chatarmin.com/docs
|
|
832
|
+
|
|
833
|
+
---
|
|
834
|
+
|
|
165
835
|
## License
|
|
166
836
|
|
|
167
837
|
MIT © Chatarmin GmbH
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatarmin/os",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Type-safe SDK for ChatarminOS - Customer & Subscription Management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
7
|
-
"access": "
|
|
7
|
+
"access": "public"
|
|
8
8
|
},
|
|
9
9
|
"main": "./dist/index.js",
|
|
10
10
|
"types": "./dist/index.d.ts",
|