@propertysimple/client 0.1.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 (54) hide show
  1. package/API.md +79 -0
  2. package/README.md +348 -0
  3. package/client/src/directory-sync.d.ts +123 -0
  4. package/client/src/directory-sync.js +404 -0
  5. package/client/src/index.d.ts +5123 -0
  6. package/client/src/index.js +285 -0
  7. package/client/src/managed-sync.d.ts +129 -0
  8. package/client/src/managed-sync.js +410 -0
  9. package/contracts/src/catalog-derived.d.ts +7 -0
  10. package/contracts/src/catalog-derived.js +78 -0
  11. package/contracts/src/catalog-fields.d.ts +231 -0
  12. package/contracts/src/catalog-fields.js +194 -0
  13. package/contracts/src/catalog-inventory.d.ts +14 -0
  14. package/contracts/src/catalog-inventory.js +10 -0
  15. package/contracts/src/directory-execution.d.ts +320 -0
  16. package/contracts/src/directory-execution.js +122 -0
  17. package/contracts/src/directory-identity.d.ts +1255 -0
  18. package/contracts/src/directory-identity.js +129 -0
  19. package/contracts/src/directory-sales.d.ts +333 -0
  20. package/contracts/src/directory-sales.js +102 -0
  21. package/contracts/src/directory.d.ts +1088 -0
  22. package/contracts/src/directory.js +179 -0
  23. package/contracts/src/index.d.ts +18 -0
  24. package/contracts/src/index.js +18 -0
  25. package/contracts/src/intelligence.d.ts +366 -0
  26. package/contracts/src/intelligence.js +157 -0
  27. package/contracts/src/location-serving.d.ts +13 -0
  28. package/contracts/src/location-serving.js +89 -0
  29. package/contracts/src/location.d.ts +73 -0
  30. package/contracts/src/location.js +75 -0
  31. package/contracts/src/openapi.d.ts +22 -0
  32. package/contracts/src/openapi.js +109 -0
  33. package/contracts/src/operations.d.ts +8316 -0
  34. package/contracts/src/operations.js +56 -0
  35. package/contracts/src/opportunity-events.d.ts +38 -0
  36. package/contracts/src/opportunity-events.js +69 -0
  37. package/contracts/src/platform-api.d.ts +12 -0
  38. package/contracts/src/platform-api.js +1 -0
  39. package/contracts/src/professional-intelligence.d.ts +256 -0
  40. package/contracts/src/professional-intelligence.js +90 -0
  41. package/contracts/src/property-features.d.ts +94 -0
  42. package/contracts/src/property-features.js +398 -0
  43. package/contracts/src/publication.d.ts +44 -0
  44. package/contracts/src/publication.js +42 -0
  45. package/contracts/src/routes.d.ts +437 -0
  46. package/contracts/src/routes.js +496 -0
  47. package/contracts/src/schemas.d.ts +3198 -0
  48. package/contracts/src/schemas.js +674 -0
  49. package/contracts/src/search.d.ts +2630 -0
  50. package/contracts/src/search.js +223 -0
  51. package/contracts/src/smart-feed.d.ts +3731 -0
  52. package/contracts/src/smart-feed.js +109 -0
  53. package/openapi.json +43542 -0
  54. package/package.json +60 -0
package/API.md ADDED
@@ -0,0 +1,79 @@
1
+ # PropertySimple client API reference
2
+
3
+ Generated from the shared route registry. Run `npm run sdk:docs` in the platform repository to update it.
4
+
5
+ Every operation below is available through `ps.call(operation, input, options)`, with inferred input and response types. The [quickstart](README.md) documents convenient namespace helpers such as `ps.listings.get(id)` and `ps.search.listings(query)`. Request fields use the API's snake_case names. Supply `{ idempotencyKey: 'stable-request-id' }` in options for mutating POST requests; search POSTs are read-only.
6
+
7
+ This reference describes the SDK contract, not a claim that every feature is enabled or accepted in production. Credential scopes, dataset grants, source freshness and index readiness still govern access. Legacy `/api/*`, admin pages, public health/OpenAPI routes and internal jobs are outside this versioned SDK registry. The package also includes `openapi.json` with full input/output schemas.
8
+
9
+ | Operation for `ps.call` | HTTP endpoint | Required scope | Purpose |
10
+ | ---------------------------------- | ---------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------- |
11
+ | `searchFeatures` | `GET /v1/search/features` | `catalog:read` | Read typed feature definitions and supported search operators. |
12
+ | `searchListings` | `POST /v1/search/listings` | `catalog:read` | Search current inventory with typed hard constraints and evidence. |
13
+ | `searchFacets` | `POST /v1/search/facets` | `catalog:read` | Read authorized refinement counts for the same normalized search. |
14
+ | `searchMap` | `POST /v1/search/map` | `catalog:read` | Read bounded map points and clusters with matching search semantics. |
15
+ | `searchSuggest` | `GET /v1/search/suggest` | `catalog:read` | Suggest registered features and permitted listing locations. |
16
+ | `searchInterpret` | `POST /v1/search/interpret` | `catalog:read` | Interpret a bounded search vocabulary into editable filters and unresolved text. |
17
+ | `directoryGetCanonicalIdentity` | `GET /v1/directory/canonical-identities/{id}` | `directory:read` | Resolve reviewed identity links using only permitted source members. |
18
+ | `directoryMergeIdentities` | `POST /v1/directory/identity-merges` | `directory:identity:write` | Record an audited identity merge with expected source bindings. |
19
+ | `directorySplitIdentity` | `POST /v1/directory/identity-splits` | `directory:identity:write` | Split a reviewed identity without moving account or action history. |
20
+ | `directoryListInventory` | `GET /v1/directory/entities/{id}/inventory` | `directory:read` | Read represented or current-roster inventory with optional verified corporate rollup. |
21
+ | `discoverDirectorySalesCandidates` | `GET /v1/directory/sales-candidates` | `directory:sales` | Discover a bounded authorized page of subjects for sales-plan evaluation. |
22
+ | `claimDirectoryActions` | `POST /v1/directory/action-claims` | `directory:execute` | Reserve directory recommendations under a separately reviewed execution policy. |
23
+ | `startDirectoryAttempt` | `POST /v1/directory/actions/{id}/attempts` | `directory:execute` | Recheck authority and start one attempt, or return reconciliation-only replay. |
24
+ | `getDirectoryAction` | `GET /v1/directory/actions/{id}` | `directory:execute` | Read a product-scoped directory action and its execution state. |
25
+ | `recordDirectoryActionOutcomes` | `POST /v1/directory/action-outcomes` | `directory:execute` | Record authenticated provider receipts without retrying ambiguous effects. |
26
+ | `directoryRecordProfile` | `POST /v1/directory/profile-observations` | `directory:write` | Record a trusted producer’s scoped profile evidence. |
27
+ | `directoryRecordRelationship` | `POST /v1/directory/relationship-observations` | `directory:write` | Record a trusted producer’s evidenced relationship. |
28
+ | `directoryListProfessionals` | `GET /v1/directory/professionals` | `directory:read` | Read permitted source-backed professionals. |
29
+ | `directoryListOffices` | `GET /v1/directory/offices` | `directory:read` | Read permitted source offices. |
30
+ | `directoryListOrganizations` | `GET /v1/directory/organizations` | `directory:read` | Read evidenced business and brand observations. |
31
+ | `directoryListTeams` | `GET /v1/directory/teams` | `directory:read` | Read evidenced teams. |
32
+ | `directoryGetEntity` | `GET /v1/directory/entities/{id}` | `directory:read` | Read a scoped professional, office, organization or team. |
33
+ | `directoryResolveAgent` | `GET /v1/directory/legacy-agents/{id}` | `directory:read` | Resolve a listing agent ID to its permitted public directory identity. |
34
+ | `directoryResolveIdentity` | `GET /v1/directory/identities` | `directory:read` | Resolve a typed directory source identity. |
35
+ | `directoryListRelationships` | `GET /v1/directory/relationships` | `directory:read` | Read bounded evidenced relationships. |
36
+ | `directoryGetContacts` | `GET /v1/directory/entities/{id}/contacts` | `directory:contacts` | Read purpose-permitted directory contact endpoints. |
37
+ | `directoryGetMetrics` | `GET /v1/directory/entities/{id}/metrics` | `directory:read` | Read authorized observed inventory metrics. |
38
+ | `createDirectorySnapshot` | `POST /v1/directory/snapshots` | `directory:sync` | Capture a directory or combined feed boundary. |
39
+ | `getDirectorySnapshot` | `GET /v1/directory/snapshots/{id}` | `directory:sync` | Read snapshot scope, boundary and expiry. |
40
+ | `listDirectorySnapshotItems` | `GET /v1/directory/snapshots/{id}/items` | `directory:sync` | Page immutable typed snapshot resources. |
41
+ | `listDirectoryChanges` | `GET /v1/directory/changes` | `directory:sync` | Replay typed changes with complete commit-group boundaries. |
42
+ | `createDirectorySalesPlan` | `POST /v1/directory/sales-plans` | `directory:sales` | Prepare bounded professional or organization sales recommendations. |
43
+ | `getDirectorySalesPlan` | `GET /v1/directory/sales-plans/{id}` | `directory:sales` | Read a scoped recommendation-only directory sales plan. |
44
+ | `requestDirectoryAssessment` | `POST /v1/directory/assessments` | `intelligence:judge` | Queue a canonical-evidence directory assessment. |
45
+ | `latestDirectoryAssessment` | `GET /v1/directory/assessments/latest` | `intelligence:read` | Read prepared directory intelligence with current evidence checks. |
46
+ | `getDirectoryAssessment` | `GET /v1/directory/assessments/{id}` | `intelligence:read` | Read a purpose-scoped directory assessment. |
47
+ | `reportPublication` | `POST /v1/publications` | `publications:write` | Acknowledge this product’s current listing publication. |
48
+ | `resolveDestination` | `GET /v1/listings/{id}/destination` | `catalog:read` | Resolve a current product-reported destination. |
49
+ | `evaluateJudgment` | `POST /v1/judgments` | `intelligence:judge` | Evaluate a bounded versioned task against caller-supplied evidence. |
50
+ | `getJudgment` | `GET /v1/judgments/{id}` | `intelligence:read` | Read this credential subject’s durable judgment audit. |
51
+ | `intelligenceCapabilities` | `GET /v1/intelligence-capabilities` | `intelligence:read` | Read implemented judgment tasks and explicit unsupported capabilities. |
52
+ | `capabilities` | `GET /v1/capabilities` | Authenticated caller | Read this caller’s supported capabilities. |
53
+ | `getCatalogHealth` | `GET /v1/catalog-health` | `catalog:read` | Read authorized source freshness independently of catalog changes. |
54
+ | `listListings` | `GET /v1/listings` | `catalog:read` | Search the current authorized catalog. |
55
+ | `listListingMap` | `GET /v1/listings/map` | `catalog:read` | Read bounded map markers within a required geographic box. |
56
+ | `listSimilarListings` | `GET /v1/listings/{id}/similar` | `catalog:read` | Rank a bounded local shortlist using transparent fact similarity. |
57
+ | `getListing` | `GET /v1/listings/{id}` | `catalog:read` | Read a current listing. |
58
+ | `getListingContacts` | `GET /v1/listings/{id}/contacts` | `contacts:read` | Read protected source contact routing for a current active listing. |
59
+ | `resolveListingIdentity` | `GET /v1/listing-identities` | `catalog:read` | Resolve a namespaced source alias. |
60
+ | `resolveAgentIdentity` | `GET /v1/agent-identities` | `agents:read` | Resolve a namespaced agent source alias. |
61
+ | `getAgent` | `GET /v1/agents/{id}` | `agents:read` | Read permitted agent attribution. |
62
+ | `listAgentListings` | `GET /v1/agents/{id}/listings` | `catalog:read` | Read an agent’s authorized inventory. |
63
+ | `classificationSchema` | `GET /v1/classification-schema` | `catalog:read` | Read the current classification vocabulary. |
64
+ | `createSnapshot` | `POST /v1/catalog-snapshots` | `catalog:sync` | Create a consistent snapshot with its replay cursor. |
65
+ | `getSnapshot` | `GET /v1/catalog-snapshots/{id}` | `catalog:sync` | Read snapshot readiness and expiry. |
66
+ | `listSnapshotItems` | `GET /v1/catalog-snapshots/{id}/items` | `catalog:sync` | Page an immutable snapshot. |
67
+ | `listCatalogChanges` | `GET /v1/catalog-changes` | `catalog:sync` | Replay authorized projection changes and removals. |
68
+ | `listEvents` | `GET /v1/events` | `catalog:sync` | Replay committed domain occurrences. |
69
+ | `listActionProfiles` | `GET /v1/action-profiles` | `opportunities:read` | Read published action profiles. |
70
+ | `listOpportunities` | `GET /v1/opportunities` | `opportunities:read` | Read current candidates without reserving actions. |
71
+ | `createPlan` | `POST /v1/opportunity-plans` | `plans:create` | Create or reuse an actual persisted selection. |
72
+ | `getPlan` | `GET /v1/opportunity-plans/{id}` | `opportunities:read` | Read plan progress, coverage and execution counts. |
73
+ | `listPlanItems` | `GET /v1/opportunity-plans/{id}/items` | `opportunities:read` | Read stable selection pages. |
74
+ | `claimDispatchBatch` | `POST /v1/dispatch-batches` | `actions:dispatch` | Reserve executable actions after critical revalidation. |
75
+ | `startAttempt` | `POST /v1/actions/{id}/attempts` | `actions:dispatch` | Fence and record dispatch before an external effect. |
76
+ | `recordActionOutcomes` | `POST /v1/action-outcomes` | `actions:dispatch` | Record idempotent sender receipts with per-item results. |
77
+ | `recordProductContextEvents` | `POST /v1/product-context-events` | `product-events:write` | Ingest owned product context snapshots and deltas. |
78
+ | `createWebhookSubscription` | `POST /v1/webhook-subscriptions` | `webhooks:manage` | Register a subscription only where delivery support is configured. |
79
+ | `readiness` | `GET /v1/operations/readiness` | `operations:read` | Inspect configured operational dependencies. |
package/README.md ADDED
@@ -0,0 +1,348 @@
1
+ # @propertysimple/client
2
+
3
+ The typed **server-side PropertySimple SDK** for listing details, property search,
4
+ agents, catalog synchronization, directory data and the platform's other public
5
+ API operations. Configure one client in your backend, then use `ps.listings`,
6
+ `ps.search`, `ps.agents` and `ps.catalog` throughout your application.
7
+
8
+ This package is distributed publicly for **authenticated PropertySimple API
9
+ integrations**. An API key is required to access data; no credentials or listing
10
+ data are included in the package. The existing `@propertysimple/listings` package
11
+ is separate and unchanged.
12
+
13
+ **Service readiness:** SDK availability and API feature readiness are separate.
14
+ Start direct catalog integrations with `ps.listings` and `ps.agents`. Managed
15
+ search uses `ps.search` and requires a ready, authorized index on the API server.
16
+ At the initial 0.1.0 release, full production search acceptance is still pending;
17
+ do not treat this package release as approval to switch search traffic. Installing
18
+ the SDK does not activate imports, indexing, synchronization schedules, Jev,
19
+ geocoding or outbound actions.
20
+
21
+ ## Installation
22
+
23
+ Requires **Node.js 24** and ESM (`"type": "module"` in your application's
24
+ `package.json`). JavaScript and TypeScript are supported; declarations are
25
+ included. TypeScript applications should use their normal `@types/node` setup.
26
+ CommonJS and browser clients are not supported by this release.
27
+
28
+ ```sh
29
+ npm install @propertysimple/client
30
+ ```
31
+
32
+ The archive contains compiled JavaScript, TypeScript declarations, the shared
33
+ contracts, this guide, [the complete operation reference](API.md) and
34
+ `openapi.json`. Its only external runtime dependency is Zod. Consumers do not need
35
+ `@ps/client`, `@ps/contracts`, the platform source tree or a TypeScript runtime loader.
36
+ Commit your application's lockfile so deployments use the version you tested.
37
+
38
+ ## Configure once
39
+
40
+ Create a backend-only module such as `src/lib/ps.ts`. The environment variable
41
+ names below are application conventions; the SDK reads only the supplied options.
42
+ Do not use browser-exposed environment variables for service credentials.
43
+
44
+ <!-- sdk-example: ps.ts -->
45
+
46
+ ```ts
47
+ import PropertySimple from '@propertysimple/client';
48
+
49
+ const apiKey = process.env.PROPERTYSIMPLE_API_KEY;
50
+ if (!apiKey) throw new Error('Set PROPERTYSIMPLE_API_KEY on the server.');
51
+
52
+ const ps = new PropertySimple({
53
+ apiKey,
54
+ });
55
+
56
+ export default ps;
57
+ ```
58
+
59
+ The default base URL is `https://api.propertysimple.ai`. Supply a scoped service
60
+ key issued for your application; you do not need Typesense, ListHub or database
61
+ credentials. For staging or local development, explicitly pass `baseUrl` when
62
+ constructing the client, for example `baseUrl: 'http://127.0.0.1:3100'`. Each instance owns its credentials; multiple
63
+ instances can coexist without changing another tenant's configuration.
64
+
65
+ Request a service key and dataset ID from your PropertySimple platform
66
+ administrator. Keys grant specific datasets and operations: catalog reads use
67
+ `catalog:read`, agent profiles use `agents:read`, and protected contacts need their
68
+ own scope. Choose the minimum permissions your integration needs. A dataset ID is
69
+ an authorization boundary, not an MLS number or a value to guess.
70
+
71
+ Keep the key in your backend's secret manager or an ignored local `.env` file.
72
+ Never put it in a browser bundle, mobile app, public repository, URL, or a
73
+ `NEXT_PUBLIC_`/`VITE_` environment variable. The SDK sends it as an HTTPS Bearer
74
+ credential. Browser and mobile clients should call your own authenticated backend;
75
+ that backend owns the `ps` instance and validates end-user access.
76
+
77
+ In application code, import this configured instance. Adjust the relative path
78
+ or use your application's `@/lib/ps` alias:
79
+
80
+ <!-- sdk-example: listings.ts -->
81
+
82
+ ```ts
83
+ import ps from './ps.js';
84
+
85
+ const datasetId = 'your-authorized-dataset';
86
+
87
+ const page = await ps.listings.list({
88
+ dataset_id: datasetId,
89
+ mls_number: 'EXAMPLE-123',
90
+ limit: 25,
91
+ });
92
+
93
+ const listing = await ps.listings.get('your-listing-id', {
94
+ dataset_id: datasetId,
95
+ profile: 'portal_active',
96
+ });
97
+
98
+ const identity = await ps.listings.resolveIdentity({
99
+ source: 'listhub',
100
+ key: 'your-source-listing-key',
101
+ });
102
+
103
+ const agent = await ps.agents.get('your-agent-id');
104
+ ```
105
+
106
+ `listings.get` includes the full gallery. `profile: 'portal_active'` requires
107
+ currently eligible active portal inventory; generic catalog reads can retain
108
+ meaningful pending/sold status. A missing or removed listing is not evidence that
109
+ it sold. A source key and a canonical listing ID are different identities: resolve
110
+ existing source keys instead of treating addresses as unique IDs.
111
+
112
+ For a first connectivity check, use `await ps.call('capabilities', {})`, then
113
+ `await ps.catalog.health(datasetId)` and a small `ps.listings.list` request.
114
+ Capabilities describe the caller's API support; health describes source freshness.
115
+ Neither response establishes that a Typesense generation is ready.
116
+
117
+ The examples use ESM imports. Run plain JavaScript in a `.mjs` file, or set
118
+ `"type": "module"`; for local environment files Node 24 supports
119
+ `node --env-file=.env your-script.mjs`. TypeScript examples should be compiled by
120
+ your application's normal build. In a server-rendered framework, keep this module
121
+ in server routes/services and use a Node.js runtime.
122
+
123
+ ## Search
124
+
125
+ <!-- sdk-example: search.ts -->
126
+
127
+ ```ts
128
+ import ps from './ps.js';
129
+
130
+ const results = await ps.search.listings({
131
+ dataset_id: 'your-authorized-dataset',
132
+ q: '123 Main Street',
133
+ filters: {
134
+ transaction_type: 'for_sale',
135
+ currency: 'USD',
136
+ price: { max: '900000.00' },
137
+ bedrooms: { min: 3 },
138
+ },
139
+ sort: 'relevance',
140
+ limit: 24,
141
+ });
142
+ ```
143
+
144
+ Search responses include compact listing cards with a cover photo, `photo_count`,
145
+ match reasons and index/freshness metadata. Use `ps.listings.get(id, options)` for
146
+ complete details. The SDK calls the shared API; it never exposes Typesense keys.
147
+ Source facts can be searched without Jev; interpreted features require their own
148
+ current evidence. Missing evidence is not a negative match.
149
+
150
+ The map helper takes `{ query, viewport, zoom, limit }`. Use the same applied
151
+ query for listings, facets and maps, and honor generation/checkpoint, coverage,
152
+ truncation and expiry metadata. Search readiness or stale-index errors must not
153
+ silently become empty inventory or a broader query with filters removed.
154
+
155
+ ## Methods and complete API coverage
156
+
157
+ | Namespace | Common methods |
158
+ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
159
+ | `ps.listings` | `list`, `get`, `resolveIdentity`, `contacts`, `map`, `similar` |
160
+ | `ps.search` | `listings`, `suggest`, `facets`, `map`, `features`, `interpret` |
161
+ | `ps.agents` | `get`, `resolveIdentity`, `listings` |
162
+ | `ps.catalog` | `health`, `createSnapshot`, `snapshot`, `snapshotItems`, `changes`, `events`, `opportunityObservations` |
163
+ | `ps.directory` | `professionals`, `offices`, `organizations`, `teams`, `get`, `inventory`, identity, contact and synchronization helpers |
164
+ | `ps.publications` | `report`, `destination` |
165
+ | `ps.judgments` | `capabilities`, `evaluate`, `get` |
166
+ | `ps.opportunityPlans` | `create`, `get`, `items`, `claimDispatchBatch`, `waitUntilReady` |
167
+ | `ps.actions` / `ps.directoryActions` | Explicit action claims, attempts and outcomes |
168
+
169
+ All 69 registered versioned public operations are available through
170
+ `ps.call(operation, input, options)`, including operations without a shortcut.
171
+ Input and output types are inferred from the operation name. Input fields use the
172
+ API's snake_case names; helpers use camelCase. For example:
173
+
174
+ <!-- sdk-example: operations.ts -->
175
+
176
+ ```ts
177
+ import ps from './ps.js';
178
+
179
+ const capabilities = await ps.call('capabilities', {});
180
+ const health = await ps.catalog.health('your-authorized-dataset');
181
+ const listing = await ps.call('getListing', {
182
+ id: 'your-listing-id',
183
+ dataset_id: 'your-authorized-dataset',
184
+ });
185
+ ```
186
+
187
+ See [API.md](API.md) for every operation, HTTP route and required scope. Legacy
188
+ `/api/*` adapters, admin pages, `/health/live`, `/openapi.json` and internal worker
189
+ routes are not versioned SDK operations. Having a method does not grant its scope
190
+ or enable an optional feature in production.
191
+ The reference and OpenAPI document also ship at
192
+ `node_modules/@propertysimple/client/API.md` and
193
+ `node_modules/@propertysimple/client/openapi.json`, so integrations can inspect
194
+ them without repository access. The repository guides linked below require
195
+ access to the PropertySimple source repository.
196
+
197
+ ## Pagination and synchronization
198
+
199
+ Continue a listing page with its `next_cursor` and the original query. Honor
200
+ `has_more`, including when an authorized feed page is empty. Do not increase an
201
+ offset to page a large dataset. `ps.iterateListings(query)` is convenient for live
202
+ browsing, but concurrent updates can move records between pages.
203
+
204
+ <!-- sdk-example: pagination.ts -->
205
+
206
+ ```ts
207
+ import ps from './ps.js';
208
+
209
+ const query = { dataset_id: 'your-authorized-dataset', limit: 25 };
210
+ const firstPage = await ps.listings.list(query);
211
+
212
+ if (firstPage.has_more && firstPage.next_cursor) {
213
+ const nextPage = await ps.listings.list({
214
+ ...query,
215
+ cursor: firstPage.next_cursor,
216
+ });
217
+ console.log(nextPage.data.map((listing) => listing.id));
218
+ }
219
+ ```
220
+
221
+ For a consistent replica, create a catalog snapshot, read its immutable pages,
222
+ then replay `ps.catalog.changes` from the supplied cursor. Persist the checkpoint
223
+ atomically with applied updates/removals. An expired cursor requires a new
224
+ snapshot; resuming from an invented timestamp loses removals.
225
+
226
+ The optional `@propertysimple/client/managed-sync` export supplies
227
+ `CatalogSyncWorker` and `PostgresCatalogTarget`. The
228
+ `@propertysimple/client/directory-sync` export supplies `DirectorySyncWorker`
229
+ and `PostgresDirectoryTarget` for directory or combined feeds. They use an
230
+ explicitly supplied database adapter and a separate consumer-owned schema;
231
+ installation does not create tables, overwrite product data or install a schedule.
232
+ Keep the database wrappers, checkpoints and cache invalidation handling required
233
+ by the [managed synchronization guide](https://github.com/PropertySimple/ps-listings-platform/blob/main/docs/managed-sync.md).
234
+
235
+ ## Errors, requests and retries
236
+
237
+ `ApiError` contains the HTTP `status`, stable `code` and validated `problem`
238
+ (including the request ID when provided). `ContractError` means an input or a
239
+ successful response failed the shared contract; never treat it as empty inventory.
240
+
241
+ <!-- sdk-example: errors.ts -->
242
+
243
+ ```ts
244
+ import { ApiError, ContractError } from '@propertysimple/client';
245
+ import ps from './ps.js';
246
+
247
+ try {
248
+ await ps.listings.get('your-listing-id');
249
+ } catch (error) {
250
+ if (error instanceof ApiError) {
251
+ console.error(error.status, error.code, error.problem?.request_id);
252
+ } else if (error instanceof ContractError) {
253
+ console.error('API contract mismatch', error.operation);
254
+ }
255
+ throw error;
256
+ }
257
+ ```
258
+
259
+ Client options include the optional `baseUrl` override, `timeoutMs` (default 15,000 milliseconds per attempt),
260
+ `maxRetries` (default two; allowed zero through five) and an optional `fetch`
261
+ implementation. Requests accept an abort `signal`. Network failures and HTTP
262
+ 429/503 can retry within those limits; authorization errors do not retry.
263
+ Use an abort signal for a total deadline across attempts. Redirects are rejected;
264
+ HTTPS is required except for loopback development.
265
+
266
+ For an integration timeout spanning retries, pass
267
+ `{ signal: AbortSignal.timeout(30_000) }` as the request options. Log the operation,
268
+ HTTP status, problem code and request ID; exclude API keys, contact details and
269
+ complete response bodies from ordinary logs. In particular:
270
+
271
+ | Result | Integration behavior |
272
+ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
273
+ | HTTP 401/403 | Check the service key, dataset grant and operation scope; do not retry with broader credentials automatically. |
274
+ | HTTP 404 | Handle the unavailable resource explicitly; do not infer that a listing sold. |
275
+ | HTTP 429/503 | Honor the SDK's bounded retry behavior and surface persistent unavailability. |
276
+ | `SEARCH_INDEX_NOT_READY` / `SEARCH_SNAPSHOT_TOO_OLD` | Keep search unavailable until a valid generation is ready; do not show an empty inventory as a successful response. |
277
+ | `ContractError` | Check API/SDK compatibility and report the contract mismatch. |
278
+
279
+ Mutating POST operations require a caller-supplied
280
+ `{ idempotencyKey: 'stable-request-id' }` request option. Retries keep the same key
281
+ and body. Search POSTs are read-only and need no key. An action attempt with
282
+ `replayed: true` is an existing attempt to reconcile, not permission to send again.
283
+ The SDK does not call email, advertising or payment providers. A plan does not
284
+ constitute permission to contact someone or make a purchase.
285
+
286
+ ## Types, validation and compatibility
287
+
288
+ The default export, named `PropertySimple` and named `ListingsClient` refer to the
289
+ same class. Existing instance-based code can retain `ListingsClient`; there is no
290
+ process-global `Listings.init()` configuration.
291
+
292
+ The root exports the `DEFAULT_BASE_URL` constant and `Listing`, `InputRequest`, `Output`, `Operation`, `Principal`,
293
+ `ClientOptions` and `RequestOptions` types. Full schemas and types are available
294
+ from `@propertysimple/client/contracts`; location expiry/privacy projection helpers
295
+ are available from `@propertysimple/client/location-serving`.
296
+
297
+ Keep API and SDK contract versions compatible. Responses are validated strictly;
298
+ coordinate additive schema changes before deployment when older consumers would
299
+ reject new fields. Source freshness, dataset grants, contacts permissions and
300
+ current address privacy still apply. Private-address listings can remain in
301
+ nonspatial results, but their street/unit/subdivision and coordinates must be
302
+ cleared from consumer copies and caches. Do not restore an old geocode or contact.
303
+
304
+ ## Migrating existing applications
305
+
306
+ This package is separate from `@propertysimple/listings`, not a drop-in package
307
+ rename. Start one configured `ps` instance, map legacy source IDs to canonical IDs,
308
+ use cursors, and verify actual fields, status/privacy behavior and application
309
+ relationships before switching traffic. Keep the old integration available during
310
+ a reviewed migration. The server's authenticated `/api/*` compatibility adapters
311
+ remain available for existing retrieval callers; new code should use `/v1` methods.
312
+ Those adapters are not the public-portal serving contract.
313
+
314
+ Read the [retrieval replacement guide](https://github.com/PropertySimple/ps-listings-platform/blob/main/docs/listings-api-replacement.md)
315
+ and [product integration guide](https://github.com/PropertySimple/ps-listings-platform/blob/main/docs/product-integration.md)
316
+ for the five existing retrieval mappings, source freshness and cutover checks.
317
+ Installing this package does not modify or switch an existing consumer application.
318
+
319
+ ## Maintainer checks
320
+
321
+ The source workspace retains the internal `@ps/client` name so existing platform
322
+ imports continue working. `package.release.json` defines the distribution named
323
+ `@propertysimple/client`; `sdk:build` compiles the SDK and its reachable shared
324
+ contracts together, then replaces private workspace imports with local imports.
325
+ The version comes from this workspace's `package.json`.
326
+
327
+ ```sh
328
+ npm run sdk:docs -- --check
329
+ npm run sdk:test
330
+ npm run sdk:pack
331
+ ```
332
+
333
+ For local integration testing, install the archive from
334
+ `packages/client/dist/propertysimple-client-0.1.0.tgz` into a separate application.
335
+ Publish the exact inspected archive to avoid including workspace configuration:
336
+
337
+ ```sh
338
+ npm publish ./packages/client/dist/propertysimple-client-0.1.0.tgz --access public --registry https://registry.npmjs.org/ --ignore-scripts
339
+ ```
340
+
341
+ `npm run sdk:test` builds and packs the release, installs it in a temporary project
342
+ with local dependency archives and network access disabled for npm, checks every
343
+ public export and the README examples' TypeScript types, and exercises synthetic
344
+ requests. It performs no live API/provider work. Build output and archives stay
345
+ ignored under `packages/client/dist`. Inspect the archive's file list and scan it
346
+ for credentials before publication. Publishing requires npm organization access
347
+ and any two-factor authentication required by that account. Registry versions are
348
+ immutable; bump the client workspace version and rebuild for later releases.
@@ -0,0 +1,123 @@
1
+ import { SmartFeedSelectorSchema, type SmartFeedResource, type Output } from '../../contracts/src/index.js';
2
+ import { type ListingsClient } from './index.js';
3
+ import { type TargetDatabase, type TargetSql, type ProjectionReadOptions } from './managed-sync.js';
4
+ type ResourceType = SmartFeedResource['resource_type'];
5
+ type Selector = ReturnType<typeof SmartFeedSelectorSchema.parse>;
6
+ type Group = {
7
+ id: string;
8
+ end: string;
9
+ size: number;
10
+ next: number;
11
+ start: string;
12
+ };
13
+ interface Checkpoint {
14
+ phase: 'create' | 'snapshot' | 'replay' | 'blocked';
15
+ generation: string;
16
+ snapshotId?: string;
17
+ snapshotCursor?: string;
18
+ replayCursor?: string;
19
+ lastSequence: string;
20
+ visibleSequence: string;
21
+ snapshotSequence?: string;
22
+ group?: Group;
23
+ sourceFreshness?: Output<'listDirectoryChanges'>['meta']['source_freshness'];
24
+ }
25
+ export interface DirectorySyncState {
26
+ version: string;
27
+ checkpoint: Checkpoint;
28
+ active_generation: string | null;
29
+ visible_seq: string;
30
+ synchronized_at: string | null;
31
+ }
32
+ export interface DirectorySyncConfiguration {
33
+ streamId: string;
34
+ datasetId: string;
35
+ /** Nonsecret stable identity for environment + credential scope. Change streams on scope changes. */
36
+ sourceNamespace: string;
37
+ selector?: {
38
+ mode?: 'directory' | 'combined';
39
+ party_id?: string;
40
+ inventory_basis?: 'listing_representation';
41
+ };
42
+ }
43
+ export interface DirectoryInvalidation {
44
+ id: string;
45
+ kind: 'reset' | 'group';
46
+ generation: string;
47
+ from_sequence: string;
48
+ through_sequence: string;
49
+ lease_token: string;
50
+ }
51
+ /**
52
+ * A consumer-owned typed projection. Versions received for an incomplete group are durable but
53
+ * invisible. Completing a group changes one visibility pointer rather than applying an unbounded
54
+ * mutation transaction. Product-owned users, leads, slugs and billing never enter this schema.
55
+ */
56
+ export declare class PostgresDirectoryTarget {
57
+ private readonly db;
58
+ readonly configuration: DirectorySyncConfiguration;
59
+ readonly selector: Selector;
60
+ private readonly fingerprint;
61
+ constructor(db: TargetDatabase, configuration: DirectorySyncConfiguration);
62
+ /**
63
+ * Explicit approved install/upgrade. v2 keeps resource_type as text, adds version metadata,
64
+ * and invalidates every v1 checkpoint so identity bindings enter through a complete snapshot.
65
+ * Stable local UUID mappings and old versions remain for product-owned recovery/pruning.
66
+ * Quiesce old SDK workers before invoking this; initialization is not a request-time action.
67
+ */
68
+ initialize(): Promise<void>;
69
+ state(): Promise<DirectorySyncState>;
70
+ commit(expected: DirectorySyncState, next: Checkpoint, write: (tx: TargetSql) => Promise<void>, options?: {
71
+ activate?: boolean;
72
+ synchronized?: boolean;
73
+ }): Promise<boolean>;
74
+ /** Two set-based writes per page, independent of page size. Checkpoint commits in the caller. */
75
+ writePage(tx: TargetSql, generation: string, items: Array<{
76
+ resource_type: ResourceType;
77
+ resource_id: string;
78
+ sequence: string;
79
+ resource: SmartFeedResource | null;
80
+ }>): Promise<void>;
81
+ invalidate(tx: TargetSql, generation: string, kind: 'reset' | 'group', from: string, through: string): Promise<void>;
82
+ reset(expected: DirectorySyncState, blocked?: boolean): Promise<boolean>;
83
+ resume(): Promise<void>;
84
+ mapExistingIdentity(resourceType: ResourceType, resourceId: string, localId: string): Promise<void>;
85
+ private readable;
86
+ list(options: ProjectionReadOptions & {
87
+ resourceType?: ResourceType;
88
+ after?: string;
89
+ limit?: number;
90
+ }): Promise<Array<{
91
+ resource: SmartFeedResource;
92
+ local_id: string;
93
+ }>>;
94
+ get(resourceType: ResourceType, resourceId: string, options: ProjectionReadOptions): Promise<SmartFeedResource | null>;
95
+ claimInvalidations(limit?: number, leaseSeconds?: number): Promise<DirectoryInvalidation[]>;
96
+ /** Page every affected key before acknowledging a group; resolve current state for cache effects. */
97
+ invalidationItems(event: DirectoryInvalidation, after?: string, limit?: number): Promise<Array<{
98
+ resource_type: ResourceType;
99
+ resource_id: string;
100
+ }>>;
101
+ acknowledgeInvalidation(event: Pick<DirectoryInvalidation, 'id' | 'lease_token'>): Promise<boolean>;
102
+ /** Preserve staged groups and unacknowledged invalidations while reclaiming bounded history. */
103
+ pruneObsolete(limit?: number): Promise<number>;
104
+ }
105
+ type FeedSource = Pick<ListingsClient, 'directory'>;
106
+ export declare class DirectorySyncWorker {
107
+ private readonly source;
108
+ private readonly target;
109
+ private readonly pageSize;
110
+ constructor(source: FeedSource, target: PostgresDirectoryTarget, pageSize?: number);
111
+ step(options?: {
112
+ signal?: AbortSignal;
113
+ }): Promise<{
114
+ status: 'progress' | 'idle' | 'conflict' | 'reset' | 'blocked';
115
+ applied: number;
116
+ }>;
117
+ private validateChange;
118
+ run(options: {
119
+ maxSteps: number;
120
+ signal?: AbortSignal;
121
+ }): Promise<Array<Awaited<ReturnType<DirectorySyncWorker['step']>>>>;
122
+ }
123
+ export {};