@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 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`. The npm package `village` belongs to another company; never install it.
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. Pick who pays
8
+ ## 1. Quick start
9
9
 
10
- Does your app pay for its users' Village usage? Then **payer `app`**. Otherwise, including internal tools, **payer `user`**: each user's Village workspace pays, and you pay nothing.
10
+ Build and verify locally first. No Village account, human approval, card, or real network is needed until Going live.
11
11
 
12
- | | payer `user` | payer `app` |
13
- | --------------------------------- | ----------------------------------------- | ---------------------------------- |
14
- | Sign-in | The user signs in with Village in a popup | Your server mints a token per user |
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
- ## 2. Set up
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
- ```bash
22
- npm install @villagehq/sdk
23
- npx @villagehq/sdk init --name "Acme" --origin http://localhost:3000 --payer user # or --payer app
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
- `init` prints a URL and opens it. Your human signs in to Village and approves the app. Then `init`:
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
- 1. writes test keys to `.env.local`: `VILLAGE_PUBLISHABLE_KEY` and `NEXT_PUBLIC_VILLAGE_PUBLISHABLE_KEY`, plus `VILLAGE_SECRET_KEY` for payer `app`;
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
- It never prints a key. Don't paste keys into chat; read them from the environment.
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
- Flags: `--name`, `--origin`, `--payer app|user`, `--api-url`, `--app-url`, `--yes` (replace keys already in `.env.local`). For staging: `--api-url https://api-stg.village.ai --app-url https://staging.village.ai`, which also writes `VILLAGE_API_URL`, `NEXT_PUBLIC_VILLAGE_API_URL` and `NEXT_PUBLIC_VILLAGE_APP_URL`.
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
- **Test keys** (`pk_test_…`, `sk_test_…`) work at once, cost nothing, need no card, accept `http://localhost:*` and `http://127.0.0.1:*`, and run against a synthetic network: no real people, no real data. A test user's network turns `ready` about 5 s after connecting.
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
- ## 3. Code
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 again after a 401. `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.
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.0';
144
- import { defineVillageElements } from 'https://esm.sh/@villagehq/sdk@0.2.0-beta.0/elements';
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: 'pk_test_…' }); // a publishable key is public
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
- 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.
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
- ## 4. User states
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
- ## 5. Elements
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 (section 6). Elements render in the browser only, which is fine: this data is per user.
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
- ## 6. Stores, for your own UI
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
- ## 7. Other calls
429
+ ### Other calls
339
430
 
340
- Each returns once, and throws a `VillageError`:
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
- 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.
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
- const villageApp = createVillageApp({ secretKey: process.env.VILLAGE_SECRET_KEY ?? '' });
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
- ## 9. What to ask your human
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
- Staging: `https://api-stg.village.ai` and `https://staging.village.ai`. 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`.
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
- Put Village warm introductions in your product: who your users know at a company, and who can introduce them.
3
+ Start at https://docs.village.ai/build.
4
4
 
5
- **Building an integration? Follow the guide: https://docs.village.ai/build** (also shipped as [`AGENTS.md`](./AGENTS.md) in this package, matching your installed version).
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
- ```ts
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
- `pnpm test`, `pnpm type-check`, and `pnpm check:package` (builds, then type-checks a consumer against the output). Regenerate API types with `pnpm run generate` (live API) or `pnpm run 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 they're generated.
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.
@@ -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".`);
@@ -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), storage ?? defaultSessionStorage(), transportOptions, endSession)
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.0",
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
  },