@adea-ai/plugins 1.0.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 (79) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +74 -0
  3. package/dist/agent-plugins-catalog.d.ts +26 -0
  4. package/dist/agent-plugins-catalog.js +699 -0
  5. package/dist/agent-plugins-catalog.js.map +1 -0
  6. package/dist/agent-plugins-harness.d.ts +54 -0
  7. package/dist/agent-plugins-harness.js +212 -0
  8. package/dist/agent-plugins-harness.js.map +1 -0
  9. package/dist/agent-plugins-schema.d.ts +402 -0
  10. package/dist/agent-plugins-schema.js +200 -0
  11. package/dist/agent-plugins-schema.js.map +1 -0
  12. package/dist/consumer-index.d.ts +217 -0
  13. package/dist/consumer-index.js +358 -0
  14. package/dist/consumer-index.js.map +1 -0
  15. package/dist/display-name.d.ts +8 -0
  16. package/dist/display-name.js +160 -0
  17. package/dist/display-name.js.map +1 -0
  18. package/dist/harness.d.ts +13 -0
  19. package/dist/harness.js +169 -0
  20. package/dist/harness.js.map +1 -0
  21. package/dist/icon-mirror.d.ts +69 -0
  22. package/dist/icon-mirror.js +147 -0
  23. package/dist/icon-mirror.js.map +1 -0
  24. package/dist/icons.d.ts +65 -0
  25. package/dist/icons.js +268 -0
  26. package/dist/icons.js.map +1 -0
  27. package/dist/index.d.ts +304 -0
  28. package/dist/index.js +1877 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/portable-catalog.d.ts +9 -0
  31. package/dist/portable-catalog.js +175 -0
  32. package/dist/portable-catalog.js.map +1 -0
  33. package/dist/publication.d.ts +98 -0
  34. package/dist/publication.js +121 -0
  35. package/dist/publication.js.map +1 -0
  36. package/dist/schema.d.ts +705 -0
  37. package/dist/schema.js +267 -0
  38. package/dist/schema.js.map +1 -0
  39. package/dist/site-icons.d.ts +55 -0
  40. package/dist/site-icons.js +155 -0
  41. package/dist/site-icons.js.map +1 -0
  42. package/dist/sources.d.ts +54 -0
  43. package/dist/sources.js +306 -0
  44. package/dist/sources.js.map +1 -0
  45. package/package.json +102 -0
  46. package/schemas/agent-package.v1.json +387 -0
  47. package/schemas/catalog.v1.schema.json +407 -0
  48. package/schemas/compatibility.v1.schema.json +23 -0
  49. package/schemas/harness-profile.v1.json +75 -0
  50. package/schemas/installation-plan.v2.json +695 -0
  51. package/schemas/sources-lock.v1.schema.json +61 -0
  52. package/src/agent-plugins-catalog.test.ts +402 -0
  53. package/src/agent-plugins-catalog.ts +800 -0
  54. package/src/agent-plugins-harness.test.ts +272 -0
  55. package/src/agent-plugins-harness.ts +281 -0
  56. package/src/agent-plugins-schema.test.ts +27 -0
  57. package/src/agent-plugins-schema.ts +219 -0
  58. package/src/artifacts.test.ts +545 -0
  59. package/src/catalog-installation.test.ts +42 -0
  60. package/src/consumer-index.test.ts +465 -0
  61. package/src/consumer-index.ts +592 -0
  62. package/src/display-name.test.ts +56 -0
  63. package/src/display-name.ts +157 -0
  64. package/src/harness.ts +202 -0
  65. package/src/icon-mirror.ts +196 -0
  66. package/src/icons.test.ts +185 -0
  67. package/src/icons.ts +297 -0
  68. package/src/index.test.ts +888 -0
  69. package/src/index.ts +2538 -0
  70. package/src/portable-catalog.test.ts +186 -0
  71. package/src/portable-catalog.ts +203 -0
  72. package/src/publication.test.ts +117 -0
  73. package/src/publication.ts +171 -0
  74. package/src/schema.ts +309 -0
  75. package/src/site-icon-fallback.test.ts +267 -0
  76. package/src/site-icons.test.ts +171 -0
  77. package/src/site-icons.ts +161 -0
  78. package/src/sources.ts +360 -0
  79. package/src/upstream-headers.test.ts +148 -0
@@ -0,0 +1,148 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import { createSiteIconFetcher, fetchUpstreamBytes, upstreamRequestHeaders } from './index.js'
3
+
4
+ /**
5
+ * Header policy for upstream requests.
6
+ *
7
+ * A live build runs with a GitHub token that authorises repository writes, and
8
+ * the same helper fetches vendor sites for brand marks. The credential must
9
+ * never leave GitHub's hosts — and sending it changes what a vendor answers:
10
+ * appwrite.io replies 500 to a page carrying an Authorization header, so a
11
+ * build that leaked the token also lost marks it had already curated.
12
+ */
13
+
14
+ const VENDOR = 'https://appwrite.io/'
15
+ const VENDOR_ICON = 'https://cdn.example/apple-touch-icon.png'
16
+ const GITHUB_API = 'https://api.github.com/repos/adea-ai/plugins/git/trees/abc?recursive=1'
17
+ const RAW = 'https://raw.githubusercontent.com/adea-ai/plugins/abc/plugin.json'
18
+
19
+ const saved = {
20
+ GITHUB_TOKEN: process.env.GITHUB_TOKEN,
21
+ GH_TOKEN: process.env.GH_TOKEN,
22
+ }
23
+
24
+ afterEach(() => {
25
+ for (const key of ['GITHUB_TOKEN', 'GH_TOKEN'] as const) {
26
+ const value = saved[key]
27
+ if (value === undefined) delete process.env[key]
28
+ else process.env[key] = value
29
+ }
30
+ })
31
+
32
+ function withTokens(github?: string, gh?: string): void {
33
+ for (const [key, value] of [
34
+ ['GITHUB_TOKEN', github],
35
+ ['GH_TOKEN', gh],
36
+ ] as const) {
37
+ if (value === undefined) delete process.env[key]
38
+ else process.env[key] = value
39
+ }
40
+ }
41
+
42
+ /** Captures the init each fetch is called with, so headers and signals are inspectable. */
43
+ function captureFetch(response: () => Response = () => new Response('ok')) {
44
+ const original = globalThis.fetch
45
+ const inits: RequestInit[] = []
46
+ globalThis.fetch = (async (_input: unknown, init?: RequestInit) => {
47
+ inits.push(init ?? {})
48
+ return response()
49
+ }) as typeof fetch
50
+ return {
51
+ inits,
52
+ restore: () => {
53
+ globalThis.fetch = original
54
+ },
55
+ }
56
+ }
57
+
58
+ describe('upstream request headers', () => {
59
+ test('never attach the repository credential to a vendor host', () => {
60
+ withTokens('ghs_realtoken', 'ghs_realtoken')
61
+ for (const url of [VENDOR, VENDOR_ICON]) {
62
+ const headers = upstreamRequestHeaders(url)
63
+ expect(headers.authorization).toBeUndefined()
64
+ // GitHub's media type is part of the same disclosure: a vendor asked for
65
+ // an image with it can answer with something that is not the mark.
66
+ expect(headers.accept).toBeUndefined()
67
+ expect(headers['user-agent']).toBeDefined()
68
+ }
69
+ })
70
+
71
+ test('attach the credential to GitHub hosts', () => {
72
+ withTokens('ghs_realtoken', 'ghs_realtoken')
73
+ for (const url of [GITHUB_API, RAW]) {
74
+ const headers = upstreamRequestHeaders(url)
75
+ expect(headers.authorization).toBe('Bearer ghs_realtoken')
76
+ expect(headers.accept).toBe('application/vnd.github+json')
77
+ }
78
+ })
79
+
80
+ test('attach nothing when no token is configured', () => {
81
+ withTokens(undefined, undefined)
82
+ expect(upstreamRequestHeaders(GITHUB_API).authorization).toBeUndefined()
83
+ })
84
+
85
+ test('treat an unparseable URL as anonymous', () => {
86
+ withTokens('ghs_realtoken', 'ghs_realtoken')
87
+ expect(upstreamRequestHeaders('not a url').authorization).toBeUndefined()
88
+ })
89
+
90
+ test('fetchUpstreamBytes sends no credential to a vendor host', async () => {
91
+ withTokens('ghs_realtoken', 'ghs_realtoken')
92
+ const mock = captureFetch(() => new Response(new Uint8Array([1, 2, 3])))
93
+ try {
94
+ await fetchUpstreamBytes(VENDOR_ICON)
95
+ } finally {
96
+ mock.restore()
97
+ }
98
+ expect(mock.inits).toHaveLength(1)
99
+ const headers = mock.inits[0]?.headers as Record<string, string>
100
+ expect(headers.authorization).toBeUndefined()
101
+ })
102
+
103
+ test('fetchUpstreamBytes still authenticates a GitHub host', async () => {
104
+ withTokens('ghs_realtoken', 'ghs_realtoken')
105
+ const mock = captureFetch(() => new Response('{}'))
106
+ try {
107
+ await fetchUpstreamBytes(GITHUB_API)
108
+ } finally {
109
+ mock.restore()
110
+ }
111
+ const headers = mock.inits[0]?.headers as Record<string, string>
112
+ expect(headers.authorization).toBe('Bearer ghs_realtoken')
113
+ })
114
+ })
115
+
116
+ describe('site-icon fetcher bounds', () => {
117
+ test('gives every request a live ceiling instead of a shared, aged one', async () => {
118
+ const mock = captureFetch()
119
+ try {
120
+ const fetcher = createSiteIconFetcher()
121
+ await fetcher.fetchBytes(VENDOR_ICON)
122
+ await fetcher.fetchBytes(VENDOR_ICON)
123
+ } finally {
124
+ mock.restore()
125
+ }
126
+ // Both requests carry a signal, and neither is already aborted: the ceiling
127
+ // is created per request, so a second call cannot inherit a spent budget.
128
+ expect(mock.inits).toHaveLength(2)
129
+ for (const init of mock.inits) {
130
+ expect(init.signal).toBeInstanceOf(AbortSignal)
131
+ expect(init.signal?.aborted).toBe(false)
132
+ }
133
+ })
134
+
135
+ test('honours a caller deadline that has already passed', async () => {
136
+ const mock = captureFetch()
137
+ try {
138
+ const fetcher = createSiteIconFetcher(AbortSignal.abort())
139
+ await fetcher.fetchBytes(VENDOR_ICON)
140
+ } finally {
141
+ mock.restore()
142
+ }
143
+ // The request still goes out carrying the spent deadline, so the platform
144
+ // aborts it; a real fetch rejects rather than returning a page.
145
+ expect(mock.inits).toHaveLength(1)
146
+ expect(mock.inits[0]?.signal?.aborted).toBe(true)
147
+ })
148
+ })