@villagehq/sdk 0.0.0-stage → 0.2.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/AGENTS.md +433 -0
  2. package/LICENSE +21 -0
  3. package/README.md +34 -2
  4. package/bin/village-sdk.d.mts +15 -0
  5. package/bin/village-sdk.mjs +380 -0
  6. package/dist/core/api-types.d.ts +7 -0
  7. package/dist/core/api-types.js +1 -0
  8. package/dist/core/connect-popup.d.ts +8 -0
  9. package/dist/core/connect-popup.js +85 -0
  10. package/dist/core/contract-types.d.ts +41 -0
  11. package/dist/core/contract-types.js +1 -0
  12. package/dist/core/display.d.ts +20 -0
  13. package/dist/core/display.js +27 -0
  14. package/dist/core/errors.d.ts +35 -0
  15. package/dist/core/errors.js +116 -0
  16. package/dist/core/identifiers.d.ts +16 -0
  17. package/dist/core/identifiers.js +65 -0
  18. package/dist/core/pages.d.ts +23 -0
  19. package/dist/core/pages.js +28 -0
  20. package/dist/core/paths-stores.d.ts +86 -0
  21. package/dist/core/paths-stores.js +132 -0
  22. package/dist/core/session.d.ts +20 -0
  23. package/dist/core/session.js +121 -0
  24. package/dist/core/store.d.ts +18 -0
  25. package/dist/core/store.js +56 -0
  26. package/dist/core/sync-modal.d.ts +6 -0
  27. package/dist/core/sync-modal.js +90 -0
  28. package/dist/core/transport.d.ts +43 -0
  29. package/dist/core/transport.js +131 -0
  30. package/dist/core/user.d.ts +31 -0
  31. package/dist/core/user.js +130 -0
  32. package/dist/core/village.d.ts +214 -0
  33. package/dist/core/village.js +297 -0
  34. package/dist/elements/elements.d.ts +31 -0
  35. package/dist/elements/elements.js +303 -0
  36. package/dist/generated/api.d.ts +10203 -0
  37. package/dist/generated/api.js +5 -0
  38. package/dist/react/react.d.ts +53 -0
  39. package/dist/react/react.js +42 -0
  40. package/dist/server/server.d.ts +208 -0
  41. package/dist/server/server.js +62 -0
  42. package/package.json +82 -4
package/AGENTS.md ADDED
@@ -0,0 +1,433 @@
1
+ # Build on Village
2
+
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
+
5
+ - Package: `@villagehq/sdk`. The npm package `village` belongs to another company; never install it.
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
+
8
+ ## 1. Pick who pays
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.
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 |
18
+
19
+ ## 2. Set up
20
+
21
+ ```bash
22
+ npm install @villagehq/sdk
23
+ npx @villagehq/sdk init --name "Acme" --origin http://localhost:3000 --payer user # or --payer app
24
+ ```
25
+
26
+ `init` prints a URL and opens it. Your human signs in to Village and approves the app. Then `init`:
27
+
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.
31
+
32
+ It never prints a key. Don't paste keys into chat; read them from the environment.
33
+
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`.
35
+
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.
37
+
38
+ ## 3. Code
39
+
40
+ ### Payer `user`, Next.js App Router
41
+
42
+ ```ts
43
+ // lib/village.ts
44
+ 'use client';
45
+
46
+ import { createVillage } from '@villagehq/sdk';
47
+ import { defineVillageElements } from '@villagehq/sdk/elements';
48
+
49
+ export const village = createVillage({
50
+ 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
+ });
54
+
55
+ defineVillageElements(village); // does nothing on the server
56
+ ```
57
+
58
+ ```tsx
59
+ // components/warm-intros.tsx
60
+ 'use client';
61
+
62
+ import type {} from '@villagehq/sdk/react'; // JSX types for <village-*>
63
+ import '@/lib/village'; // registers the elements
64
+
65
+ export function WarmIntros({ company }: { company: string }) {
66
+ return (
67
+ <section>
68
+ <village-connect />
69
+ <village-facepile company={company} />
70
+ <village-paths company={company} onvillage-path-select={(event) => askForIntro(event.detail)} />
71
+ </section>
72
+ );
73
+ }
74
+ ```
75
+
76
+ Import `village` from `lib/village` wherever you need `village.connect()`, `village.signOut()` or a store. That's the whole integration: no server code. Sessions live in `localStorage` (`village.session.<publishableKey>`), slide for 30 days, and renew on their own. `village.signOut()` disconnects the app; call it when your user signs out.
77
+
78
+ ### Payer `app`, Next.js App Router
79
+
80
+ Your server mints a Village token for your signed-in user:
81
+
82
+ ```ts
83
+ // app/api/village/token/route.ts
84
+ import { createVillageApp } from '@villagehq/sdk/server';
85
+
86
+ const villageApp = createVillageApp({
87
+ secretKey: process.env.VILLAGE_SECRET_KEY ?? '',
88
+ apiUrl: process.env.VILLAGE_API_URL,
89
+ });
90
+
91
+ export async function POST(request: Request) {
92
+ const user = await requireSignedInUser(request); // your auth: return 401 when signed out
93
+ const { token } = await villageApp.tokens.create({ id: user.id, email: user.email });
94
+ return Response.json({ token });
95
+ }
96
+ ```
97
+
98
+ The browser asks your route for it. Create the client per signed-in user:
99
+
100
+ ```tsx
101
+ 'use client';
102
+
103
+ import { createVillage } from '@villagehq/sdk';
104
+ import { defineVillageElements } from '@villagehq/sdk/elements';
105
+ import type {} from '@villagehq/sdk/react';
106
+ import { useState } from 'react';
107
+
108
+ async function readVillageToken() {
109
+ const response = await fetch('/api/village/token', { method: 'POST' });
110
+ if (!response.ok) throw new Error(`Village token route answered ${response.status}`);
111
+ const { token } = await response.json();
112
+ return token;
113
+ }
114
+
115
+ function createUserVillage() {
116
+ const village = createVillage({
117
+ publishableKey: process.env.NEXT_PUBLIC_VILLAGE_PUBLISHABLE_KEY,
118
+ token: readVillageToken,
119
+ apiUrl: process.env.NEXT_PUBLIC_VILLAGE_API_URL,
120
+ });
121
+ defineVillageElements(village);
122
+ return village;
123
+ }
124
+
125
+ // Render with key={user.id}, so another user gets another client.
126
+ export function WarmIntros({ company }: { company: string }) {
127
+ useState(createUserVillage); // one client for this user, created once
128
+ return (
129
+ <>
130
+ <village-connect />
131
+ <village-facepile company={company} />
132
+ </>
133
+ );
134
+ }
135
+ ```
136
+
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.
138
+
139
+ ### Plain HTML, no build step
140
+
141
+ ```html
142
+ <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';
145
+
146
+ const village = createVillage({ publishableKey: 'pk_test_…' }); // a publishable key is public
147
+ defineVillageElements(village);
148
+ </script>
149
+
150
+ <village-connect></village-connect>
151
+ <village-facepile company="stripe.com"><span slot="empty">No warm intros yet</span></village-facepile>
152
+ ```
153
+
154
+ Pin the version in the URL.
155
+
156
+ ### Server only (payer `app`)
157
+
158
+ ```ts
159
+ import { createVillageApp } from '@villagehq/sdk/server';
160
+
161
+ const villageApp = createVillageApp({ secretKey: process.env.VILLAGE_SECRET_KEY ?? '' });
162
+ const member = villageApp.as({ id: user.id, email: user.email });
163
+
164
+ const { status } = await member.user.get();
165
+ if (status === 'ready' || status === 'needs_attention') {
166
+ const page = await member.companies.paths('stripe.com', { degrees: [1, 2], limit: 10 });
167
+ }
168
+ ```
169
+
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.
171
+
172
+ ## 4. User states
173
+
174
+ `village.user.get()` or `village.user.subscribe(({ user, error }) => …)`; `user.status` is one of:
175
+
176
+ | `status` | Meaning | Render |
177
+ | ----------------- | ---------------------------------------------------- | --------------------------------------------------- |
178
+ | `signed_out` | Payer `user`: nobody signed in, or the session ended | A button that calls `village.connect()` |
179
+ | `not_connected` | Payer `app`: no Village network linked yet | A button that calls `village.connect()` |
180
+ | `syncing` | Importing the network; paths come soon | Progress |
181
+ | `ready` | Everything works | The feature |
182
+ | `needs_attention` | Works on older data; an integration needs re-auth | The feature, plus "Reconnect" → `village.connect()` |
183
+
184
+ `isNetworkReady(user)` is `true` for `ready` and `needs_attention`. Call `connect()` from a click: browsers block popups otherwise.
185
+
186
+ ## 5. Elements
187
+
188
+ Register once with `defineVillageElements(village)` (from `@villagehq/sdk/elements`).
189
+
190
+ | Element | Attributes | Shows |
191
+ | -------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
192
+ | `<village-connect>` | none | The connect button, sync progress, or its default slot when ready (fallback text "Village connected") |
193
+ | `<village-facepile>` | `company` (domain, URL or LinkedIn company URL) | Avatars of people who can introduce you, `+N`, and the strength label |
194
+ | `<village-paths>` | `company` or `person` (LinkedIn URL or slug), `limit` (default 5) | A list of introducers; each is a button |
195
+
196
+ Every element sets `state` and `data-state` to its current state: `loading`, `signed_out`, `not_connected`, `not_synced`, `out_of_credits`, `none`, `found`, `error` (`<village-connect>` uses the user statuses). Its root is an `aria-live` region; images carry the person's name as `alt`.
197
+
198
+ **Customize**, in increasing control:
199
+
200
+ ```css
201
+ village-facepile {
202
+ --village-avatar-size: 32px;
203
+ --village-accent: #0f766e; /* buttons, initials, focus ring */
204
+ --village-radius: 6px; /* buttons and list items */
205
+ --village-font: inherit; /* the default */
206
+ }
207
+ village-facepile::part(avatar) {
208
+ border-radius: 6px;
209
+ }
210
+ village-paths::part(item):hover {
211
+ background: #f4f4f5;
212
+ }
213
+ village-facepile[state='none'] {
214
+ display: none;
215
+ }
216
+ village-facepile:not(:defined) {
217
+ display: inline-block;
218
+ min-height: 32px;
219
+ } /* no layout shift */
220
+ ```
221
+
222
+ Parts: `root`, `avatar`, `more`, `label`, `button`, `item`, `name`, `reason`.
223
+
224
+ Slots (replace the default content):
225
+
226
+ | Slot | When |
227
+ | ------------ | ------------------------------------------------------------ |
228
+ | `loading` | `loading` |
229
+ | `signed-out` | `signed_out` and `not_connected` (default: a connect button) |
230
+ | `action` | The connect button's label |
231
+ | `empty` | `none` |
232
+
233
+ Events (bubbling, composed `CustomEvent`s):
234
+
235
+ | Event | `detail` |
236
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
237
+ | `village-path-select` | `{ person, introducer, degree, reason }`; `introducer` is `null` for someone the user knows directly. Each person is `{ name, initials, avatar_url, linkedin_url }` |
238
+ | `village-connected` | `{ user }`, after `connect()` leaves the user connected (not when they close the popup or modal without connecting) |
239
+ | `village-error` | `{ error }`, when `connect()` fails, e.g. `popup_blocked` |
240
+
241
+ **React 19 + Tailwind.** `import type {} from '@villagehq/sdk/react'` adds the JSX types. Events are `on` + the event name:
242
+
243
+ ```tsx
244
+ <village-facepile
245
+ company="stripe.com"
246
+ className="[--village-accent:var(--color-teal-700)] [&::part(avatar)]:rounded-md"
247
+ onvillage-error={(event) => console.error(event.detail.error)}
248
+ >
249
+ <span slot="empty" className="text-sm text-muted-foreground">
250
+ No warm intros yet
251
+ </span>
252
+ </village-facepile>
253
+ ```
254
+
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.
256
+
257
+ ## 6. Stores, for your own UI
258
+
259
+ Every element is built on a store. Use them for fully custom UI in any framework.
260
+
261
+ ```ts
262
+ const intros = village.paths.check('stripe.com'); // facepile data
263
+ const paths = village.paths.company('stripe.com', { limit: 10 });
264
+ const personPaths = village.paths.person('linkedin.com/in/satyanadella');
265
+
266
+ const stop = intros.subscribe((state) => render(state)); // called right away, then on every change
267
+ intros.get(); // the current state, same object until it changes
268
+ intros.refresh(); // fetch again
269
+ stop();
270
+ ```
271
+
272
+ ```ts
273
+ type PathsCheckState =
274
+ | { kind: 'loading' }
275
+ | { kind: 'signed_out' }
276
+ | { kind: 'not_connected' }
277
+ | { kind: 'not_synced' }
278
+ | { kind: 'out_of_credits'; payer: 'app' | 'user'; topupUrl: string | null }
279
+ | { kind: 'none' }
280
+ | { kind: 'found'; people: FacepilePerson[]; more: number; strength: number | null; strength_label: string | null }
281
+ | { kind: 'error'; error: VillageError };
282
+ // paths.person / paths.company: the same, with { kind: 'found'; paths } instead.
283
+ ```
284
+
285
+ What the stores do for you:
286
+
287
+ - **Shared**: the same company returns the same store, so 50 badges make one fetch.
288
+ - **Batched**: every `check` asked for in the same tick goes out as one request (100 companies each).
289
+ - **Credit-safe**: a store fetches on its first subscriber and keeps its result after the last one leaves. It never retries on its own; `refresh()` does.
290
+ - **Live**: a `not_synced` store fetches again when the network turns ready; every store resets on sign-out.
291
+
292
+ React:
293
+
294
+ ```tsx
295
+ import { useVillage } from '@villagehq/sdk/react';
296
+
297
+ function IntroBadge({ company }: { company: string }) {
298
+ const state = useVillage(village.paths.check(company));
299
+ if (state.kind === 'found') return <span>{state.people.length + state.more} warm intros</span>;
300
+ if (state.kind === 'signed_out' || state.kind === 'not_connected') return <ConnectButton />;
301
+ return null;
302
+ }
303
+ ```
304
+
305
+ Also `useVillageUser(village)` → `{ user, status, isLoading, error, refresh }`, `useVillageConnect(village)` → `{ connect, isConnecting }`, and `<VillageGate village fallback>`.
306
+
307
+ Vue:
308
+
309
+ ```ts
310
+ import { onScopeDispose, shallowRef } from 'vue';
311
+ import type { VillageStore } from '@villagehq/sdk';
312
+
313
+ export function useVillageStore<State>(store: VillageStore<State>) {
314
+ const state = shallowRef(store.get());
315
+ onScopeDispose(store.subscribe((next) => (state.value = next)));
316
+ return state;
317
+ }
318
+ ```
319
+
320
+ Svelte (stores follow the Svelte store contract):
321
+
322
+ ```svelte
323
+ <script>
324
+ const intros = village.paths.check('stripe.com');
325
+ </script>
326
+ {#if $intros.kind === 'found'}{$intros.strength_label}{/if}
327
+ ```
328
+
329
+ Vanilla:
330
+
331
+ ```ts
332
+ village.paths.check('stripe.com').subscribe((state) => {
333
+ badge.dataset.state = state.kind;
334
+ badge.textContent = state.kind === 'found' ? `${state.people.length + state.more} warm intros` : '';
335
+ });
336
+ ```
337
+
338
+ ## 7. Other calls
339
+
340
+ Each returns once, and throws a `VillageError`:
341
+
342
+ ```ts
343
+ await village.people.search('CTOs in Berlin'); // a Page: results, hasMore, next(), for await
344
+ await village.companies.search({ prompt: 'fintech in NYC', scope: 'global' });
345
+ await village.people.paths('satyanadella'); // null when Village doesn't know them
346
+ await village.companies.paths('stripe.com', { degrees: [2, 3] });
347
+ await village.people.enrich('satyanadella');
348
+ await village.companies.enrich('stripe.com');
349
+ const { data } = await village.api.GET('/v2/lists'); // any v2 endpoint, typed
350
+ ```
351
+
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';
374
+
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
+ ```
382
+
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
425
+
426
+ ```
427
+ connect-src https://api.village.ai;
428
+ img-src https:; # avatars come from several CDNs
429
+ frame-src https://village.ai; # payer app: the sync modal
430
+ script-src https://esm.sh; # only for the CDN import
431
+ ```
432
+
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`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Village
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,35 @@
1
- # Temporary Holding Version
1
+ # @villagehq/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Put Village warm introductions in your product: who your users know at a company, and who can introduce them.
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).
6
+
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
10
+ ```
11
+
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
+ ```
23
+
24
+ | Import | What |
25
+ | ------------------------- | ----------------------------------------------------------------------------------------------------------------- |
26
+ | `@villagehq/sdk` | `createVillage`: user status, `connect()`, stores (`village.paths.*`), search, paths, enrichment, the typed `api` |
27
+ | `@villagehq/sdk/elements` | `<village-connect>`, `<village-facepile>`, `<village-paths>` web components |
28
+ | `@villagehq/sdk/react` | `useVillage(store)`, `useVillageUser`, `useVillageConnect`, `<VillageGate>`, JSX types for the elements |
29
+ | `@villagehq/sdk/server` | `createVillageApp({ secretKey })`: tokens, acting as a user, test users |
30
+
31
+ The npm package `village` is unrelated to Village.
32
+
33
+ ## Developing
34
+
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.
@@ -0,0 +1,15 @@
1
+ export type VillageCliOptions = {
2
+ argv?: string[];
3
+ cwd?: string;
4
+ fetch?: (url: string, init: RequestInit) => Promise<Response>;
5
+ openUrl?: (url: string) => void;
6
+ log?: (line: string) => void;
7
+ logError?: (line: string) => void;
8
+ sleep?: (durationMs: number) => Promise<void>;
9
+ isInteractive?: boolean;
10
+ ask?: (question: string) => Promise<string>;
11
+ };
12
+
13
+ export function main(options?: VillageCliOptions): Promise<number>;
14
+
15
+ export function mergeEnv(text: string | null, values: Record<string, string>): string;