@erseco/code-snippets-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.
- package/LICENSE +674 -0
- package/README.md +81 -0
- package/dist/auth.d.ts +3 -0
- package/dist/auth.js +76 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +19 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +219 -0
- package/dist/client.d.ts +25 -0
- package/dist/client.js +233 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +11 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/session.d.ts +10 -0
- package/dist/session.js +98 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.js +1 -0
- package/docs/api.md +75 -0
- package/docs/architecture.md +41 -0
- package/docs/authentication.md +85 -0
- package/docs/development.md +49 -0
- package/package.json +79 -0
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type ErrorCode = "CONFIG" | "AUTH" | "HTTP" | "NETWORK" | "RESPONSE" | "VALIDATION" | "STATE";
|
|
2
|
+
/** Never includes HTTP bodies, cookies, passwords or ticket URLs. */
|
|
3
|
+
export declare class CodeSnippetsError extends Error {
|
|
4
|
+
readonly code: ErrorCode;
|
|
5
|
+
readonly status?: number | undefined;
|
|
6
|
+
constructor(code: ErrorCode, message: string, status?: number | undefined);
|
|
7
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Never includes HTTP bodies, cookies, passwords or ticket URLs. */
|
|
2
|
+
export class CodeSnippetsError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
constructor(code, message, status) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.name = "CodeSnippetsError";
|
|
10
|
+
}
|
|
11
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function checkedUrl(value: string, allowHttp?: boolean): URL;
|
|
2
|
+
/** In-memory session. Redirects may only visit configured origins. */
|
|
3
|
+
export declare class Session {
|
|
4
|
+
private readonly origins;
|
|
5
|
+
private readonly timeoutMs;
|
|
6
|
+
private readonly allowHttp;
|
|
7
|
+
private readonly cookies;
|
|
8
|
+
constructor(origins: Set<string>, timeoutMs: number, allowHttp: boolean);
|
|
9
|
+
request(input: URL, init?: RequestInit, follow?: boolean): Promise<Response>;
|
|
10
|
+
}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { CookieJar } from "tough-cookie";
|
|
2
|
+
import { CodeSnippetsError } from "./errors.js";
|
|
3
|
+
export function checkedUrl(value, allowHttp = false) {
|
|
4
|
+
let url;
|
|
5
|
+
try {
|
|
6
|
+
url = new URL(value);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
throw new CodeSnippetsError("CONFIG", "Invalid URL");
|
|
10
|
+
}
|
|
11
|
+
if (url.username ||
|
|
12
|
+
url.password ||
|
|
13
|
+
url.hash ||
|
|
14
|
+
!["https:", ...(allowHttp ? ["http:"] : [])].includes(url.protocol)) {
|
|
15
|
+
throw new CodeSnippetsError("CONFIG", "Use HTTPS URLs without credentials or fragments");
|
|
16
|
+
}
|
|
17
|
+
return url;
|
|
18
|
+
}
|
|
19
|
+
/** In-memory session. Redirects may only visit configured origins. */
|
|
20
|
+
export class Session {
|
|
21
|
+
origins;
|
|
22
|
+
timeoutMs;
|
|
23
|
+
allowHttp;
|
|
24
|
+
cookies = new CookieJar();
|
|
25
|
+
constructor(origins, timeoutMs, allowHttp) {
|
|
26
|
+
this.origins = origins;
|
|
27
|
+
this.timeoutMs = timeoutMs;
|
|
28
|
+
this.allowHttp = allowHttp;
|
|
29
|
+
}
|
|
30
|
+
async request(input, init = {}, follow = true) {
|
|
31
|
+
let url = input;
|
|
32
|
+
let method = init.method ?? "GET";
|
|
33
|
+
let body = init.body;
|
|
34
|
+
const headers = new Headers(init.headers);
|
|
35
|
+
const signal = AbortSignal.timeout(this.timeoutMs);
|
|
36
|
+
const explicitCookie = headers.get("cookie");
|
|
37
|
+
for (let step = 0; step <= 10; step++) {
|
|
38
|
+
checkedUrl(url.href, this.allowHttp);
|
|
39
|
+
if (!this.origins.has(url.origin))
|
|
40
|
+
throw new CodeSnippetsError("AUTH", "Redirect to an unconfigured origin refused");
|
|
41
|
+
const cookie = await this.cookies.getCookieString(url.href);
|
|
42
|
+
headers.delete("cookie");
|
|
43
|
+
const combinedCookie = [
|
|
44
|
+
cookie,
|
|
45
|
+
url.origin === input.origin ? explicitCookie : null,
|
|
46
|
+
]
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.join("; ");
|
|
49
|
+
if (combinedCookie)
|
|
50
|
+
headers.set("cookie", combinedCookie);
|
|
51
|
+
let response;
|
|
52
|
+
try {
|
|
53
|
+
response = await fetch(url, {
|
|
54
|
+
...init,
|
|
55
|
+
method,
|
|
56
|
+
body: body ?? null,
|
|
57
|
+
headers,
|
|
58
|
+
redirect: "manual",
|
|
59
|
+
signal,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new CodeSnippetsError("NETWORK", "Request failed or timed out");
|
|
64
|
+
}
|
|
65
|
+
for (const value of response.headers.getSetCookie()) {
|
|
66
|
+
await this.cookies.setCookie(value, url.href, { ignoreError: true });
|
|
67
|
+
}
|
|
68
|
+
if (![301, 302, 303, 307, 308].includes(response.status))
|
|
69
|
+
return response;
|
|
70
|
+
const location = response.headers.get("location");
|
|
71
|
+
if (!follow || !location) {
|
|
72
|
+
await response.body?.cancel();
|
|
73
|
+
throw new CodeSnippetsError("AUTH", "Unexpected redirect; check the session and site URL", response.status);
|
|
74
|
+
}
|
|
75
|
+
const next = new URL(location, url);
|
|
76
|
+
// A 307/308 must never replay a password form to another origin.
|
|
77
|
+
if (next.origin !== url.origin) {
|
|
78
|
+
if (method !== "GET" &&
|
|
79
|
+
method !== "HEAD" &&
|
|
80
|
+
[307, 308].includes(response.status)) {
|
|
81
|
+
await response.body?.cancel();
|
|
82
|
+
throw new CodeSnippetsError("AUTH", "Cross-origin credential replay refused");
|
|
83
|
+
}
|
|
84
|
+
headers.delete("authorization");
|
|
85
|
+
headers.delete("x-wp-nonce");
|
|
86
|
+
}
|
|
87
|
+
if (response.status === 303 ||
|
|
88
|
+
([301, 302].includes(response.status) && method === "POST")) {
|
|
89
|
+
method = "GET";
|
|
90
|
+
body = null;
|
|
91
|
+
headers.delete("content-type");
|
|
92
|
+
}
|
|
93
|
+
await response.body?.cancel();
|
|
94
|
+
url = next;
|
|
95
|
+
}
|
|
96
|
+
throw new CodeSnippetsError("AUTH", "Too many login redirects");
|
|
97
|
+
}
|
|
98
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Writable Code Snippets fields. IDs belong to the destination site. */
|
|
2
|
+
export interface SnippetInput {
|
|
3
|
+
name: string;
|
|
4
|
+
code: string;
|
|
5
|
+
desc?: string;
|
|
6
|
+
scope?: string;
|
|
7
|
+
priority?: number;
|
|
8
|
+
tags?: string[];
|
|
9
|
+
active?: boolean;
|
|
10
|
+
shared_network?: boolean;
|
|
11
|
+
condition_id?: number;
|
|
12
|
+
locked?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface Snippet extends SnippetInput {
|
|
15
|
+
id: number;
|
|
16
|
+
desc: string;
|
|
17
|
+
scope: string;
|
|
18
|
+
priority: number;
|
|
19
|
+
tags: string[];
|
|
20
|
+
active: boolean;
|
|
21
|
+
network: boolean;
|
|
22
|
+
trashed: boolean;
|
|
23
|
+
code_error?: unknown;
|
|
24
|
+
}
|
|
25
|
+
export type Authentication = {
|
|
26
|
+
type: "application-password";
|
|
27
|
+
username: string;
|
|
28
|
+
password: string;
|
|
29
|
+
} | {
|
|
30
|
+
type: "wordpress";
|
|
31
|
+
username: string;
|
|
32
|
+
password: string;
|
|
33
|
+
loginUrl?: string;
|
|
34
|
+
} | {
|
|
35
|
+
type: "cas";
|
|
36
|
+
username: string;
|
|
37
|
+
password: string;
|
|
38
|
+
loginUrl: string;
|
|
39
|
+
serviceUrl?: string;
|
|
40
|
+
entryUrl?: string;
|
|
41
|
+
} | {
|
|
42
|
+
type: "session";
|
|
43
|
+
cookie: string;
|
|
44
|
+
nonce: string;
|
|
45
|
+
};
|
|
46
|
+
export interface ClientOptions {
|
|
47
|
+
baseUrl: string;
|
|
48
|
+
auth: Authentication;
|
|
49
|
+
network?: boolean;
|
|
50
|
+
adminUrl?: string;
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
/** Explicit HTTP opt-in for disposable local environments. */
|
|
53
|
+
allowInsecureHttp?: boolean;
|
|
54
|
+
}
|
|
55
|
+
export interface ListOptions {
|
|
56
|
+
page?: number;
|
|
57
|
+
perPage?: number;
|
|
58
|
+
search?: string;
|
|
59
|
+
status?: "all" | "active" | "inactive";
|
|
60
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# API and CLI
|
|
2
|
+
|
|
3
|
+
## Library
|
|
4
|
+
|
|
5
|
+
All methods are asynchronous. `login()` initializes authentication headers or a
|
|
6
|
+
session. Application password validity is checked on the first REST request.
|
|
7
|
+
API methods initialize authentication automatically and share an in-progress login.
|
|
8
|
+
|
|
9
|
+
| Method | Result |
|
|
10
|
+
| --------------------------------------------- | --------------------------------------------------- |
|
|
11
|
+
| `list({ page?, perPage?, search?, status? })` | `Snippet[]` |
|
|
12
|
+
| `get(id)` | `Snippet` |
|
|
13
|
+
| `create({ name, code, ... })` | `Snippet`, inactive unless `active: true` |
|
|
14
|
+
| `update(id, changes)` | `Snippet` |
|
|
15
|
+
| `activate(id)` / `deactivate(id)` | `Snippet` fetched after the operation |
|
|
16
|
+
| `delete(id)` | Trashed snippet, or `null` after permanent deletion |
|
|
17
|
+
| `restore(id)` | `Snippet` fetched after restoration |
|
|
18
|
+
|
|
19
|
+
IDs are positive integers local to each destination. Writable fields: `name`,
|
|
20
|
+
`code`, `desc`, `scope`, `priority`, `tags`, `active`, `shared_network`, `condition_id`
|
|
21
|
+
and `locked`. `network` belongs to the client configuration. Additional scopes and
|
|
22
|
+
fields may require a specific plugin edition.
|
|
23
|
+
|
|
24
|
+
Without explicit pagination, Code Snippets 3.10.2 returns the complete collection.
|
|
25
|
+
With `page`/`perPage`, only that page is returned; there is no implicit pagination
|
|
26
|
+
loop. `perPage` must be between 1 and 100. `status` accepts `all`, `active`, `inactive`.
|
|
27
|
+
|
|
28
|
+
`update` reads remote state and preserves known fields you did not specify. Avoid
|
|
29
|
+
concurrent updates of the same snippet: GET/POST is not a transaction. If saving an
|
|
30
|
+
active snippet deactivates it without reporting a code error, the client attempts
|
|
31
|
+
to restore activation once and verifies the result. When changing from/to
|
|
32
|
+
`single-use`, `active` is omitted unless explicitly supplied, and execution is never
|
|
33
|
+
retried. An explicit single-use activation may return `active: false` because the
|
|
34
|
+
verification read already consumed the execution; verify its application-specific effect.
|
|
35
|
+
|
|
36
|
+
`delete` follows plugin semantics: trash first, then permanently delete if already
|
|
37
|
+
trashed. It never retries automatically.
|
|
38
|
+
|
|
39
|
+
`CodeSnippetsError` provides `code`, `message` and an optional HTTP `status`.
|
|
40
|
+
Codes: `CONFIG`, `VALIDATION`, `AUTH`, `HTTP`, `NETWORK`, `RESPONSE`, `STATE`.
|
|
41
|
+
Errors do not include HTTP bodies, tickets, cookies or PHP source returned by the
|
|
42
|
+
server. A network error during a write does not prove the write was not applied.
|
|
43
|
+
|
|
44
|
+
## CLI
|
|
45
|
+
|
|
46
|
+
`wp-code-snippets --help` lists all arguments. The library accepts code as supplied;
|
|
47
|
+
with `--file`, the CLI normalizes CRLF and strips only boundary PHP tags.
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
wp-code-snippets get 42 --env-file .env
|
|
51
|
+
wp-code-snippets pull 42 --output backup.php --env-file .env
|
|
52
|
+
wp-code-snippets update 42 --input changes.json --env-file .env --yes
|
|
53
|
+
wp-code-snippets activate 42 --env-file .env --yes
|
|
54
|
+
wp-code-snippets deactivate 42 --env-file .env --yes
|
|
55
|
+
wp-code-snippets delete 42 --env-file .env --yes
|
|
56
|
+
wp-code-snippets restore 42 --env-file .env --yes
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`--input` accepts a JSON object; `--input -` reads it from stdin, useful for Python
|
|
60
|
+
consumers. Explicit field arguments override JSON fields. `--output` never overwrites
|
|
61
|
+
an existing file. Without it, `pull` writes raw code to stdout. `push` aliases `update`
|
|
62
|
+
and always requires an ID.
|
|
63
|
+
|
|
64
|
+
`--dry-run` describes a write without applying it. It is not a signed plan and does
|
|
65
|
+
not detect remote changes. Consumers choose versions and files to publish. The CLI
|
|
66
|
+
does not publish entire directories, resolve ambiguous names or create Git commits.
|
|
67
|
+
|
|
68
|
+
## Python consumers
|
|
69
|
+
|
|
70
|
+
Install the npm package in the consuming project. Python can invoke
|
|
71
|
+
`node node_modules/@erseco/code-snippets-client/dist/bin.js` with an argument list,
|
|
72
|
+
parse stdout JSON and check the exit status. Pass configuration through the
|
|
73
|
+
environment and changes through stdin (`--input -`); never interpolate passwords
|
|
74
|
+
or PHP into a shell command. Each invocation keeps one session; separate invocations
|
|
75
|
+
authenticate again.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Architecture and compatibility
|
|
2
|
+
|
|
3
|
+
Initial decision, 2026-09-13. AI assistance: Codex.
|
|
4
|
+
|
|
5
|
+
One npm package contains the library and CLI. `src/client.ts` adapts the REST API,
|
|
6
|
+
`src/auth.ts` handles authentication, and `src/session.ts` handles transport, cookies
|
|
7
|
+
and redirects. The CLI lives in `src/cli.ts`, with its entry point in `src/bin.ts`.
|
|
8
|
+
|
|
9
|
+
The client uses Node's native `fetch` and TypeScript compiled with `tsc`, without
|
|
10
|
+
a bundler. `tough-cookie` implements cookie rules and `cheerio/slim` parses HTML
|
|
11
|
+
forms, avoiding custom cookie and attribute parsers. The package is ESM and Node-only.
|
|
12
|
+
It includes no plugin PHP source, institutional configuration or backup data.
|
|
13
|
+
|
|
14
|
+
Consumers own deployment selection and ordering. There is no generic provider layer,
|
|
15
|
+
server, UI, Git synchronization or telemetry. No application-specific WordPress
|
|
16
|
+
business rules belong in this client.
|
|
17
|
+
|
|
18
|
+
## Verified contract
|
|
19
|
+
|
|
20
|
+
The reference is Code Snippets 3.10.2 installed from WordPress.org, specifically
|
|
21
|
+
`php/REST_API/Snippets/Snippets_REST_Controller.php`. The development tree may change
|
|
22
|
+
before release: [upstream controller](https://github.com/codesnippetspro/code-snippets/blob/core-beta/src/php/REST_API/Snippets/Snippets_REST_Controller.php).
|
|
23
|
+
|
|
24
|
+
- Requests use `?rest_route=/code-snippets/v1/snippets`, supporting plain permalinks.
|
|
25
|
+
- Mutations use POST, accepted by `WP_REST_Server::EDITABLE`.
|
|
26
|
+
- In 3.10.2, activation/deactivation serialize the model as `{}`. A subsequent GET
|
|
27
|
+
normalizes the result and verifies persisted state.
|
|
28
|
+
- Restoration returns HTTP 204; the client also fetches the snippet afterward.
|
|
29
|
+
- The first deletion trashes the snippet; the next permanently deletes it.
|
|
30
|
+
- HTTP 200 alone is not sufficient evidence of successful activation.
|
|
31
|
+
|
|
32
|
+
## Test boundaries
|
|
33
|
+
|
|
34
|
+
Local integration covers WordPress 7.1 / PHP 8.3 / Code Snippets 3.10.2, WordPress
|
|
35
|
+
login, application passwords, permissions and PHP snippet operations. Subsite routing
|
|
36
|
+
and network parameters are tested with HTTP simulations. Initial integration does
|
|
37
|
+
not demonstrate a real multisite network or Pro features.
|
|
38
|
+
|
|
39
|
+
CAS tests simulate forms and callbacks based on Cassify's flow. They have not run
|
|
40
|
+
against a production Apereo/Cassify deployment, MFA or every possible CAS theme.
|
|
41
|
+
Extend the relevant tests before claiming additional compatibility.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Authentication
|
|
2
|
+
|
|
3
|
+
Set `baseUrl` to the complete site or subsite URL, without query parameters.
|
|
4
|
+
`network: true` selects network snippets. Set `adminUrl` when the administration
|
|
5
|
+
path differs; it must use the same origin as WordPress.
|
|
6
|
+
HTTPS is required unless `allowInsecureHttp: true` explicitly enables local HTTP tests.
|
|
7
|
+
|
|
8
|
+
## Application passwords
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
const auth = {
|
|
12
|
+
type: "application-password" as const,
|
|
13
|
+
username: process.env.WP_USERNAME!,
|
|
14
|
+
password: process.env.WP_PASSWORD!,
|
|
15
|
+
};
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Use a WordPress application password, not the user's regular password. Application
|
|
19
|
+
passwords must be enabled, and the account needs the plugin's permissions.
|
|
20
|
+
[WordPress REST authentication](https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/).
|
|
21
|
+
|
|
22
|
+
## WordPress login
|
|
23
|
+
|
|
24
|
+
`auth: { type: 'wordpress', username, password }` retrieves the `wp-login.php` form,
|
|
25
|
+
preserves cookies and obtains a nonce from the snippet administration page.
|
|
26
|
+
An optional `loginUrl` supports a custom login path on the same origin.
|
|
27
|
+
It does not run JavaScript or complete MFA, CAPTCHA or interactive SSO challenges.
|
|
28
|
+
|
|
29
|
+
## CAS / Cassify
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { CodeSnippetsClient } from "@erseco/code-snippets-client";
|
|
33
|
+
|
|
34
|
+
const client = new CodeSnippetsClient({
|
|
35
|
+
baseUrl: "https://wordpress.example/subsite",
|
|
36
|
+
auth: {
|
|
37
|
+
type: "cas",
|
|
38
|
+
username: process.env.WP_USERNAME!,
|
|
39
|
+
password: process.env.WP_PASSWORD!,
|
|
40
|
+
loginUrl: "https://login.example/cas/login",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
By default, the client opens WordPress `wp-login.php` and follows the redirect
|
|
46
|
+
configured by Cassify. It preserves hidden fields, including `execution` and `lt`
|
|
47
|
+
when present, submits `username`, `password` and `_eventId=submit`, then follows the
|
|
48
|
+
service ticket back to WordPress. **WordPress/Cassify validates the ticket**; this
|
|
49
|
+
library is an HTTP form client, not a CAS server or service-ticket validator.
|
|
50
|
+
|
|
51
|
+
- `entryUrl`: alternate entry point on the WordPress origin.
|
|
52
|
+
- `serviceUrl`: when set, starts directly at `loginUrl?service=...`. Only use a
|
|
53
|
+
callback accepted by your WordPress setup; some deployments need a PHP session
|
|
54
|
+
initialized through WordPress first.
|
|
55
|
+
- No images or scripts are executed, and no additional challenges are completed.
|
|
56
|
+
- Forms must use the conventional field names above. Custom field names, MFA,
|
|
57
|
+
dynamically generated forms and the CAS REST/TGT protocol are not supported.
|
|
58
|
+
- CI simulates login and callback without production credentials. It does not
|
|
59
|
+
certify universal compatibility with Apereo or Cassify deployments.
|
|
60
|
+
|
|
61
|
+
Reference flow:
|
|
62
|
+
[WP Cassify](https://github.com/WP-Cassify/wp-cassify-develop/blob/main/wp-cassify/classes/wp_cassify_plugin.php).
|
|
63
|
+
|
|
64
|
+
## Existing sessions
|
|
65
|
+
|
|
66
|
+
`auth: { type: 'session', cookie, nonce }` accepts a session obtained externally,
|
|
67
|
+
including after an interactive login. These values are not persisted. The nonce
|
|
68
|
+
must belong to that session and the user must have the required permissions.
|
|
69
|
+
|
|
70
|
+
## Session boundaries and failures
|
|
71
|
+
|
|
72
|
+
Cookies remain in memory using `tough-cookie`. Redirects are restricted to the
|
|
73
|
+
configured WordPress and CAS origins. A form cannot submit a password to another
|
|
74
|
+
origin; cross-origin 307/308 redirects cannot replay its body either. Cookies follow
|
|
75
|
+
domain and path rules: different ports do not isolate cookies.
|
|
76
|
+
|
|
77
|
+
REST requests do not follow redirects to login forms. An expired session fails;
|
|
78
|
+
create a new client to authenticate again. Writes with uncertain outcomes are not
|
|
79
|
+
retried automatically. `timeoutMs` defaults to 30,000. Redirects share their request's
|
|
80
|
+
timeout and are limited to ten hops.
|
|
81
|
+
|
|
82
|
+
The CLI only loads an explicit `--env-file .env`. Use `WP_AUTH=cas` with
|
|
83
|
+
`CAS_LOGIN_URL`, `WP_USERNAME` and `WP_PASSWORD`. Existing sessions use
|
|
84
|
+
`WP_AUTH=session`, `WP_COOKIE` and `WP_NONCE`. Optional variables are documented in
|
|
85
|
+
`.env.example` and `--help`. Never pass passwords as command-line arguments.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Development and releases
|
|
2
|
+
|
|
3
|
+
Requirements: Node >=22.14, npm, and Docker for integration tests. `npm run` commands
|
|
4
|
+
also work through an agent on Windows; the Makefile is an optional shortcut.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npm ci
|
|
8
|
+
npm run check
|
|
9
|
+
npm run test:package
|
|
10
|
+
npm run test:integration
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`check` runs formatting, strict TypeScript, Vitest with coverage and compilation.
|
|
14
|
+
HTTP tests use ephemeral loopback servers. `test:package` creates a tarball, validates
|
|
15
|
+
its file allowlist, installs it in a temporary directory and tests the installed
|
|
16
|
+
import and CLI. Run `npm run build` first when invoking the package test separately.
|
|
17
|
+
CI retains coverage reports as artifacts.
|
|
18
|
+
|
|
19
|
+
`test:integration` starts `.wp-env.json`, exercises CRUD, metadata, activation,
|
|
20
|
+
double evaluation, permissions, application passwords and single-use snippets. It
|
|
21
|
+
cleans up its snippets and stops the environment in `finally`. It never reads a
|
|
22
|
+
`WP_URL` environment variable. Ports are 8896 and 8897. `npm run wp:destroy` removes
|
|
23
|
+
this disposable environment.
|
|
24
|
+
|
|
25
|
+
Runtime and development dependencies are pinned with a lockfile. Overrides for
|
|
26
|
+
`qs`, `ws` and `ajv` fix transitive WordPress environment dependencies; remove them
|
|
27
|
+
once upstream resolves patched versions without overrides.
|
|
28
|
+
|
|
29
|
+
## Releases
|
|
30
|
+
|
|
31
|
+
1. Update the version and CHANGELOG. Run every check.
|
|
32
|
+
2. Merge into `main` with passing CI.
|
|
33
|
+
3. Create and push a tag named `v` followed by the exact package version.
|
|
34
|
+
4. `publish.yml` repeats CI, installs and verifies the package, publishes that exact
|
|
35
|
+
tarball to npm and attaches it to a GitHub Release.
|
|
36
|
+
|
|
37
|
+
There is no per-commit publication or secondary registry: npm and GitHub Releases
|
|
38
|
+
cover installation and artifact downloads. Never reuse a published version number.
|
|
39
|
+
|
|
40
|
+
The workflow uses npm Trusted Publishing (OIDC). Configure these values in npm:
|
|
41
|
+
|
|
42
|
+
- Repository: `erseco/code-snippets-client`.
|
|
43
|
+
- Workflow: `publish.yml`.
|
|
44
|
+
- Environment: `npm`.
|
|
45
|
+
|
|
46
|
+
A new package may require an authenticated first publication before configuring its
|
|
47
|
+
trusted publisher. Never add an npm token to the repository or workflow. A repeated
|
|
48
|
+
publication only succeeds when that version has an identical tarball checksum;
|
|
49
|
+
different contents cause a failure.
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@erseco/code-snippets-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript client and CLI for the WordPress Code Snippets REST API, with WordPress and CAS authentication",
|
|
5
|
+
"license": "GPL-3.0-only",
|
|
6
|
+
"author": "Ernesto Serrano",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/erseco/code-snippets-client.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/erseco/code-snippets-client#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/erseco/code-snippets-client/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"wordpress",
|
|
17
|
+
"code-snippets",
|
|
18
|
+
"rest-api",
|
|
19
|
+
"cas",
|
|
20
|
+
"cli",
|
|
21
|
+
"typescript"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"bin": {
|
|
33
|
+
"wp-code-snippets": "./dist/bin.js"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"docs"
|
|
40
|
+
],
|
|
41
|
+
"sideEffects": false,
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22.14"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc -p tsconfig.build.json",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"format": "prettier --write .",
|
|
52
|
+
"format:check": "prettier --check .",
|
|
53
|
+
"test": "vitest run --coverage",
|
|
54
|
+
"test:integration": "node scripts/integration.mjs",
|
|
55
|
+
"check": "npm run format:check && npm run typecheck && npm test && npm run build",
|
|
56
|
+
"prepack": "npm run build",
|
|
57
|
+
"test:package": "node scripts/test-package.mjs",
|
|
58
|
+
"wp:start": "wp-env start",
|
|
59
|
+
"wp:stop": "wp-env stop",
|
|
60
|
+
"wp:destroy": "wp-env destroy"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"cheerio": "1.2.0",
|
|
64
|
+
"tough-cookie": "6.0.2"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@types/node": "22.20.2",
|
|
68
|
+
"@vitest/coverage-v8": "5.0.0",
|
|
69
|
+
"@wordpress/env": "11.15.0",
|
|
70
|
+
"prettier": "3.9.6",
|
|
71
|
+
"typescript": "7.0.2",
|
|
72
|
+
"vitest": "5.0.0"
|
|
73
|
+
},
|
|
74
|
+
"overrides": {
|
|
75
|
+
"qs": "^6.16.0",
|
|
76
|
+
"ws": "^8.21.3",
|
|
77
|
+
"ajv": "^8.20.0"
|
|
78
|
+
}
|
|
79
|
+
}
|