@publier/openapi 0.3.83 → 0.4.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.
@@ -10,6 +10,7 @@ interface OpenApiSpec {
10
10
  */
11
11
  webhooks?: Record<string, OpenApiPathItem>;
12
12
  components?: OpenApiComponents;
13
+ security?: Record<string, string[]>[];
13
14
  tags?: OpenApiTag[];
14
15
  }
15
16
  interface OpenApiInfo {
package/dist/index.d.mts CHANGED
@@ -11,6 +11,7 @@ interface OpenApiSpec {
11
11
  */
12
12
  webhooks?: Record<string, OpenApiPathItem>;
13
13
  components?: OpenApiComponents;
14
+ security?: Record<string, string[]>[];
14
15
  tags?: OpenApiTag[];
15
16
  }
16
17
  interface OpenApiInfo {
@@ -241,6 +242,8 @@ interface MethodBadgeProps {
241
242
  method: HttpMethod;
242
243
  }
243
244
  interface APIPlaygroundProps {
245
+ /** Full parsed specification supplies inherited security and server defaults. */
246
+ spec?: OpenApiSpec;
244
247
  method: HttpMethod;
245
248
  path: string;
246
249
  operation: OpenApiOperation;
package/dist/parser.d.mts CHANGED
@@ -10,6 +10,7 @@ interface OpenApiSpec {
10
10
  */
11
11
  webhooks?: Record<string, OpenApiPathItem>;
12
12
  components?: OpenApiComponents;
13
+ security?: Record<string, string[]>[];
13
14
  tags?: OpenApiTag[];
14
15
  }
15
16
  interface OpenApiInfo {
package/dist/server.d.mts CHANGED
@@ -10,6 +10,7 @@ interface OpenApiSpec {
10
10
  */
11
11
  webhooks?: Record<string, OpenApiPathItem>;
12
12
  components?: OpenApiComponents;
13
+ security?: Record<string, string[]>[];
13
14
  tags?: OpenApiTag[];
14
15
  }
15
16
  interface OpenApiInfo {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@publier/openapi",
3
- "version": "0.3.83",
3
+ "version": "0.4.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "files": [
@@ -45,7 +45,7 @@
45
45
  "access": "public"
46
46
  },
47
47
  "dependencies": {
48
- "@publier/native": "7.0.1",
48
+ "@publier/native": "8.0.0",
49
49
  "@readme/httpsnippet": "^11.1.0",
50
50
  "openapi-sampler": "^1.7.3",
51
51
  "yaml": "^2.9.0"
@@ -1,46 +1,34 @@
1
1
  export class PublierApiPlayground extends HTMLElement {
2
+ #listeners?: AbortController;
3
+
2
4
  connectedCallback() {
3
- this.querySelector('form')?.addEventListener('submit', this.#send.bind(this));
5
+ this.#listeners?.abort();
6
+ this.#listeners = new AbortController();
7
+ const form = this.querySelector('form');
8
+ form?.addEventListener('submit', this.#send, { signal: this.#listeners.signal });
9
+ form?.addEventListener('change', this.#selectSecurity, { signal: this.#listeners.signal });
10
+ this.#selectSecurity();
4
11
  }
5
12
 
6
- async #send(e: SubmitEvent) {
7
- e.preventDefault();
8
- const form = e.target as HTMLFormElement;
9
- const data = new FormData(form);
10
-
11
- const method = (form.dataset.method ?? 'get').toUpperCase();
12
- let path = form.dataset.path ?? '/';
13
-
14
- for (const [key, val] of data.entries()) {
15
- if (key.startsWith('path:')) {
16
- const paramName = key.slice(5);
17
- path = path.replace(`{${paramName}}`, encodeURIComponent((val as string) || paramName));
18
- }
19
- }
20
-
21
- const qs = new URLSearchParams();
22
- for (const [key, val] of data.entries()) {
23
- if (key.startsWith('query:') && val) qs.set(key.slice(6), val as string);
24
- }
25
- const server = (data.get('server') as string) ?? '';
26
- const fullUrl = `${server}${path}${qs.size > 0 ? `?${qs}` : ''}`;
13
+ disconnectedCallback() {
14
+ this.#listeners?.abort();
15
+ }
27
16
 
28
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
29
- const bearer = data.get('auth:bearer') as string;
30
- const apiKey = data.get('auth:apikey') as string;
31
- if (bearer) headers.Authorization = `Bearer ${bearer}`;
32
- if (apiKey) headers['X-API-Key'] = apiKey;
33
- for (const [key, val] of data.entries()) {
34
- if (key.startsWith('header:') && val) headers[key.slice(7)] = val as string;
17
+ #selectSecurity = () => {
18
+ const selected = this.querySelector<HTMLSelectElement>('[name="security"]')?.value ?? '0';
19
+ for (const group of this.querySelectorAll<HTMLFieldSetElement>('[data-pg-auth-group]')) {
20
+ group.hidden = group.dataset.pgAuthGroup !== selected;
21
+ group.disabled = group.hidden;
35
22
  }
23
+ };
36
24
 
37
- const hasBody = !['GET', 'HEAD', 'OPTIONS'].includes(method);
38
- const body = hasBody ? (data.get('body') as string) || undefined : undefined;
39
-
25
+ #send = async (e: SubmitEvent) => {
26
+ e.preventDefault();
27
+ const form = e.target as HTMLFormElement;
40
28
  const sendBtn = this.querySelector<HTMLButtonElement>('[data-pg-send]');
41
29
  const responseEl = this.querySelector('[data-pg-response]');
42
30
  const errorEl = this.querySelector('[data-pg-error]');
43
-
31
+ if (sendBtn?.disabled) return;
44
32
  if (sendBtn) {
45
33
  sendBtn.disabled = true;
46
34
  sendBtn.textContent = 'Sending…';
@@ -49,26 +37,84 @@ export class PublierApiPlayground extends HTMLElement {
49
37
  if (responseEl) responseEl.innerHTML = '';
50
38
 
51
39
  try {
52
- const res = await fetch(fullUrl, { method, headers, body });
40
+ const data = new FormData(form);
41
+ const method = (form.dataset.method ?? 'get').toUpperCase();
42
+ let path = form.dataset.path ?? '/';
43
+ for (const [key, val] of data) {
44
+ if (key.startsWith('path:')) {
45
+ path = path.replaceAll(`{${key.slice(5)}}`, encodeURIComponent(String(val)));
46
+ }
47
+ }
48
+ const server = String(data.get('server') ?? '').replace(/\/$/, '');
49
+ const url = new URL(`${server}${path}`, document.baseURI);
50
+ if (!['https:', 'http:'].includes(url.protocol))
51
+ throw new Error('Use an HTTP or HTTPS server.');
52
+ const headers = new Headers();
53
+ for (const [key, val] of data) {
54
+ if (key.startsWith('query:') && val) url.searchParams.set(key.slice(6), String(val));
55
+ if (key.startsWith('header:') && val) headers.set(key.slice(7), String(val));
56
+ }
57
+ let credentials: RequestCredentials = 'same-origin';
58
+ const unsupported = form.querySelector(
59
+ '[data-pg-auth-group]:not([hidden]) [data-pg-unsupported]',
60
+ );
61
+ if (unsupported) throw new Error(unsupported.textContent ?? 'Unsupported authentication.');
62
+ for (const input of form.querySelectorAll<HTMLInputElement>('[data-pg-auth-kind]')) {
63
+ const value = data.get(input.name);
64
+ if (!value) continue;
65
+ const kind = input.dataset.pgAuthKind;
66
+ if (kind === 'apiKey') {
67
+ const name = input.dataset.pgAuthName ?? '';
68
+ if (input.dataset.pgAuthIn === 'query') url.searchParams.set(name, String(value));
69
+ else headers.set(name, String(value));
70
+ } else if (kind === 'bearer') {
71
+ headers.set('Authorization', `Bearer ${value}`);
72
+ } else if (kind === 'basic') {
73
+ const bytes = new TextEncoder().encode(String(value));
74
+ headers.set(
75
+ 'Authorization',
76
+ `Basic ${btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join(''))}`,
77
+ );
78
+ } else if (kind === 'cookie') credentials = 'include';
79
+ }
80
+ let body: BodyInit | undefined;
81
+ const rawBody = String(data.get('body') ?? '');
82
+ if (!['GET', 'HEAD'].includes(method) && rawBody) {
83
+ const media = String(data.get('content-type') ?? 'application/json');
84
+ if (media === 'multipart/form-data' || media === 'application/x-www-form-urlencoded') {
85
+ const fields: unknown = JSON.parse(rawBody);
86
+ if (!fields || typeof fields !== 'object' || Array.isArray(fields)) {
87
+ throw new Error('Form bodies must be a JSON object of scalar fields.');
88
+ }
89
+ const encoded = media === 'multipart/form-data' ? new FormData() : new URLSearchParams();
90
+ for (const [key, value] of Object.entries(fields)) {
91
+ if (value !== null && typeof value === 'object')
92
+ throw new Error('Form fields must be scalar values.');
93
+ encoded.append(key, value === null ? '' : String(value));
94
+ }
95
+ body = encoded;
96
+
97
+ if (media === 'multipart/form-data') headers.delete('Content-Type');
98
+ else headers.set('Content-Type', media);
99
+ } else {
100
+ body = rawBody;
101
+ headers.set('Content-Type', media);
102
+ }
103
+ }
104
+ const res = await fetch(url.href, { method, headers, body, credentials });
53
105
  const bodyText = await res.text();
54
-
55
106
  let formatted = bodyText;
56
107
  try {
57
108
  formatted = JSON.stringify(JSON.parse(bodyText), null, 2);
58
109
  } catch {}
59
-
60
- const isSuccess = res.status >= 200 && res.status < 300;
61
- const statusColor = isSuccess ? 'var(--color-green-400)' : 'var(--color-red-400)';
62
-
63
110
  if (responseEl) {
64
- responseEl.innerHTML = `
65
- <div style="display:flex;align-items:center;padding:0.375rem 0.75rem;border-bottom:1px solid var(--color-border,rgba(0,0,0,.12))">
66
- <span style="font-family:ui-monospace,monospace;font-size:0.75rem;font-weight:600;color:${statusColor}">
67
- ${this.#escHtml(String(res.status))} ${this.#escHtml(res.statusText)}
68
- </span>
69
- </div>
70
- <pre style="margin:0;overflow:auto;padding:0.625rem 0.75rem;font-family:ui-monospace,monospace;font-size:0.75rem;line-height:1.6"><code>${this.#escHtml(formatted)}</code></pre>
71
- `;
111
+ const status = document.createElement('div');
112
+ status.className = 'px-4 py-3 font-mono text-sm';
113
+ status.textContent = `${res.status} ${res.statusText}`;
114
+ const pre = document.createElement('pre');
115
+ pre.className = 'px-4 py-3 overflow-auto font-mono text-sm';
116
+ pre.textContent = formatted;
117
+ responseEl.replaceChildren(status, pre);
72
118
  }
73
119
  } catch (err) {
74
120
  if (errorEl) errorEl.textContent = err instanceof Error ? err.message : 'Request failed';
@@ -78,11 +124,7 @@ export class PublierApiPlayground extends HTMLElement {
78
124
  sendBtn.textContent = 'Send Request';
79
125
  }
80
126
  }
81
- }
82
-
83
- #escHtml(str: string): string {
84
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
85
- }
127
+ };
86
128
  }
87
129
 
88
130
  if (typeof customElements !== 'undefined' && !customElements.get('publier-api-playground')) {
@@ -3,8 +3,8 @@ import { openapiRenderApiPlayground } from '@publier/native';
3
3
  import type { APIPlaygroundProps } from './props.ts';
4
4
  type Props = APIPlaygroundProps;
5
5
 
6
- const { method, path, operation, servers = [] } = Astro.props;
7
- const html = openapiRenderApiPlayground(JSON.stringify({ method, path, operation, servers }));
6
+ const { method, path, operation, spec, servers = spec?.servers ?? [] } = Astro.props;
7
+ const html = openapiRenderApiPlayground(JSON.stringify({ method, path, operation, servers, spec }));
8
8
  ---
9
9
 
10
10
  <Fragment set:html={html} />
@@ -31,6 +31,8 @@ export interface MethodBadgeProps {
31
31
  }
32
32
 
33
33
  export interface APIPlaygroundProps {
34
+
35
+ spec?: OpenApiSpec;
34
36
  method: HttpMethod;
35
37
  path: string;
36
38
  operation: OpenApiOperation;