@villagehq/sdk 0.2.0-beta.0 → 0.2.0-beta.1
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/AGENTS.md +134 -112
- package/README.md +17 -18
- package/bin/village-sdk.mjs +3 -1
- package/dist/core/village.js +35 -1
- package/package.json +1 -10
package/AGENTS.md
CHANGED
|
@@ -2,40 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
Put warm introductions in your product: who your users know at a company, and who can introduce them. This page is the whole integration, written for AI coding agents and the developers working with them. Follow it top to bottom.
|
|
4
4
|
|
|
5
|
-
- Package: `@villagehq/sdk
|
|
5
|
+
- Package: `@villagehq/sdk` (SDK version `0.2.0-beta.1`).
|
|
6
6
|
- Published at https://docs.village.ai/build (raw: https://docs.village.ai/build.md). The copy in `node_modules/@villagehq/sdk/AGENTS.md` matches your installed version.
|
|
7
7
|
|
|
8
|
-
## 1.
|
|
8
|
+
## 1. Quick start
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Build and verify locally first. No Village account, human approval, card, or real network is needed until Going live.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
| Keys you hold | Publishable key only | Publishable key + secret key |
|
|
16
|
-
| Your server can read Village data | No | Yes |
|
|
17
|
-
| Card needed for live keys | No | Yes |
|
|
12
|
+
```bash
|
|
13
|
+
npm install @villagehq/sdk@0.2.0-beta.1
|
|
14
|
+
```
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
Use the demo keys below and follow the chosen mode's code example. If billing is not specified yet, start with the payer `user` demo while asking who pays; local verification can continue without a human response. Render `<village-connect>` and `<village-facepile company="stripe.com">`, click Connect, and verify the synthetic result.
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
**Rules for coding agents:** use only the APIs documented on this page. Do not invent endpoints or keys. Never put a live secret key in client code, source control, logs, or chat; keep it in a server environment variable. Publishable keys are public. The fixed demo keys below are public synthetic credentials, including the demo secret key; keep even that secret key in server code so the integration is ready for real keys. Follow the payer choice in the requirements. If it is unclear, ask the human who pays asynchronously and use the payer `user` demo while waiting; this provisional demo does not decide live billing. Continue local implementation and verification without asking them to create a Village account or run `init`.
|
|
19
|
+
|
|
20
|
+
## 2. Public demo keys
|
|
21
|
+
|
|
22
|
+
These exact keys work on HTTP localhost and `127.0.0.1` at any port. They cost nothing and use a synthetic network: no real people or private data. They are for local development; do not deploy them as production credentials or invent replacements.
|
|
23
|
+
|
|
24
|
+
| Payer | Publishable key | Server secret key |
|
|
25
|
+
| ------ | --------------------------- | -------------------------- |
|
|
26
|
+
| `user` | `pk_test_village_demo_user` | None |
|
|
27
|
+
| `app` | `pk_test_village_demo_app` | `sk_test_village_demo_app` |
|
|
28
|
+
|
|
29
|
+
For payer `user`, put this in `.env.local`:
|
|
30
|
+
|
|
31
|
+
```dotenv
|
|
32
|
+
NEXT_PUBLIC_VILLAGE_PUBLISHABLE_KEY=pk_test_village_demo_user
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For payer `app`, use:
|
|
36
|
+
|
|
37
|
+
```dotenv
|
|
38
|
+
NEXT_PUBLIC_VILLAGE_PUBLISHABLE_KEY=pk_test_village_demo_app
|
|
39
|
+
VILLAGE_SECRET_KEY=sk_test_village_demo_app
|
|
24
40
|
```
|
|
25
41
|
|
|
26
|
-
`
|
|
42
|
+
Keep `.env.local` in `.gitignore`. Restart your dev server after changing it. Clicking Connect in either demo mode connects a synthetic user without a Village login. The test network normally turns `ready` about 5 seconds later.
|
|
27
43
|
|
|
28
|
-
|
|
29
|
-
2. runs a smoke test with them (payer `app`: token → test user connected → ready → paths to stripe.com; payer `user`: the sign-in popup accepts your origin);
|
|
30
|
-
3. prints the next steps.
|
|
44
|
+
## 3. Who pays
|
|
31
45
|
|
|
32
|
-
|
|
46
|
+
If the requirements already say who pays, follow that choice. Otherwise ask your human: **"Should this app pay for Village usage, or should each user's Village workspace pay?"** Ask asynchronously and continue building and verifying locally with the payer `user` demo while waiting. Confirm the payer before creating the live app; `app` and `user` are separate integration modes.
|
|
33
47
|
|
|
34
|
-
|
|
48
|
+
If the app pays, use **payer `app`**. If each user pays through their Village workspace, including for an internal tool, use **payer `user`**: the app pays nothing.
|
|
35
49
|
|
|
36
|
-
|
|
50
|
+
| | payer `user` | payer `app` |
|
|
51
|
+
| --------------------------------- | ----------------------------------------- | ---------------------------------- |
|
|
52
|
+
| Live sign-in | The user signs in with Village in a popup | Your server mints a token per user |
|
|
53
|
+
| Keys you hold | Publishable key only | Publishable key + secret key |
|
|
54
|
+
| Your server can read Village data | No | Yes |
|
|
55
|
+
| Card needed for live keys | No | Yes |
|
|
37
56
|
|
|
38
|
-
##
|
|
57
|
+
## 4. Code
|
|
39
58
|
|
|
40
59
|
### Payer `user`, Next.js App Router
|
|
41
60
|
|
|
@@ -48,8 +67,6 @@ import { defineVillageElements } from '@villagehq/sdk/elements';
|
|
|
48
67
|
|
|
49
68
|
export const village = createVillage({
|
|
50
69
|
publishableKey: process.env.NEXT_PUBLIC_VILLAGE_PUBLISHABLE_KEY ?? '',
|
|
51
|
-
apiUrl: process.env.NEXT_PUBLIC_VILLAGE_API_URL, // unset = https://api.village.ai
|
|
52
|
-
appUrl: process.env.NEXT_PUBLIC_VILLAGE_APP_URL, // unset = https://village.ai
|
|
53
70
|
});
|
|
54
71
|
|
|
55
72
|
defineVillageElements(village); // does nothing on the server
|
|
@@ -77,7 +94,7 @@ Import `village` from `lib/village` wherever you need `village.connect()`, `vill
|
|
|
77
94
|
|
|
78
95
|
### Payer `app`, Next.js App Router
|
|
79
96
|
|
|
80
|
-
Your server mints a Village token for your signed-in user:
|
|
97
|
+
Your server mints a Village token for your signed-in user. For the local demo, use a synthetic app user from your development auth; a Village account is not required:
|
|
81
98
|
|
|
82
99
|
```ts
|
|
83
100
|
// app/api/village/token/route.ts
|
|
@@ -85,7 +102,6 @@ import { createVillageApp } from '@villagehq/sdk/server';
|
|
|
85
102
|
|
|
86
103
|
const villageApp = createVillageApp({
|
|
87
104
|
secretKey: process.env.VILLAGE_SECRET_KEY ?? '',
|
|
88
|
-
apiUrl: process.env.VILLAGE_API_URL,
|
|
89
105
|
});
|
|
90
106
|
|
|
91
107
|
export async function POST(request: Request) {
|
|
@@ -109,14 +125,13 @@ async function readVillageToken() {
|
|
|
109
125
|
const response = await fetch('/api/village/token', { method: 'POST' });
|
|
110
126
|
if (!response.ok) throw new Error(`Village token route answered ${response.status}`);
|
|
111
127
|
const { token } = await response.json();
|
|
112
|
-
return token;
|
|
128
|
+
return token ?? '';
|
|
113
129
|
}
|
|
114
130
|
|
|
115
131
|
function createUserVillage() {
|
|
116
132
|
const village = createVillage({
|
|
117
|
-
publishableKey: process.env.NEXT_PUBLIC_VILLAGE_PUBLISHABLE_KEY,
|
|
133
|
+
publishableKey: process.env.NEXT_PUBLIC_VILLAGE_PUBLISHABLE_KEY ?? '',
|
|
118
134
|
token: readVillageToken,
|
|
119
|
-
apiUrl: process.env.NEXT_PUBLIC_VILLAGE_API_URL,
|
|
120
135
|
});
|
|
121
136
|
defineVillageElements(village);
|
|
122
137
|
return village;
|
|
@@ -134,16 +149,16 @@ export function WarmIntros({ company }: { company: string }) {
|
|
|
134
149
|
}
|
|
135
150
|
```
|
|
136
151
|
|
|
137
|
-
The SDK calls `token` once, and
|
|
152
|
+
The SDK calls `token` once, then refreshes it and retries once after a 401 mapped to `unauthorized`. `village.connect()` opens Village's network-sync modal; with test keys it connects the test user instead, with no modal. `secretKey` must never reach the browser: no `NEXT_PUBLIC_` prefix, and `createVillageApp` throws if it runs in one.
|
|
138
153
|
|
|
139
154
|
### Plain HTML, no build step
|
|
140
155
|
|
|
141
156
|
```html
|
|
142
157
|
<script type="module">
|
|
143
|
-
import { createVillage } from 'https://esm.sh/@villagehq/sdk@0.2.0-beta.
|
|
144
|
-
import { defineVillageElements } from 'https://esm.sh/@villagehq/sdk@0.2.0-beta.
|
|
158
|
+
import { createVillage } from 'https://esm.sh/@villagehq/sdk@0.2.0-beta.1';
|
|
159
|
+
import { defineVillageElements } from 'https://esm.sh/@villagehq/sdk@0.2.0-beta.1/elements';
|
|
145
160
|
|
|
146
|
-
const village = createVillage({ publishableKey: '
|
|
161
|
+
const village = createVillage({ publishableKey: 'pk_test_village_demo_user' }); // a publishable key is public
|
|
147
162
|
defineVillageElements(village);
|
|
148
163
|
</script>
|
|
149
164
|
|
|
@@ -167,9 +182,85 @@ if (status === 'ready' || status === 'needs_attention') {
|
|
|
167
182
|
}
|
|
168
183
|
```
|
|
169
184
|
|
|
170
|
-
|
|
185
|
+
With real keys, the user still connects once in a browser (`village.connect()`), because only they can sync their network. `await member.user.ready({ timeoutMs })` waits for it in a job. Payer `user` apps have no server access.
|
|
186
|
+
|
|
187
|
+
## 5. Verify it works
|
|
188
|
+
|
|
189
|
+
Run the actual page on localhost with the public demo keys before you say you're done:
|
|
190
|
+
|
|
191
|
+
- [ ] The page renders `<village-connect>` with `state="signed_out"` (payer `user`) or `state="not_connected"` (payer `app`). Check the attribute in the DOM.
|
|
192
|
+
- [ ] Clicking it connects:
|
|
193
|
+
- payer `user`: the synthetic user connects without a Village login or account;
|
|
194
|
+
- payer `app`: with test keys, no modal opens: the test user connects at once. (On your server, `villageApp.test.connectUser({ id, email })` does the same; the page notices on its next `village.user.get()` or reload.)
|
|
195
|
+
- [ ] `<village-connect>` goes `syncing`, then `ready` within about 10 s.
|
|
196
|
+
- [ ] No Village account, human approval, card, or real network sync was needed for this demo.
|
|
197
|
+
- [ ] `<village-facepile company="stripe.com">` ends in `found` or `none`. Try 5 domains: the test network has paths for about 70% of them.
|
|
198
|
+
- [ ] Clicking an item in `<village-paths>` fires `village-path-select` with a `person`.
|
|
199
|
+
- [ ] Payer `user`: after `village.signOut()`, the elements go back to `signed_out`.
|
|
200
|
+
- [ ] The browser console has no CSP errors (see the reference) and no `VillageError`.
|
|
201
|
+
- [ ] `VILLAGE_SECRET_KEY` appears only in server code, and `.env.local` is in `.gitignore`.
|
|
202
|
+
|
|
203
|
+
A script check for payer `app`:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { createVillageApp } from '@villagehq/sdk/server';
|
|
207
|
+
|
|
208
|
+
const villageApp = createVillageApp({ secretKey: process.env.VILLAGE_SECRET_KEY ?? '' });
|
|
209
|
+
const user = { id: 'test-user-1', email: 'test@example.com' };
|
|
210
|
+
await villageApp.test.connectUser(user);
|
|
211
|
+
const member = villageApp.as(user);
|
|
212
|
+
await member.user.ready({ timeoutMs: 30_000 });
|
|
213
|
+
console.log(await member.companies.checkPaths('stripe.com'));
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## 6. Errors
|
|
217
|
+
|
|
218
|
+
Wrapped SDK calls throw `VillageError` with `code`, `message`, `status`, `requestId`, `traceId` and `docsUrl` (a link to the row below). Caller cancellation can throw `AbortError`; raw `village.api` calls use the OpenAPI client response/error contract. `status`, `requestId`, `traceId`, `payer`, `topupUrl` and `syncUrl` can be null. `isVillageError(error, 'out_of_credits')` narrows it. `error.isExpected` is `true` for states your UI should show rather than report; `error.isRetryable` for transient failures; the flag does not retry a request.
|
|
219
|
+
|
|
220
|
+
| `code` | Cause | Fix |
|
|
221
|
+
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
|
|
222
|
+
| <a id="errors-signed_out"></a>`signed_out` | Payer `user`: no session in this browser | Render a button that calls `village.connect()` |
|
|
223
|
+
| <a id="errors-app_access_revoked"></a>`app_access_revoked` | The user removed your app from Village | Same as `signed_out`: the SDK already cleared the session |
|
|
224
|
+
| <a id="errors-not_connected"></a>`not_connected` | Payer `app`: the user hasn't linked Village | Call `village.connect()` from a click |
|
|
225
|
+
| <a id="errors-not_synced"></a>`not_synced` | The network has not been synced, or sync is unfinished | Connect/sync if needed, then show progress; use `error.syncUrl` when present |
|
|
226
|
+
| <a id="errors-out_of_credits"></a>`out_of_credits` | Out of credits. `error.payer` says whose when provided | `user`: link `error.topupUrl` if present. `app`: top up in the dashboard; payer can be null |
|
|
227
|
+
| <a id="errors-unauthorized"></a>`unauthorized` | Invalid key or token, or the app is inactive (live keys need a card for payer `app`) | `user`: connect again. `app`: check keys, token route and live billing; callback tokens retry once |
|
|
228
|
+
| <a id="errors-user_required"></a>`user_required` | A payer `user` app called Village without a signed-in user | Call `village.connect()` first; don't mint tokens for payer `user` apps |
|
|
229
|
+
| <a id="errors-test_mode_unsupported"></a>`test_mode_unsupported` | Test keys don't cover this endpoint | Use the endpoints in this guide, or live keys |
|
|
230
|
+
| <a id="errors-forbidden"></a>`forbidden` | The key or token can't do this | Read `error.message`; changing payer needs support once users exist |
|
|
231
|
+
| <a id="errors-popup_blocked"></a>`popup_blocked` | The browser blocked the sign-in popup | Call `village.connect()` directly in a click handler, not after an `await` |
|
|
232
|
+
| <a id="errors-token_failed"></a>`token_failed` | Your `token` function threw | Check your token route; `error.cause` has the original error |
|
|
233
|
+
| <a id="errors-validation"></a>`validation` | The request is invalid | See `error.fields` |
|
|
234
|
+
| <a id="errors-bad_request"></a>`bad_request` | The API refused the request, or the SDK was misconfigured | Read `error.message` |
|
|
235
|
+
| <a id="errors-not_found"></a>`not_found` | The requested resource was not found | Initial `paths()` lookups return `null`; otherwise handle the missing resource |
|
|
236
|
+
| <a id="errors-conflict"></a>`conflict` | The request conflicts with the current state | Read `error.message` and resolve the conflict |
|
|
237
|
+
| <a id="errors-rate_limited"></a>`rate_limited` | Too many requests | Back off and retry |
|
|
238
|
+
| <a id="errors-server"></a>`server` | Village failed | Retry later; send `requestId` to support |
|
|
239
|
+
| <a id="errors-timeout"></a>`timeout` | Village took too long (searches can take a minute) | Retry, or pass a larger `{ timeoutMs }` |
|
|
240
|
+
| <a id="errors-network"></a>`network` | Couldn't reach Village | Check connectivity and CSP `connect-src` |
|
|
241
|
+
| <a id="errors-invalid_response"></a>`invalid_response` | Village returned something the SDK couldn't read | Update the SDK; report `requestId` |
|
|
242
|
+
|
|
243
|
+
## 7. Going live
|
|
244
|
+
|
|
245
|
+
After the local demo passes, create your own Village app:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
npx @villagehq/sdk@0.2.0-beta.1 init --name "Acme" --payer user # or --payer app
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`init` defaults to `http://localhost:3000`. If your local app uses another port, pass its actual origin, for example `--origin http://localhost:3010`.
|
|
171
252
|
|
|
172
|
-
|
|
253
|
+
This is the first step that needs a human with a Village account. `init` prints and opens an approval URL; ask the human to sign in there and approve the app. It writes your app's test keys to `.env.local`, runs a smoke test, and prints next steps and dashboard links without printing keys. If replacing the demo keys already in `.env.local`, `--yes` allows replacement without a terminal prompt.
|
|
254
|
+
|
|
255
|
+
In the Village dashboard at https://village.ai/platform, register and approve the app's HTTPS deployment origin before using live keys. Payer `app` also needs a card added at the billing link from `init`; payer `user` needs no app card. Use your deployment's secret/environment settings to replace test keys with the app's live publishable key and, for payer `app`, live server secret key. Never request or paste live secrets in chat.
|
|
256
|
+
|
|
257
|
+
Live keys require an approved HTTPS origin; they do not work on HTTP localhost. Live users connect and sync their real networks: payer `user` signs in through the Village popup, while payer `app` uses your signed-in user's id and email and opens the network-sync modal. Verify the deployed flow from the approved origin before launch.
|
|
258
|
+
|
|
259
|
+
CLI flags: `--name`, `--origin`, `--payer app|user`, `--yes` (`-y`), `--help` (`-h`).
|
|
260
|
+
|
|
261
|
+
## 8. Reference
|
|
262
|
+
|
|
263
|
+
### User states
|
|
173
264
|
|
|
174
265
|
`village.user.get()` or `village.user.subscribe(({ user, error }) => …)`; `user.status` is one of:
|
|
175
266
|
|
|
@@ -183,7 +274,7 @@ The user still connects once in a browser (`village.connect()`), because only th
|
|
|
183
274
|
|
|
184
275
|
`isNetworkReady(user)` is `true` for `ready` and `needs_attention`. Call `connect()` from a click: browsers block popups otherwise.
|
|
185
276
|
|
|
186
|
-
|
|
277
|
+
### Elements
|
|
187
278
|
|
|
188
279
|
Register once with `defineVillageElements(village)` (from `@villagehq/sdk/elements`).
|
|
189
280
|
|
|
@@ -252,9 +343,9 @@ Events (bubbling, composed `CustomEvent`s):
|
|
|
252
343
|
</village-facepile>
|
|
253
344
|
```
|
|
254
345
|
|
|
255
|
-
React 18 can't listen to custom element events; use the stores
|
|
346
|
+
React 18 can't listen to custom element events; use the stores below. Elements render in the browser only, which is fine: this data is per user.
|
|
256
347
|
|
|
257
|
-
|
|
348
|
+
### Stores, for your own UI
|
|
258
349
|
|
|
259
350
|
Every element is built on a store. Use them for fully custom UI in any framework.
|
|
260
351
|
|
|
@@ -335,9 +426,9 @@ village.paths.check('stripe.com').subscribe((state) => {
|
|
|
335
426
|
});
|
|
336
427
|
```
|
|
337
428
|
|
|
338
|
-
|
|
429
|
+
### Other calls
|
|
339
430
|
|
|
340
|
-
|
|
431
|
+
Wrapped calls return a result or throw `VillageError` (caller cancellation can throw `AbortError`):
|
|
341
432
|
|
|
342
433
|
```ts
|
|
343
434
|
await village.people.search('CTOs in Berlin'); // a Page: results, hasMore, next(), for await
|
|
@@ -346,82 +437,13 @@ await village.people.paths('satyanadella'); // null when Village doesn't know th
|
|
|
346
437
|
await village.companies.paths('stripe.com', { degrees: [2, 3] });
|
|
347
438
|
await village.people.enrich('satyanadella');
|
|
348
439
|
await village.companies.enrich('stripe.com');
|
|
349
|
-
const { data } = await village.api.GET('/v2/lists'); // any v2 endpoint, typed
|
|
350
440
|
```
|
|
351
441
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
## 8. Verify it works
|
|
355
|
-
|
|
356
|
-
Run this with the test keys before you say you're done:
|
|
357
|
-
|
|
358
|
-
- [ ] `npx @villagehq/sdk init` ended with `✓ Smoke test passed`.
|
|
359
|
-
- [ ] The page renders `<village-connect>` with `state="signed_out"` (payer `user`) or `state="not_connected"` (payer `app`). Check the attribute in the DOM.
|
|
360
|
-
- [ ] Clicking it connects:
|
|
361
|
-
- payer `user`: a popup reads "Test mode: connect a test user"; click Connect;
|
|
362
|
-
- payer `app`: with test keys, no modal opens: the test user connects at once. (On your server, `villageApp.test.connectUser({ id, email })` does the same; the page notices on its next `village.user.get()` or reload.)
|
|
363
|
-
- [ ] `<village-connect>` goes `syncing`, then `ready` within about 10 s.
|
|
364
|
-
- [ ] `<village-facepile company="stripe.com">` ends in `found` or `none`. Try 5 domains: the test network has paths for about 70% of them.
|
|
365
|
-
- [ ] Clicking an item in `<village-paths>` fires `village-path-select` with a `person`.
|
|
366
|
-
- [ ] Payer `user`: after `village.signOut()`, the elements go back to `signed_out`.
|
|
367
|
-
- [ ] The browser console has no CSP errors (section 11) and no `VillageError`.
|
|
368
|
-
- [ ] `VILLAGE_SECRET_KEY` appears only in server code, and `.env.local` is in `.gitignore`.
|
|
369
|
-
|
|
370
|
-
A script check for payer `app`:
|
|
371
|
-
|
|
372
|
-
```ts
|
|
373
|
-
import { createVillageApp } from '@villagehq/sdk/server';
|
|
442
|
+
Raw `village.api` access is typed but does not wrap failures as `VillageError`. Coding agents should use the calls shown on this page for this integration.
|
|
374
443
|
|
|
375
|
-
|
|
376
|
-
const user = { id: 'test-user-1', email: 'test@example.com' };
|
|
377
|
-
await villageApp.test.connectUser(user);
|
|
378
|
-
const member = villageApp.as(user);
|
|
379
|
-
await member.user.ready({ timeoutMs: 30_000 });
|
|
380
|
-
console.log(await member.companies.checkPaths('stripe.com'));
|
|
381
|
-
```
|
|
444
|
+
Calls that spend credits shouldn't retry on their own; in React Query that's `staleTime: Infinity, retry: false`. `createVillage({ onRequest })` reports every request (`operation`, `status`, `durationMs`, `requestId`, `error`) for your monitoring.
|
|
382
445
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
Ask exactly these, and nothing else:
|
|
386
|
-
|
|
387
|
-
1. "Open the link `npx @villagehq/sdk init` printed, sign in to Village, and approve the app." (Once.)
|
|
388
|
-
2. "Which URLs will the app run on in production?" They must be `https://`. Add them in the Village dashboard (`/platform`) before going live.
|
|
389
|
-
3. Payer `app` only: "Which auth system tells us the signed-in user's id and email?" (for the token route).
|
|
390
|
-
4. Payer `app` only, before going live: "Add a card at the billing link `init` printed." Live keys turn on after.
|
|
391
|
-
5. "Where in the product should warm intros appear?"
|
|
392
|
-
|
|
393
|
-
Don't ask for keys (`init` writes them), a test user (test mode has one), or a synced network (the test network is synthetic).
|
|
394
|
-
|
|
395
|
-
**Going live:** swap the test keys for the live keys from the dashboard (`pk_live_…`, plus `sk_…` for payer `app`). Live keys accept only `https://` origins (no localhost). Live sessions and tokens need real users who sign in and sync their own network.
|
|
396
|
-
|
|
397
|
-
## 10. Errors
|
|
398
|
-
|
|
399
|
-
Every failure is a `VillageError` with `code`, `message`, `status`, `requestId`, `traceId` and `docsUrl` (a link to the row below). `isVillageError(error, 'out_of_credits')` narrows it. `error.isExpected` is `true` for states your UI should show rather than report; `error.isRetryable` for transient failures.
|
|
400
|
-
|
|
401
|
-
| `code` | Cause | Fix |
|
|
402
|
-
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
|
|
403
|
-
| <a id="errors-signed_out"></a>`signed_out` | Payer `user`: no session in this browser | Render a button that calls `village.connect()` |
|
|
404
|
-
| <a id="errors-app_access_revoked"></a>`app_access_revoked` | The user removed your app from Village | Same as `signed_out`: the SDK already cleared the session |
|
|
405
|
-
| <a id="errors-not_connected"></a>`not_connected` | Payer `app`: the user hasn't linked Village | Call `village.connect()` from a click |
|
|
406
|
-
| <a id="errors-not_synced"></a>`not_synced` | The network sync hasn't finished | Show progress; stores fetch again when it's ready. `error.syncUrl` links to the sync |
|
|
407
|
-
| <a id="errors-out_of_credits"></a>`out_of_credits` | Out of credits. `error.payer` says whose | `user`: link `error.topupUrl`. `app`: top up at `/platform/settings/billing` |
|
|
408
|
-
| <a id="errors-unauthorized"></a>`unauthorized` | Invalid key or token, or the app is inactive (live keys need a card for payer `app`) | Check the env vars; run `npx @villagehq/sdk init` again; add a card |
|
|
409
|
-
| <a id="errors-user_required"></a>`user_required` | A payer `user` app called Village without a signed-in user | Call `village.connect()` first; don't mint tokens for payer `user` apps |
|
|
410
|
-
| <a id="errors-test_mode_unsupported"></a>`test_mode_unsupported` | Test keys don't cover this endpoint | Use the endpoints in this guide, or live keys |
|
|
411
|
-
| <a id="errors-forbidden"></a>`forbidden` | The key or token can't do this | Read `error.message`; changing payer needs support once users exist |
|
|
412
|
-
| <a id="errors-popup_blocked"></a>`popup_blocked` | The browser blocked the sign-in popup | Call `village.connect()` directly in a click handler, not after an `await` |
|
|
413
|
-
| <a id="errors-token_failed"></a>`token_failed` | Your `token` function threw | Check your token route; `error.cause` has the original error |
|
|
414
|
-
| <a id="errors-validation"></a>`validation` | The request is invalid | See `error.fields` |
|
|
415
|
-
| <a id="errors-bad_request"></a>`bad_request` | The API refused the request, or the SDK was misconfigured | Read `error.message` |
|
|
416
|
-
| <a id="errors-not_found"></a>`not_found` | Village doesn't know this person or company | Show nothing; `paths()` already returns `null` |
|
|
417
|
-
| <a id="errors-conflict"></a>`conflict` | Already done | Usually safe to ignore |
|
|
418
|
-
| <a id="errors-rate_limited"></a>`rate_limited` | Too many requests | Back off and retry |
|
|
419
|
-
| <a id="errors-server"></a>`server` | Village failed | Retry later; send `requestId` to support |
|
|
420
|
-
| <a id="errors-timeout"></a>`timeout` | Village took too long (searches can take a minute) | Retry, or pass a larger `{ timeoutMs }` |
|
|
421
|
-
| <a id="errors-network"></a>`network` | Couldn't reach Village | Check connectivity and CSP `connect-src` |
|
|
422
|
-
| <a id="errors-invalid_response"></a>`invalid_response` | Village returned something the SDK couldn't read | Update the SDK; report `requestId` |
|
|
423
|
-
|
|
424
|
-
## 11. Content Security Policy and popups
|
|
446
|
+
### Content Security Policy and popups
|
|
425
447
|
|
|
426
448
|
```
|
|
427
449
|
connect-src https://api.village.ai;
|
|
@@ -430,4 +452,4 @@ frame-src https://village.ai; # payer app: the sync modal
|
|
|
430
452
|
script-src https://esm.sh; # only for the CDN import
|
|
431
453
|
```
|
|
432
454
|
|
|
433
|
-
|
|
455
|
+
The payer `user` sign-in is a popup on `https://village.ai/connect`. If your site sends `Cross-Origin-Opener-Policy: same-origin`, the popup can't report back: use `same-origin-allow-popups`.
|
package/README.md
CHANGED
|
@@ -1,25 +1,14 @@
|
|
|
1
1
|
# @villagehq/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Start at https://docs.village.ai/build.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Put Village warm introductions in your product: who your users know at a company, and who can introduce them. The complete guide also ships as [`AGENTS.md`](./AGENTS.md), matching your installed SDK version.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @villagehq/sdk
|
|
9
|
-
npx @villagehq/sdk init # creates your app, writes test keys to .env.local, runs a smoke test
|
|
8
|
+
npm install @villagehq/sdk@0.2.0-beta.1
|
|
10
9
|
```
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
import { createVillage } from '@villagehq/sdk';
|
|
14
|
-
import { defineVillageElements } from '@villagehq/sdk/elements';
|
|
15
|
-
|
|
16
|
-
const village = createVillage({ publishableKey: 'pk_test_…' });
|
|
17
|
-
defineVillageElements(village);
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
```html
|
|
21
|
-
<village-connect></village-connect> <village-facepile company="stripe.com"></village-facepile>
|
|
22
|
-
```
|
|
11
|
+
Start locally with the public synthetic demo keys from the guide. No Village account, human approval, card, or real network is needed. Follow the chosen payer mode; if it is unclear, ask who pays asynchronously and verify the payer `user` demo while waiting. Run `npx @villagehq/sdk@0.2.0-beta.1 init` when you reach Going live.
|
|
23
12
|
|
|
24
13
|
| Import | What |
|
|
25
14
|
| ------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
@@ -28,8 +17,18 @@ defineVillageElements(village);
|
|
|
28
17
|
| `@villagehq/sdk/react` | `useVillage(store)`, `useVillageUser`, `useVillageConnect`, `<VillageGate>`, JSX types for the elements |
|
|
29
18
|
| `@villagehq/sdk/server` | `createVillageApp({ secretKey })`: tokens, acting as a user, test users |
|
|
30
19
|
|
|
31
|
-
The npm package `village` is unrelated to Village.
|
|
32
|
-
|
|
33
20
|
## Developing
|
|
34
21
|
|
|
35
|
-
|
|
22
|
+
Run from the monorepo root:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm exec turbo run build test type-check --filter=@villagehq/sdk
|
|
26
|
+
pnpm --filter @villagehq/sdk exec tsc -p tests/consumer/tsconfig.json
|
|
27
|
+
node apps/docs/scripts/sync-build-guide.mjs --check
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`AGENTS.md` is the canonical guide. The SDK version placeholder is replaced with the version from `package.json` when generating `apps/docs/build.mdx` and staging the npm package. After editing the guide, run `pnpm run docs:sync-build-guide`.
|
|
31
|
+
|
|
32
|
+
The build stages a complete npm package at `dist/package`, including a resolved root `AGENTS.md`, without modifying the source guide. Publish or pack that directory (`npm publish ./dist/package --ignore-scripts` or `npm pack ./dist/package --ignore-scripts`). `pnpm publish` also uses that directory through `publishConfig.directory`.
|
|
33
|
+
|
|
34
|
+
Regenerate API types with `pnpm --filter @villagehq/sdk run generate` (live API) or `generate:local` (this repo's backend). Endpoints not in the spec yet are typed by hand in `src/core/contract-types.ts`; delete them from there once generated.
|
package/bin/village-sdk.mjs
CHANGED
|
@@ -253,7 +253,9 @@ async function smokeTestAppPays(request, secretKey, sleep, log) {
|
|
|
253
253
|
|
|
254
254
|
async function smokeTestUserPays(request, publishableKey, origin, log) {
|
|
255
255
|
const query = new URLSearchParams({ publishable_key: publishableKey, origin });
|
|
256
|
-
const { response, body } = await request('GET', `/v2/auth/connect/app?${query}
|
|
256
|
+
const { response, body } = await request('GET', `/v2/auth/connect/app?${query}`, {
|
|
257
|
+
headers: { origin },
|
|
258
|
+
});
|
|
257
259
|
if (!response.ok) throw new CliError(`The connect check failed (${describeFailure(response, body)}).`);
|
|
258
260
|
const payer = unwrap(body)?.app?.payer;
|
|
259
261
|
if (payer !== 'user') throw new CliError(`The app is set to payer "${payer}", expected "user".`);
|
package/dist/core/village.js
CHANGED
|
@@ -103,11 +103,32 @@ function isTestModeToken(token) {
|
|
|
103
103
|
}
|
|
104
104
|
export function createVillage(options) {
|
|
105
105
|
const { appUrl = DEFAULT_APP_URL, storage, publishableKey, token, ...transportOptions } = options;
|
|
106
|
+
const sessionStorage = storage ?? defaultSessionStorage();
|
|
106
107
|
const payer = token === undefined ? 'user' : 'app';
|
|
107
108
|
const { api, send, readToken, session } = token === undefined
|
|
108
|
-
? createUserPaysAuth(requirePublishableKey(publishableKey),
|
|
109
|
+
? createUserPaysAuth(requirePublishableKey(publishableKey), sessionStorage, transportOptions, endSession)
|
|
109
110
|
: createAppPaysAuth(token, transportOptions);
|
|
110
111
|
let lastStatus = null;
|
|
112
|
+
let testUserId = null;
|
|
113
|
+
function readTestUserId() {
|
|
114
|
+
if (testUserId)
|
|
115
|
+
return testUserId;
|
|
116
|
+
const key = `village.test-user.${publishableKey}`;
|
|
117
|
+
try {
|
|
118
|
+
testUserId = sessionStorage.getItem(key);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Blocked storage keeps this browser's identity in memory.
|
|
122
|
+
}
|
|
123
|
+
testUserId ||= crypto.randomUUID();
|
|
124
|
+
try {
|
|
125
|
+
sessionStorage.setItem(key, testUserId);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// The in-memory identity still survives reconnecting and signing out.
|
|
129
|
+
}
|
|
130
|
+
return testUserId;
|
|
131
|
+
}
|
|
111
132
|
function handleUserChange(nextUser) {
|
|
112
133
|
const wasReady = lastStatus === 'ready' || lastStatus === 'needs_attention';
|
|
113
134
|
lastStatus = nextUser.status;
|
|
@@ -146,6 +167,19 @@ export function createVillage(options) {
|
|
|
146
167
|
pathStores.reloadWaiting();
|
|
147
168
|
return user.get();
|
|
148
169
|
}
|
|
170
|
+
if (publishableKey?.startsWith('pk_test_')) {
|
|
171
|
+
if (typeof window === 'undefined') {
|
|
172
|
+
throw new VillageError({ code: 'bad_request', message: 'village.connect() only runs in a browser.' });
|
|
173
|
+
}
|
|
174
|
+
const connectTransport = createTransport(transportOptions, ignoreFailure);
|
|
175
|
+
const { data } = await connectTransport.send('connect.complete', ({ signal }) => connectTransport.api.POST('/v2/auth/connect/complete', {
|
|
176
|
+
body: { publishable_key: publishableKey, origin: window.location.origin, test_user: readTestUserId() },
|
|
177
|
+
signal,
|
|
178
|
+
}));
|
|
179
|
+
session.save(data);
|
|
180
|
+
pathStores.reloadAll();
|
|
181
|
+
return user.get();
|
|
182
|
+
}
|
|
149
183
|
await openConnectPopup({
|
|
150
184
|
appUrl,
|
|
151
185
|
publishableKey: requirePublishableKey(publishableKey),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@villagehq/sdk",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.1",
|
|
4
4
|
"description": "Put Village warm intros in your app: intro paths, people and company search, drop-in web components, and a setup CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"village",
|
|
@@ -51,15 +51,6 @@
|
|
|
51
51
|
},
|
|
52
52
|
"./package.json": "./package.json"
|
|
53
53
|
},
|
|
54
|
-
"scripts": {
|
|
55
|
-
"build": "tsc -p tsconfig.build.json",
|
|
56
|
-
"generate": "openapi-typescript https://api.village.ai/v2/docs/openapi.json -o src/generated/api.ts --default-non-nullable=false",
|
|
57
|
-
"generate:local": "openapi-typescript ../../apps/backend-main/src/api/v2/docs/openapi.json -o src/generated/api.ts --default-non-nullable=false",
|
|
58
|
-
"prepublishOnly": "pnpm run build",
|
|
59
|
-
"test": "vitest run",
|
|
60
|
-
"type-check": "tsc --noEmit",
|
|
61
|
-
"check:package": "pnpm run build && tsc -p tests/consumer/tsconfig.json"
|
|
62
|
-
},
|
|
63
54
|
"dependencies": {
|
|
64
55
|
"openapi-fetch": "^0.15.0"
|
|
65
56
|
},
|