wp-native-client 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,41 +1,94 @@
1
1
  # wp-native-client
2
2
 
3
- Universal WordPress client built on the Abilities API.
3
+ Universal WordPress client built on the [Abilities API](https://make.wordpress.org/core/). Discovery + execution. Works in React Native apps, Gutenberg blocks, and Node scripts — one client, three transports, one ability surface.
4
4
 
5
- **Status:** scaffold only. Implementation lands in M3 (see [ROADMAP](../../docs/ROADMAP.md)).
5
+ ## Install
6
6
 
7
- ## What this package will provide
7
+ ```bash
8
+ # React Native or Node
9
+ npm install wp-native-client
8
10
 
9
- - `WPNativeClient`single universal client, no subclasses
10
- - `discoverAbilities()` — fetches the site's ability catalog
11
- - `execute(abilityName, args)` — invokes any ability by name
12
- - `AuthTransport` — token lifecycle (load / save / refresh / clear)
13
- - `WpApiFetchTransport` — adapter for `@wordpress/api-fetch` (Gutenberg blocks)
14
- - *(post-v0.1)* `generateTypes()` — CLI tool to codegen TypeScript types from ability JSON schemas
11
+ # Gutenberg blocks also import the /wordpress subpath (see below)
12
+ npm install wp-native-client
13
+ ```
15
14
 
16
- ## The mental model
15
+ No peer dependencies. The main entry point uses plain `fetch`. The `/wordpress` subpath entry wraps `@wordpress/api-fetch` for block contexts.
17
16
 
18
- The client doesn't know what abilities exist on a given WordPress site. **It asks.** The site is the source of truth, always current, self-describing.
17
+ ## Quick start
19
18
 
20
19
  ```ts
21
- import { WPNativeClient, AuthTransport } from 'wp-native-client';
20
+ import { WPNativeClient, AuthFetchTransport } from 'wp-native-client';
22
21
 
23
- const client = new WPNativeClient({
24
- baseUrl: 'https://extrachill.com/wp-json',
25
- transport: new AuthTransport({ /* ... */ }),
22
+ const transport = new AuthFetchTransport({
23
+ baseUrl: 'https://example.com/wp-json',
24
+ getDeviceId: () => 'your-uuid-v4',
25
+ loadTokens: async () => null,
26
+ saveTokens: async () => {},
27
+ clearTokens: async () => {},
28
+ onAuthFailure: () => console.log('session expired'),
26
29
  });
27
30
 
28
- // Discovery happens once at startup
29
- await client.discoverAbilities();
31
+ await transport.initialize();
32
+
33
+ const client = new WPNativeClient(transport);
34
+
35
+ // Discovery — fetches the site's ability catalog
36
+ await client.discover();
37
+
38
+ // Execution — call any ability by name
39
+ const me = await client.execute('wp-native/auth-me');
40
+ const posts = await client.execute('wp/post.list', { per_page: 20 });
41
+ ```
42
+
43
+ ### Three transports
44
+
45
+ | Transport | Use when | Import |
46
+ |---|---|---|
47
+ | **`AuthFetchTransport`** | React Native apps, Node scripts — manages token lifecycle (load, save, refresh, clear) | `wp-native-client` |
48
+ | **`FetchTransport`** | Unauthenticated or externally-managed auth | `wp-native-client` |
49
+ | **`WpApiFetchTransport`** | Gutenberg blocks — wraps `@wordpress/api-fetch`, nonce handling by WP core | `wp-native-client/wordpress` |
30
50
 
31
- // From there, anything any plugin registered is callable
32
- const posts = await client.execute('wp/post.list', { per_page: 20 });
33
- const artist = await client.execute('extrachill/artist.get', { id: 42 });
34
- const calendar = await client.execute('extrachill/event.calendar', { venue: 'continental-club' });
51
+ ```ts
52
+ // Gutenberg block example
53
+ import { WPNativeClient } from 'wp-native-client';
54
+ import { WpApiFetchTransport } from 'wp-native-client/wordpress';
55
+ import apiFetch from '@wordpress/api-fetch';
56
+
57
+ const client = new WPNativeClient(new WpApiFetchTransport(apiFetch));
58
+ await client.discover();
59
+ const posts = await client.execute('wp/post.list', { per_page: 10 });
35
60
  ```
36
61
 
37
- No hand-typed wrappers. No per-site subclasses. No drift between client and server.
62
+ ## Public API summary
63
+
64
+ ### Client
65
+
66
+ - **`WPNativeClient`** — the universal client. Wraps a transport, exposes `discover()`, `execute()`, `executeUnchecked()`, `describe()`, `catalog`.
67
+ - **`WPNativeClientConfig`** — optional config (`validateAbilityNames`).
68
+
69
+ ### Transports
70
+
71
+ - **`AuthFetchTransport`** — authenticated fetch with automatic token refresh and 401 retry.
72
+ - **`FetchTransport`** — plain fetch, no auth.
73
+ - **`WpApiFetchTransport`** — wraps `@wordpress/api-fetch` for block contexts. Separate entry point: `wp-native-client/wordpress`.
74
+ - **`ApiError`** — structured error with `code`, `message`, `status`.
75
+
76
+ ### Abilities
77
+
78
+ - **`discoverAbilities(transport, filter?)`** — walk the Abilities API, return an `AbilityCatalog`.
79
+ - **`AbilityCatalog`** — in-memory lookup of ability descriptors by name.
80
+ - **`AbilityDescriptor`** — metadata for a single ability (name, category, input/output schemas).
81
+
82
+ ### Types
83
+
84
+ - **`StoredTokens`** — `{ accessToken, refreshToken, accessExpiresAt }` (expiry is a Unix timestamp in seconds).
85
+ - **`Transport`**, **`TransportRequest`**, **`TransportResponse`** — transport interface contracts.
86
+ - **`AuthFetchTransportConfig`**, **`FetchTransportConfig`** — transport configuration shapes.
87
+
88
+ ## Broader project
89
+
90
+ wp-native-client is the abilities client layer of [wp-native](https://github.com/chubes4/wp-native) — an open-source framework for turning WordPress sites into real native apps. See the [main repo](https://github.com/chubes4/wp-native) for the shell, the auth plugin, and the full documentation.
38
91
 
39
- ## Why not a typed client per site?
92
+ ## License
40
93
 
41
- See the [ROADMAP](../../docs/ROADMAP.md#the-principle). Per-site typed clients defeat the purpose of a generic framework they'd require every consumer of every WordPress site to ship their own `@whatever/api-client`. Abilities discovery is the only pattern that delivers on "any WordPress site."
94
+ GPL-2.0-or-later[github.com/chubes4/wp-native](https://github.com/chubes4/wp-native)
@@ -22,6 +22,10 @@ export declare function fetchAbilitiesPage(transport: Transport, params?: Abilit
22
22
  * the whole catalog.
23
23
  */
24
24
  export declare function fetchAbility(transport: Transport, name: string): Promise<AbilityDescriptor>;
25
+ /**
26
+ * Encode an ability name without escaping its namespace separator.
27
+ */
28
+ export declare function encodeAbilityName(name: string): string;
25
29
  /**
26
30
  * Fetch all available ability categories.
27
31
  */
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../src/abilities/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,KAAK,EACV,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EAChB,MAAM,SAAS,CAAC;AAMjB;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,SAAS,EACpB,MAAM,GAAE,iBAAsB,GAC7B,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAY9B;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,CAAC,CAK5B;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,eAAe,EAAE,CAAC,CAK5B;AAED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,SAAS,EACpB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GACpD,OAAO,CAAC,cAAc,CAAC,CA0BzB"}
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../src/abilities/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,KAAK,EACV,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EAChB,MAAM,SAAS,CAAC;AAMjB;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,SAAS,EACpB,MAAM,GAAE,iBAAsB,GAC7B,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAY9B;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,CAAC,CAK5B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,eAAe,EAAE,CAAC,CAK5B;AAED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,SAAS,EACpB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GACpD,OAAO,CAAC,cAAc,CAAC,CA0BzB"}
@@ -35,10 +35,16 @@ export async function fetchAbilitiesPage(transport, params = {}) {
35
35
  */
36
36
  export async function fetchAbility(transport, name) {
37
37
  return transport.request({
38
- path: `${ABILITIES_PATH}/${encodeURIComponent(name)}`,
38
+ path: `${ABILITIES_PATH}/${encodeAbilityName(name)}`,
39
39
  method: 'GET',
40
40
  });
41
41
  }
42
+ /**
43
+ * Encode an ability name without escaping its namespace separator.
44
+ */
45
+ export function encodeAbilityName(name) {
46
+ return name.split('/').map(encodeURIComponent).join('/');
47
+ }
42
48
  /**
43
49
  * Fetch all available ability categories.
44
50
  */
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/abilities/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAO3C,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAC7B,MAAM,cAAc,GAAG,2BAA2B,CAAC;AACnD,MAAM,eAAe,GAAG,4BAA4B,CAAC;AAErD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,SAAoB,EACpB,SAA4B,EAAE;IAE9B,MAAM,KAAK,GAAG,IAAI,eAAe,EAAE,CAAC;IACpC,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,IAAI,gBAAgB,CAAC,CAAC,CAAC;IAClE,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5C,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC;IAED,OAAO,SAAS,CAAC,OAAO,CAAsB;QAC5C,IAAI,EAAE,GAAG,cAAc,IAAI,KAAK,CAAC,QAAQ,EAAE,EAAE;QAC7C,MAAM,EAAE,KAAK;KACd,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,SAAoB,EACpB,IAAY;IAEZ,OAAO,SAAS,CAAC,OAAO,CAAoB;QAC1C,IAAI,EAAE,GAAG,cAAc,IAAI,kBAAkB,CAAC,IAAI,CAAC,EAAE;QACrD,MAAM,EAAE,KAAK;KACd,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,SAAoB;IAEpB,OAAO,SAAS,CAAC,OAAO,CAAoB;QAC1C,IAAI,EAAE,eAAe;QACrB,MAAM,EAAE,KAAK;KACd,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,SAAoB,EACpB,UAAmD,EAAE;IAErD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,gBAAgB,CAAC;IACpD,MAAM,GAAG,GAAwB,EAAE,CAAC;IACpC,IAAI,IAAI,GAAG,CAAC,CAAC;IAEb,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,MAAM,GAAsB,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QACpD,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YACnC,MAAM,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACrC,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,kBAAkB,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAE1D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM;QACR,CAAC;QAED,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;QAEnB,IAAI,KAAK,CAAC,MAAM,GAAG,OAAO,EAAE,CAAC;YAC3B,MAAM;QACR,CAAC;QAED,IAAI,IAAI,CAAC,CAAC;IACZ,CAAC;IAED,OAAO,IAAI,cAAc,CAAC,GAAG,CAAC,CAAC;AACjC,CAAC"}
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/abilities/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAO3C,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAC7B,MAAM,cAAc,GAAG,2BAA2B,CAAC;AACnD,MAAM,eAAe,GAAG,4BAA4B,CAAC;AAErD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,SAAoB,EACpB,SAA4B,EAAE;IAE9B,MAAM,KAAK,GAAG,IAAI,eAAe,EAAE,CAAC;IACpC,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,IAAI,gBAAgB,CAAC,CAAC,CAAC;IAClE,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5C,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC;IAED,OAAO,SAAS,CAAC,OAAO,CAAsB;QAC5C,IAAI,EAAE,GAAG,cAAc,IAAI,KAAK,CAAC,QAAQ,EAAE,EAAE;QAC7C,MAAM,EAAE,KAAK;KACd,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,SAAoB,EACpB,IAAY;IAEZ,OAAO,SAAS,CAAC,OAAO,CAAoB;QAC1C,IAAI,EAAE,GAAG,cAAc,IAAI,iBAAiB,CAAC,IAAI,CAAC,EAAE;QACpD,MAAM,EAAE,KAAK;KACd,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3D,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,SAAoB;IAEpB,OAAO,SAAS,CAAC,OAAO,CAAoB;QAC1C,IAAI,EAAE,eAAe;QACrB,MAAM,EAAE,KAAK;KACd,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,SAAoB,EACpB,UAAmD,EAAE;IAErD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,gBAAgB,CAAC;IACpD,MAAM,GAAG,GAAwB,EAAE,CAAC;IACpC,IAAI,IAAI,GAAG,CAAC,CAAC;IAEb,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,MAAM,GAAsB,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QACpD,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YACnC,MAAM,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACrC,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,kBAAkB,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAE1D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM;QACR,CAAC;QAED,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;QAEnB,IAAI,KAAK,CAAC,MAAM,GAAG,OAAO,EAAE,CAAC;YAC3B,MAAM;QACR,CAAC;QAED,IAAI,IAAI,CAAC,CAAC;IACZ,CAAC;IAED,OAAO,IAAI,cAAc,CAAC,GAAG,CAAC,CAAC;AACjC,CAAC"}
@@ -4,7 +4,7 @@
4
4
  * Mirrors the response shapes documented at:
5
5
  * GET /wp-abilities/v1/abilities
6
6
  * GET /wp-abilities/v1/abilities/{name}
7
- * POST /wp-abilities/v1/abilities/{name}/run
7
+ * GET|POST|DELETE /wp-abilities/v1/abilities/{name}/run
8
8
  *
9
9
  * These types describe the wire format. They are intentionally permissive
10
10
  * about ability-specific input/output (modeled as `unknown`) because the
@@ -33,7 +33,7 @@ export interface AbilityDescriptor {
33
33
  category: string;
34
34
  /**
35
35
  * JSON Schema describing the shape of the `input` argument expected by
36
- * POST /abilities/{name}/run. May be an empty object for nullary abilities.
36
+ * /abilities/{name}/run. May be an empty object for nullary abilities.
37
37
  */
38
38
  input_schema: Record<string, unknown>;
39
39
  /**
@@ -57,10 +57,10 @@ export interface AbilityCategory {
57
57
  description: string;
58
58
  }
59
59
  /**
60
- * Successful response from POST /wp-abilities/v1/abilities/{name}/run.
60
+ * Legacy wrapped execution response type retained for source compatibility.
61
+ * WordPress currently returns the ability result directly from `/run`.
61
62
  *
62
- * The actual `result` shape is ability-specific; consumers narrow with
63
- * a type parameter at the `client.execute<TResult>()` call site.
63
+ * @deprecated Use the result type passed to `client.execute<TResult>()`.
64
64
  */
65
65
  export interface AbilityExecutionResponse<TResult = unknown> {
66
66
  result: TResult;
@@ -4,7 +4,7 @@
4
4
  * Mirrors the response shapes documented at:
5
5
  * GET /wp-abilities/v1/abilities
6
6
  * GET /wp-abilities/v1/abilities/{name}
7
- * POST /wp-abilities/v1/abilities/{name}/run
7
+ * GET|POST|DELETE /wp-abilities/v1/abilities/{name}/run
8
8
  *
9
9
  * These types describe the wire format. They are intentionally permissive
10
10
  * about ability-specific input/output (modeled as `unknown`) because the
package/dist/client.d.ts CHANGED
@@ -48,6 +48,7 @@ export declare class WPNativeClient {
48
48
  private readonly transport;
49
49
  private readonly config;
50
50
  private _catalog;
51
+ private readonly descriptors;
51
52
  constructor(transport: Transport, config?: WPNativeClientConfig);
52
53
  /**
53
54
  * Walk the Abilities API and populate the in-memory catalog.
@@ -81,11 +82,8 @@ export declare class WPNativeClient {
81
82
  * Execute an ability by name.
82
83
  *
83
84
  * The ability is looked up in the catalog (when validateAbilityNames is
84
- * enabled), then POSTed to /wp-abilities/v1/abilities/{name}/run with
85
- * the given input as the request body's `input` field.
86
- *
87
- * The response shape `{ result: TResult }` is unwrapped — callers receive
88
- * the result value directly.
85
+ * enabled), then sent to /wp-abilities/v1/abilities/{name}/run using the
86
+ * HTTP method required by the ability annotations.
89
87
  *
90
88
  * Throws:
91
89
  * - Error if the ability is not in the catalog (and validation enabled)
@@ -101,6 +99,7 @@ export declare class WPNativeClient {
101
99
  * the call site.
102
100
  */
103
101
  executeUnchecked<TResult = unknown, TInput = unknown>(name: string, input?: TInput): Promise<TResult>;
102
+ private executeDescriptor;
104
103
  /**
105
104
  * Get a single ability descriptor from the catalog.
106
105
  * Returns undefined if not registered or catalog not loaded.
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EACV,iBAAiB,EAElB,MAAM,mBAAmB,CAAC;AAI3B,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;;;OAUG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;IACxD,OAAO,CAAC,QAAQ,CAA+B;gBAEnC,SAAS,EAAE,SAAS,EAAE,MAAM,GAAE,oBAAyB;IAOnE;;;;;;;;;OASG;IACG,QAAQ,CAAC,OAAO,GAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,cAAc,CAAC;IAU5E;;;;OAIG;IACH,IAAI,OAAO,IAAI,cAAc,CAO5B;IAED;;;OAGG;IACH,aAAa,IAAI,cAAc,GAAG,IAAI;IAItC;;OAEG;IACH,UAAU,IAAI,OAAO;IAIrB;;;;;;;;;;;;;OAaG;IACG,OAAO,CAAC,OAAO,GAAG,OAAO,EAAE,MAAM,GAAG,OAAO,EAC/C,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC;IAcnB;;;;;;;OAOG;IACG,gBAAgB,CAAC,OAAO,GAAG,OAAO,EAAE,MAAM,GAAG,OAAO,EACxD,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC;IAwBnB;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;CAGtD"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EACV,iBAAiB,EAClB,MAAM,mBAAmB,CAAC;AAI3B,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;;;OAUG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;IACxD,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAwC;gBAExD,SAAS,EAAE,SAAS,EAAE,MAAM,GAAE,oBAAyB;IAOnE;;;;;;;;;OASG;IACG,QAAQ,CAAC,OAAO,GAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,cAAc,CAAC;IAa5E;;;;OAIG;IACH,IAAI,OAAO,IAAI,cAAc,CAO5B;IAED;;;OAGG;IACH,aAAa,IAAI,cAAc,GAAG,IAAI;IAItC;;OAEG;IACH,UAAU,IAAI,OAAO;IAIrB;;;;;;;;;;OAUG;IACG,OAAO,CAAC,OAAO,GAAG,OAAO,EAAE,MAAM,GAAG,OAAO,EAC/C,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC;IAoBnB;;;;;;;OAOG;IACG,gBAAgB,CAAC,OAAO,GAAG,OAAO,EAAE,MAAM,GAAG,OAAO,EACxD,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC;YAML,iBAAiB;IAgC/B;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;CAGtD"}
package/dist/client.js CHANGED
@@ -27,13 +27,13 @@
27
27
  * const posts = await client.execute<Post[]>('wp/post.list', { per_page: 20 });
28
28
  * const me = await client.execute<User>('wp-native/user.me');
29
29
  */
30
- import { ApiError } from './transports/fetch';
31
- import { discoverAbilities } from './abilities/discovery';
30
+ import { discoverAbilities, encodeAbilityName, fetchAbility } from './abilities/discovery';
32
31
  const RUN_PATH_PREFIX = 'wp-abilities/v1/abilities';
33
32
  export class WPNativeClient {
34
33
  transport;
35
34
  config;
36
35
  _catalog = null;
36
+ descriptors = new Map();
37
37
  constructor(transport, config = {}) {
38
38
  this.transport = transport;
39
39
  this.config = {
@@ -57,6 +57,9 @@ export class WPNativeClient {
57
57
  }
58
58
  const catalog = await discoverAbilities(this.transport, filter);
59
59
  this._catalog = catalog;
60
+ for (const ability of catalog.all()) {
61
+ this.descriptors.set(ability.name, ability);
62
+ }
60
63
  return catalog;
61
64
  }
62
65
  /**
@@ -87,11 +90,8 @@ export class WPNativeClient {
87
90
  * Execute an ability by name.
88
91
  *
89
92
  * The ability is looked up in the catalog (when validateAbilityNames is
90
- * enabled), then POSTed to /wp-abilities/v1/abilities/{name}/run with
91
- * the given input as the request body's `input` field.
92
- *
93
- * The response shape `{ result: TResult }` is unwrapped — callers receive
94
- * the result value directly.
93
+ * enabled), then sent to /wp-abilities/v1/abilities/{name}/run using the
94
+ * HTTP method required by the ability annotations.
95
95
  *
96
96
  * Throws:
97
97
  * - Error if the ability is not in the catalog (and validation enabled)
@@ -105,7 +105,12 @@ export class WPNativeClient {
105
105
  `to bypass validation.`);
106
106
  }
107
107
  }
108
- return this.executeUnchecked(name, input);
108
+ let descriptor = this._catalog?.get(name) ?? this.descriptors.get(name);
109
+ if (!descriptor) {
110
+ descriptor = await fetchAbility(this.transport, name);
111
+ this.descriptors.set(name, descriptor);
112
+ }
113
+ return this.executeDescriptor(descriptor, input);
109
114
  }
110
115
  /**
111
116
  * Execute an ability without checking the catalog.
@@ -116,20 +121,35 @@ export class WPNativeClient {
116
121
  * the call site.
117
122
  */
118
123
  async executeUnchecked(name, input) {
119
- const path = `${RUN_PATH_PREFIX}/${encodeURIComponent(name)}/run`;
124
+ const descriptor = await fetchAbility(this.transport, name);
125
+ this.descriptors.set(name, descriptor);
126
+ return this.executeDescriptor(descriptor, input);
127
+ }
128
+ async executeDescriptor(descriptor, input) {
129
+ const annotations = descriptor.meta?.annotations;
130
+ const annotationMap = annotations && typeof annotations === 'object' ? annotations : {};
131
+ const method = annotationMap.readonly
132
+ ? 'GET'
133
+ : annotationMap.destructive && annotationMap.idempotent
134
+ ? 'DELETE'
135
+ : 'POST';
136
+ let path = `${RUN_PATH_PREFIX}/${encodeAbilityName(descriptor.name)}/run`;
120
137
  const body = {
121
138
  input: input === undefined ? null : input,
122
139
  };
123
- const response = await this.transport.request({
140
+ if (method !== 'POST') {
141
+ const query = new URLSearchParams();
142
+ appendQueryValue(query, 'input', body.input);
143
+ const queryString = query.toString();
144
+ if (queryString) {
145
+ path += `?${queryString}`;
146
+ }
147
+ }
148
+ return this.transport.request({
124
149
  path,
125
- method: 'POST',
126
- body: body,
150
+ method,
151
+ ...(method === 'POST' ? { body: body } : {}),
127
152
  });
128
- if (!response || typeof response !== 'object' || !('result' in response)) {
129
- throw new ApiError(`WPNativeClient: malformed ability response for "${name}". ` +
130
- `Expected { result: ... }.`, 'malformed_ability_response', 500);
131
- }
132
- return response.result;
133
153
  }
134
154
  /**
135
155
  * Get a single ability descriptor from the catalog.
@@ -139,4 +159,20 @@ export class WPNativeClient {
139
159
  return this._catalog?.get(name);
140
160
  }
141
161
  }
162
+ function appendQueryValue(query, key, value) {
163
+ if (value === null || value === undefined) {
164
+ return;
165
+ }
166
+ if (Array.isArray(value)) {
167
+ value.forEach((item, index) => appendQueryValue(query, `${key}[${index}]`, item));
168
+ return;
169
+ }
170
+ if (typeof value === 'object') {
171
+ Object.entries(value).forEach(([childKey, childValue]) => {
172
+ appendQueryValue(query, `${key}[${childKey}]`, childValue);
173
+ });
174
+ return;
175
+ }
176
+ query.append(key, String(value));
177
+ }
142
178
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wp-native-client",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Universal WordPress client built on the Abilities API. Discovery + execution. Works in WordPress blocks, React Native, and Node — one client, three transports, one ability surface.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -52,11 +52,18 @@ export async function fetchAbility(
52
52
  name: string,
53
53
  ): Promise<AbilityDescriptor> {
54
54
  return transport.request<AbilityDescriptor>({
55
- path: `${ABILITIES_PATH}/${encodeURIComponent(name)}`,
55
+ path: `${ABILITIES_PATH}/${encodeAbilityName(name)}`,
56
56
  method: 'GET',
57
57
  });
58
58
  }
59
59
 
60
+ /**
61
+ * Encode an ability name without escaping its namespace separator.
62
+ */
63
+ export function encodeAbilityName(name: string): string {
64
+ return name.split('/').map(encodeURIComponent).join('/');
65
+ }
66
+
60
67
  /**
61
68
  * Fetch all available ability categories.
62
69
  */
@@ -4,7 +4,7 @@
4
4
  * Mirrors the response shapes documented at:
5
5
  * GET /wp-abilities/v1/abilities
6
6
  * GET /wp-abilities/v1/abilities/{name}
7
- * POST /wp-abilities/v1/abilities/{name}/run
7
+ * GET|POST|DELETE /wp-abilities/v1/abilities/{name}/run
8
8
  *
9
9
  * These types describe the wire format. They are intentionally permissive
10
10
  * about ability-specific input/output (modeled as `unknown`) because the
@@ -38,7 +38,7 @@ export interface AbilityDescriptor {
38
38
 
39
39
  /**
40
40
  * JSON Schema describing the shape of the `input` argument expected by
41
- * POST /abilities/{name}/run. May be an empty object for nullary abilities.
41
+ * /abilities/{name}/run. May be an empty object for nullary abilities.
42
42
  */
43
43
  input_schema: Record<string, unknown>;
44
44
 
@@ -66,10 +66,10 @@ export interface AbilityCategory {
66
66
  }
67
67
 
68
68
  /**
69
- * Successful response from POST /wp-abilities/v1/abilities/{name}/run.
69
+ * Legacy wrapped execution response type retained for source compatibility.
70
+ * WordPress currently returns the ability result directly from `/run`.
70
71
  *
71
- * The actual `result` shape is ability-specific; consumers narrow with
72
- * a type parameter at the `client.execute<TResult>()` call site.
72
+ * @deprecated Use the result type passed to `client.execute<TResult>()`.
73
73
  */
74
74
  export interface AbilityExecutionResponse<TResult = unknown> {
75
75
  result: TResult;
package/src/client.ts CHANGED
@@ -29,12 +29,10 @@
29
29
  */
30
30
 
31
31
  import type { Transport } from './transports/types';
32
- import { ApiError } from './transports/fetch';
33
32
  import { AbilityCatalog } from './abilities/catalog';
34
- import { discoverAbilities } from './abilities/discovery';
33
+ import { discoverAbilities, encodeAbilityName, fetchAbility } from './abilities/discovery';
35
34
  import type {
36
35
  AbilityDescriptor,
37
- AbilityExecutionResponse,
38
36
  } from './abilities/types';
39
37
 
40
38
  const RUN_PATH_PREFIX = 'wp-abilities/v1/abilities';
@@ -58,6 +56,7 @@ export class WPNativeClient {
58
56
  private readonly transport: Transport;
59
57
  private readonly config: Required<WPNativeClientConfig>;
60
58
  private _catalog: AbilityCatalog | null = null;
59
+ private readonly descriptors = new Map<string, AbilityDescriptor>();
61
60
 
62
61
  constructor(transport: Transport, config: WPNativeClientConfig = {}) {
63
62
  this.transport = transport;
@@ -83,6 +82,9 @@ export class WPNativeClient {
83
82
  }
84
83
  const catalog = await discoverAbilities(this.transport, filter);
85
84
  this._catalog = catalog;
85
+ for (const ability of catalog.all()) {
86
+ this.descriptors.set(ability.name, ability);
87
+ }
86
88
  return catalog;
87
89
  }
88
90
 
@@ -119,11 +121,8 @@ export class WPNativeClient {
119
121
  * Execute an ability by name.
120
122
  *
121
123
  * The ability is looked up in the catalog (when validateAbilityNames is
122
- * enabled), then POSTed to /wp-abilities/v1/abilities/{name}/run with
123
- * the given input as the request body's `input` field.
124
- *
125
- * The response shape `{ result: TResult }` is unwrapped — callers receive
126
- * the result value directly.
124
+ * enabled), then sent to /wp-abilities/v1/abilities/{name}/run using the
125
+ * HTTP method required by the ability annotations.
127
126
  *
128
127
  * Throws:
129
128
  * - Error if the ability is not in the catalog (and validation enabled)
@@ -143,7 +142,13 @@ export class WPNativeClient {
143
142
  }
144
143
  }
145
144
 
146
- return this.executeUnchecked<TResult, TInput>(name, input);
145
+ let descriptor = this._catalog?.get(name) ?? this.descriptors.get(name);
146
+ if (!descriptor) {
147
+ descriptor = await fetchAbility(this.transport, name);
148
+ this.descriptors.set(name, descriptor);
149
+ }
150
+
151
+ return this.executeDescriptor<TResult, TInput>(descriptor, input);
147
152
  }
148
153
 
149
154
  /**
@@ -158,27 +163,41 @@ export class WPNativeClient {
158
163
  name: string,
159
164
  input?: TInput,
160
165
  ): Promise<TResult> {
161
- const path = `${RUN_PATH_PREFIX}/${encodeURIComponent(name)}/run`;
166
+ const descriptor = await fetchAbility(this.transport, name);
167
+ this.descriptors.set(name, descriptor);
168
+ return this.executeDescriptor<TResult, TInput>(descriptor, input);
169
+ }
170
+
171
+ private async executeDescriptor<TResult, TInput>(
172
+ descriptor: AbilityDescriptor,
173
+ input?: TInput,
174
+ ): Promise<TResult> {
175
+ const annotations = descriptor.meta?.annotations;
176
+ const annotationMap = annotations && typeof annotations === 'object' ? annotations : {};
177
+ const method = annotationMap.readonly
178
+ ? 'GET'
179
+ : annotationMap.destructive && annotationMap.idempotent
180
+ ? 'DELETE'
181
+ : 'POST';
182
+ let path = `${RUN_PATH_PREFIX}/${encodeAbilityName(descriptor.name)}/run`;
162
183
  const body: { input: TInput | null } = {
163
184
  input: input === undefined ? null : input,
164
185
  };
165
186
 
166
- const response = await this.transport.request<AbilityExecutionResponse<TResult>>({
167
- path,
168
- method: 'POST',
169
- body: body as Record<string, unknown>,
170
- });
171
-
172
- if (!response || typeof response !== 'object' || !('result' in response)) {
173
- throw new ApiError(
174
- `WPNativeClient: malformed ability response for "${name}". ` +
175
- `Expected { result: ... }.`,
176
- 'malformed_ability_response',
177
- 500,
178
- );
187
+ if (method !== 'POST') {
188
+ const query = new URLSearchParams();
189
+ appendQueryValue(query, 'input', body.input);
190
+ const queryString = query.toString();
191
+ if (queryString) {
192
+ path += `?${queryString}`;
193
+ }
179
194
  }
180
195
 
181
- return response.result;
196
+ return this.transport.request<TResult>({
197
+ path,
198
+ method,
199
+ ...(method === 'POST' ? { body: body as Record<string, unknown> } : {}),
200
+ });
182
201
  }
183
202
 
184
203
  /**
@@ -189,3 +208,27 @@ export class WPNativeClient {
189
208
  return this._catalog?.get(name);
190
209
  }
191
210
  }
211
+
212
+ function appendQueryValue(
213
+ query: URLSearchParams,
214
+ key: string,
215
+ value: unknown,
216
+ ): void {
217
+ if (value === null || value === undefined) {
218
+ return;
219
+ }
220
+
221
+ if (Array.isArray(value)) {
222
+ value.forEach((item, index) => appendQueryValue(query, `${key}[${index}]`, item));
223
+ return;
224
+ }
225
+
226
+ if (typeof value === 'object') {
227
+ Object.entries(value).forEach(([childKey, childValue]) => {
228
+ appendQueryValue(query, `${key}[${childKey}]`, childValue);
229
+ });
230
+ return;
231
+ }
232
+
233
+ query.append(key, String(value));
234
+ }