@joeybuilt/plexo-sdk 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/dist/index.js ADDED
@@ -0,0 +1,599 @@
1
+ // src/types/channel.ts
2
+ var PEX_VERSION = "0.4.0";
3
+
4
+ // src/channel-client.ts
5
+ import { createHmac } from "crypto";
6
+ function sign(secret, body) {
7
+ return "sha256=" + createHmac("sha256", secret).update(body).digest("hex");
8
+ }
9
+ function defaultHeaders(opts, body) {
10
+ return {
11
+ "Content-Type": "application/json",
12
+ "X-App-Id": opts.appId,
13
+ "X-Plexo-Timestamp": (/* @__PURE__ */ new Date()).toISOString(),
14
+ "X-Plexo-Signature": sign(opts.serviceKey, body)
15
+ };
16
+ }
17
+ async function request(opts, method, path, body) {
18
+ const fetchImpl = opts.fetchImpl ?? fetch;
19
+ const serialized = body === void 0 ? "" : JSON.stringify(body);
20
+ const res = await fetchImpl(`${opts.baseUrl}${path}`, {
21
+ method,
22
+ headers: defaultHeaders(opts, serialized),
23
+ body: body === void 0 ? void 0 : serialized
24
+ });
25
+ if (!res.ok) {
26
+ const detail = await res.text().catch(() => "");
27
+ throw new Error(`channel-client: ${method} ${path} failed: ${res.status} ${detail}`);
28
+ }
29
+ if (res.status === 204) return void 0;
30
+ return await res.json();
31
+ }
32
+ function createChannelClient(opts) {
33
+ return {
34
+ list: (q) => request(
35
+ opts,
36
+ "GET",
37
+ `/api/plexo/channels${q?.workspaceId ? `?workspaceId=${encodeURIComponent(q.workspaceId)}` : ""}`
38
+ ),
39
+ subscribe: (channelId, scopes) => request(opts, "POST", `/api/plexo/channels/${channelId}/subscribe`, { scopes }),
40
+ unsubscribe: async (channelId, subscriptionId) => {
41
+ await request(
42
+ opts,
43
+ "DELETE",
44
+ `/api/plexo/channels/${channelId}/subscribe/${subscriptionId}`
45
+ );
46
+ },
47
+ threads: (channelId, q) => {
48
+ const params = new URLSearchParams();
49
+ if (q?.cursor) params.set("cursor", q.cursor);
50
+ if (q?.limit) params.set("limit", String(q.limit));
51
+ const qs = params.toString();
52
+ return request(
53
+ opts,
54
+ "GET",
55
+ `/api/plexo/channels/${channelId}/threads${qs ? `?${qs}` : ""}`
56
+ );
57
+ },
58
+ messages: (channelId, threadId, q) => {
59
+ const params = new URLSearchParams();
60
+ if (q?.cursor) params.set("cursor", q.cursor);
61
+ if (q?.limit) params.set("limit", String(q.limit));
62
+ const qs = params.toString();
63
+ return request(
64
+ opts,
65
+ "GET",
66
+ `/api/plexo/channels/${channelId}/threads/${threadId}/messages${qs ? `?${qs}` : ""}`
67
+ );
68
+ },
69
+ send: (channelId, threadId, payload) => request(
70
+ opts,
71
+ "POST",
72
+ `/api/plexo/channels/${channelId}/threads/${threadId}/messages`,
73
+ payload
74
+ ),
75
+ events: (channelId, q) => createEventStream(opts, channelId, q)
76
+ };
77
+ }
78
+ async function* createEventStream(opts, channelId, q) {
79
+ const fetchImpl = opts.fetchImpl ?? fetch;
80
+ const url = `${opts.baseUrl}/api/plexo/channels/${channelId}/events`;
81
+ const headers = {
82
+ Accept: "text/event-stream",
83
+ "X-App-Id": opts.appId,
84
+ "X-Plexo-Timestamp": (/* @__PURE__ */ new Date()).toISOString(),
85
+ "X-Plexo-Signature": sign(opts.serviceKey, "")
86
+ };
87
+ if (q?.lastEventId) headers["Last-Event-ID"] = q.lastEventId;
88
+ const res = await fetchImpl(url, { method: "GET", headers, signal: q?.signal });
89
+ if (!res.ok || !res.body) {
90
+ throw new Error(`channel-client: SSE ${url} failed: ${res.status}`);
91
+ }
92
+ const reader = res.body.getReader();
93
+ const decoder = new TextDecoder();
94
+ let buffer = "";
95
+ while (true) {
96
+ const { value, done } = await reader.read();
97
+ if (done) break;
98
+ buffer += decoder.decode(value, { stream: true });
99
+ let idx;
100
+ while ((idx = buffer.indexOf("\n\n")) !== -1) {
101
+ const raw = buffer.slice(0, idx);
102
+ buffer = buffer.slice(idx + 2);
103
+ const dataLine = raw.split("\n").find((l) => l.startsWith("data:"));
104
+ if (!dataLine) continue;
105
+ const json = dataLine.slice(5).trim();
106
+ if (!json) continue;
107
+ try {
108
+ yield JSON.parse(json);
109
+ } catch {
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ // src/types/events.ts
116
+ var TOPICS = {
117
+ // Task lifecycle
118
+ TASK_CREATED: "task.created",
119
+ TASK_COMPLETED: "task.completed",
120
+ TASK_FAILED: "task.failed",
121
+ TASK_BLOCKED: "task.blocked",
122
+ // Channel
123
+ CHANNEL_MESSAGE_RECEIVED: "channel.message.received",
124
+ CHANNEL_HEALTH_CHANGED: "channel.health.changed",
125
+ // Extension lifecycle
126
+ EXTENSION_ACTIVATED: "extension.activated",
127
+ EXTENSION_DEACTIVATED: "extension.deactivated",
128
+ EXTENSION_CRASHED: "extension.crashed",
129
+ // Connection lifecycle
130
+ CONNECTION_ADDED: "connection.added",
131
+ CONNECTION_REMOVED: "connection.removed",
132
+ // Memory
133
+ MEMORY_WRITTEN: "memory.written",
134
+ // §16 — Entity lifecycle
135
+ ENTITY_CREATED: "entity.created",
136
+ ENTITY_MODIFIED: "entity.modified",
137
+ ENTITY_DELETED: "entity.deleted",
138
+ ENTITY_LINKED: "entity.linked",
139
+ // §23 — Escalation
140
+ ESCALATION_TRIGGERED: "escalation.triggered",
141
+ ESCALATION_RESOLVED: "escalation.resolved",
142
+ ESCALATION_TIMED_OUT: "escalation.timed_out",
143
+ // §18 — Audit (host-internal, owner-tier subscribe only)
144
+ AUDIT_ENTRY_CREATED: "audit.entry.created",
145
+ // §20 — UserSelf
146
+ SELF_UPDATED: "self.updated",
147
+ SELF_PROPOSAL_RECEIVED: "self.proposal.received",
148
+ // Agent lifecycle (core architecture — Agent ≠ Extension)
149
+ AGENT_ACTIVATED: "agent.activated",
150
+ AGENT_DEACTIVATED: "agent.deactivated",
151
+ AGENT_PLAN_CREATED: "agent.plan.created",
152
+ AGENT_STEP_COMPLETED: "agent.step.completed",
153
+ AGENT_STEP_FAILED: "agent.step.failed",
154
+ // §22 — A2A
155
+ A2A_INBOUND_RECEIVED: "a2a.inbound.received",
156
+ A2A_DELEGATION_SENT: "a2a.delegation.sent",
157
+ A2A_DELEGATION_COMPLETED: "a2a.delegation.completed",
158
+ // §7.6 — Prompt Library
159
+ PROMPT_REGISTERED: "prompt.registered",
160
+ PROMPT_ENABLED: "prompt.enabled",
161
+ // §7.7 — Context Layer
162
+ CONTEXT_REGISTERED: "context.registered",
163
+ CONTEXT_UPDATED: "context.updated",
164
+ CONTEXT_EXPIRED: "context.expired"
165
+ };
166
+ function customTopic(scope, name, event) {
167
+ return `ext.${scope}.${name}.${event}`;
168
+ }
169
+
170
+ // src/validation/manifest.ts
171
+ var VALID_TYPES = ["agent", "skill", "channel", "tool", "connector"];
172
+ var LEGACY_TYPES = /* @__PURE__ */ new Set(["function", "mcp-server"]);
173
+ var VALID_ENTITY_TYPES = [
174
+ "person",
175
+ "task",
176
+ "thread",
177
+ "note",
178
+ "transaction",
179
+ "calendar_event",
180
+ "file"
181
+ ];
182
+ var VALID_TRUST_TIERS = ["owner", "verified", "community"];
183
+ var DISPLAY_NAME_MAX = 50;
184
+ var STANDARD_CAPABILITIES = /* @__PURE__ */ new Set([
185
+ // Legacy (deprecated at Standard + Full)
186
+ "memory:read",
187
+ "memory:write",
188
+ "memory:delete",
189
+ // Entity-scoped memory wildcards (owner tier only)
190
+ "memory:read:*",
191
+ "memory:write:*",
192
+ // Channel
193
+ "channel:send",
194
+ "channel:send-direct",
195
+ "channel:receive",
196
+ // Scheduling
197
+ "schedule:register",
198
+ "schedule:manage",
199
+ // UI
200
+ "ui:register-widget",
201
+ "ui:notify",
202
+ // Tasks
203
+ "tasks:create",
204
+ "tasks:read",
205
+ "tasks:read-all",
206
+ // Events
207
+ "events:subscribe",
208
+ "events:publish",
209
+ // Storage
210
+ "storage:read",
211
+ "storage:write",
212
+ // §20 — UserSelf
213
+ "self:read",
214
+ "self:write",
215
+ // §18 — Audit
216
+ "audit:read",
217
+ // §21 — Identity
218
+ "identity:present",
219
+ // §22 — A2A
220
+ "a2a:delegate",
221
+ // §24 — Model
222
+ "model:override",
223
+ // Prompts
224
+ "prompts:register",
225
+ "prompts:read",
226
+ // Context
227
+ "context:register",
228
+ "context:write",
229
+ "context:read"
230
+ ]);
231
+ var OWNER_ONLY_CAPABILITIES = /* @__PURE__ */ new Set([
232
+ "memory:read:*",
233
+ "memory:write:*",
234
+ "audit:read",
235
+ "model:override"
236
+ ]);
237
+ var RATIONALE_OPTIONAL_CAPABILITIES = /* @__PURE__ */ new Set([
238
+ "storage:read",
239
+ "storage:write",
240
+ "events:subscribe",
241
+ "events:publish",
242
+ "ui:register-widget",
243
+ "ui:notify",
244
+ "identity:present",
245
+ "prompts:register",
246
+ "prompts:read",
247
+ "context:register",
248
+ "context:read",
249
+ "schedule:register"
250
+ ]);
251
+ function validateManifest(raw, options) {
252
+ const errors = [];
253
+ const complianceLevel = options?.hostComplianceLevel ?? "core";
254
+ const installSource = options?.source ?? "registry";
255
+ if (typeof raw !== "object" || raw === null) {
256
+ return { valid: false, errors: [{ field: "root", message: "Manifest must be a JSON object" }] };
257
+ }
258
+ const m = raw;
259
+ if (typeof m["plexo"] !== "string" || !isSemver(m["plexo"])) {
260
+ errors.push({ field: "plexo", message: 'Must be a valid semver string (e.g. "0.4.0")' });
261
+ }
262
+ if (typeof m["name"] !== "string" || !isValidPackageName(m["name"])) {
263
+ errors.push({ field: "name", message: "Must match @scope/name format (lowercase alphanumeric, hyphens, dots allowed)" });
264
+ }
265
+ if (typeof m["version"] !== "string" || !isSemver(m["version"])) {
266
+ errors.push({ field: "version", message: "Must be a valid semver string" });
267
+ }
268
+ const rawType = m["type"];
269
+ if (LEGACY_TYPES.has(rawType)) {
270
+ const replacement = rawType === "function" ? "tool (or skill)" : "connector";
271
+ errors.push({
272
+ field: "type",
273
+ message: `Type "${rawType}" is deprecated in v0.4.0. Use "${replacement}" instead.`,
274
+ severity: "warning"
275
+ });
276
+ } else if (!VALID_TYPES.includes(rawType)) {
277
+ errors.push({ field: "type", message: `Must be one of: ${VALID_TYPES.join(", ")}` });
278
+ }
279
+ if (typeof m["entry"] !== "string" || m["entry"].length === 0) {
280
+ errors.push({ field: "entry", message: "Must be a non-empty string path to the entry point" });
281
+ }
282
+ if (!Array.isArray(m["capabilities"])) {
283
+ errors.push({ field: "capabilities", message: "Must be an array of capability token strings" });
284
+ } else {
285
+ ;
286
+ m["capabilities"].forEach((cap, i) => {
287
+ if (typeof cap !== "string") {
288
+ errors.push({ field: `capabilities[${i}]`, message: "Each capability must be a string" });
289
+ return;
290
+ }
291
+ if (!isValidCapability(cap)) {
292
+ errors.push({
293
+ field: `capabilities[${i}]`,
294
+ message: `Unknown capability token "${cap}". Must be a standard token, entity-scoped memory, connections:<service>, or host:<hostname>:<capability>`
295
+ });
296
+ return;
297
+ }
298
+ if (isHostScopedCapability(cap)) {
299
+ errors.push({
300
+ field: `capabilities[${i}]`,
301
+ message: `Host-scoped capability "${cap}" is not validated by this tool. The target host must confirm this token is supported.`,
302
+ severity: "warning"
303
+ });
304
+ }
305
+ if (isUnscopedMemoryCapability(cap) && (complianceLevel === "standard" || complianceLevel === "full")) {
306
+ errors.push({
307
+ field: `capabilities[${i}]`,
308
+ message: `Unscoped memory capability "${cap}" is invalid at ${complianceLevel} compliance. Use entity-scoped tokens (e.g. memory:read:person, memory:write:task).`
309
+ });
310
+ }
311
+ if (isWildcardMemoryCapability(cap)) {
312
+ const trust = m["trust"];
313
+ if (trust !== "owner") {
314
+ errors.push({
315
+ field: `capabilities[${i}]`,
316
+ message: `Wildcard memory capability "${cap}" is only allowed at trust tier: owner. Declared trust: ${trust ?? "none"}`
317
+ });
318
+ }
319
+ }
320
+ if (cap === "audit:read") {
321
+ const trust = m["trust"];
322
+ if (trust !== "owner") {
323
+ errors.push({
324
+ field: `capabilities[${i}]`,
325
+ message: `audit:read capability is only allowed at trust tier: owner. Declared trust: ${trust ?? "none"}`
326
+ });
327
+ }
328
+ }
329
+ if (installSource === "sideload" && OWNER_ONLY_CAPABILITIES.has(cap)) {
330
+ errors.push({
331
+ field: `capabilities[${i}]`,
332
+ message: `Capability "${cap}" is restricted to owner-tier extensions and cannot be used in a sideloaded install.`
333
+ });
334
+ }
335
+ });
336
+ }
337
+ const trustValue = m["trust"];
338
+ const capabilitiesList = Array.isArray(m["capabilities"]) ? m["capabilities"].filter((c) => typeof c === "string") : [];
339
+ const rationale = m["capabilitiesRationale"];
340
+ if (rationale !== void 0) {
341
+ if (typeof rationale !== "object" || rationale === null || Array.isArray(rationale)) {
342
+ errors.push({
343
+ field: "capabilitiesRationale",
344
+ message: "Must be an object mapping capability tokens to explanation strings"
345
+ });
346
+ } else {
347
+ const r = rationale;
348
+ for (const [token, explanation] of Object.entries(r)) {
349
+ if (!capabilitiesList.includes(token)) {
350
+ errors.push({
351
+ field: `capabilitiesRationale.${token}`,
352
+ message: `Rationale declared for "${token}" but the capability is not in capabilities[]`
353
+ });
354
+ continue;
355
+ }
356
+ if (typeof explanation !== "string") {
357
+ errors.push({
358
+ field: `capabilitiesRationale.${token}`,
359
+ message: "Rationale value must be a string"
360
+ });
361
+ continue;
362
+ }
363
+ if (explanation.length === 0) {
364
+ errors.push({
365
+ field: `capabilitiesRationale.${token}`,
366
+ message: "Rationale value must be non-empty"
367
+ });
368
+ } else if (explanation.length > 200) {
369
+ errors.push({
370
+ field: `capabilitiesRationale.${token}`,
371
+ message: "Rationale must be 200 characters or fewer"
372
+ });
373
+ }
374
+ }
375
+ }
376
+ }
377
+ if (trustValue === "owner" || trustValue === "verified") {
378
+ const r = rationale && typeof rationale === "object" && !Array.isArray(rationale) ? rationale : {};
379
+ for (const token of capabilitiesList) {
380
+ if (RATIONALE_OPTIONAL_CAPABILITIES.has(token)) continue;
381
+ if (typeof r[token] !== "string" || r[token].length === 0) {
382
+ errors.push({
383
+ field: "capabilitiesRationale",
384
+ message: `Capability "${token}" requires a rationale entry for trust tier "${trustValue}". Add capabilitiesRationale["${token}"].`
385
+ });
386
+ }
387
+ }
388
+ } else if (trustValue === "community" && capabilitiesList.length > 0) {
389
+ const r = rationale && typeof rationale === "object" && !Array.isArray(rationale) ? rationale : {};
390
+ for (const token of capabilitiesList) {
391
+ if (RATIONALE_OPTIONAL_CAPABILITIES.has(token)) continue;
392
+ if (typeof r[token] !== "string" || r[token].length === 0) {
393
+ errors.push({
394
+ field: "capabilitiesRationale",
395
+ message: `Capability "${token}" has no rationale \u2014 community extensions are strongly encouraged to explain each capability.`,
396
+ severity: "warning"
397
+ });
398
+ }
399
+ }
400
+ }
401
+ if (typeof m["displayName"] !== "string" || m["displayName"].length === 0) {
402
+ errors.push({ field: "displayName", message: "Must be a non-empty string" });
403
+ } else if (m["displayName"].length > DISPLAY_NAME_MAX) {
404
+ errors.push({ field: "displayName", message: `Must be ${DISPLAY_NAME_MAX} characters or fewer` });
405
+ }
406
+ if (typeof m["description"] !== "string") {
407
+ errors.push({ field: "description", message: "Must be a string" });
408
+ } else if (m["description"].length > 280) {
409
+ errors.push({ field: "description", message: "Must be 280 characters or fewer" });
410
+ }
411
+ if (typeof m["author"] !== "string" || m["author"].length === 0) {
412
+ errors.push({ field: "author", message: "Must be a non-empty string" });
413
+ }
414
+ if (typeof m["license"] !== "string" || m["license"].length === 0) {
415
+ errors.push({ field: "license", message: "Must be a valid SPDX license identifier" });
416
+ }
417
+ if (m["keywords"] !== void 0) {
418
+ if (!Array.isArray(m["keywords"])) {
419
+ errors.push({ field: "keywords", message: "Must be an array of strings" });
420
+ } else if (m["keywords"].length > 10) {
421
+ errors.push({ field: "keywords", message: "Max 10 keywords allowed" });
422
+ }
423
+ }
424
+ if (m["screenshots"] !== void 0) {
425
+ if (!Array.isArray(m["screenshots"])) {
426
+ errors.push({ field: "screenshots", message: "Must be an array of HTTPS URLs" });
427
+ } else if (m["screenshots"].length > 5) {
428
+ errors.push({ field: "screenshots", message: "Max 5 screenshots allowed" });
429
+ }
430
+ }
431
+ if (m["type"] === "connector" && m["mcpServer"] === void 0) {
432
+ errors.push({ field: "mcpServer", message: "Required for connector type extensions" });
433
+ }
434
+ if (m["channelTransport"] !== void 0) {
435
+ if (m["type"] !== "channel") {
436
+ errors.push({ field: "channelTransport", message: "channelTransport is only valid for channel type extensions" });
437
+ } else if (m["channelTransport"] !== "worker" && m["channelTransport"] !== "api") {
438
+ errors.push({ field: "channelTransport", message: 'Must be "worker" or "api"' });
439
+ }
440
+ }
441
+ if (m["trust"] !== void 0) {
442
+ if (!VALID_TRUST_TIERS.includes(m["trust"])) {
443
+ errors.push({ field: "trust", message: `Must be one of: ${VALID_TRUST_TIERS.join(", ")}` });
444
+ }
445
+ }
446
+ if (m["dataResidency"] !== void 0) {
447
+ validateDataResidency(m["dataResidency"], errors);
448
+ } else if (complianceLevel === "full") {
449
+ errors.push({
450
+ field: "dataResidency",
451
+ message: "dataResidency is required at Full compliance. Omission treated as sendsDataExternally: true with unknown destinations.",
452
+ severity: "warning"
453
+ });
454
+ }
455
+ if (m["type"] === "agent" && m["escalation"] !== void 0) {
456
+ validateEscalation(m["escalation"], errors);
457
+ }
458
+ if (m["modelRequirements"] !== void 0) {
459
+ validateModelRequirements(m["modelRequirements"], errors);
460
+ }
461
+ const hardErrors = errors.filter((e) => e.severity !== "warning");
462
+ return { valid: hardErrors.length === 0, errors };
463
+ }
464
+ function validateDataResidency(dr, errors) {
465
+ if (typeof dr !== "object" || dr === null) {
466
+ errors.push({ field: "dataResidency", message: "Must be an object" });
467
+ return;
468
+ }
469
+ const obj = dr;
470
+ if (typeof obj["sendsDataExternally"] !== "boolean") {
471
+ errors.push({ field: "dataResidency.sendsDataExternally", message: "Must be a boolean" });
472
+ }
473
+ if (obj["sendsDataExternally"] === true && !Array.isArray(obj["externalDestinations"])) {
474
+ errors.push({
475
+ field: "dataResidency.externalDestinations",
476
+ message: "Must be provided when sendsDataExternally is true"
477
+ });
478
+ }
479
+ }
480
+ function validateEscalation(esc, errors) {
481
+ if (typeof esc !== "object" || esc === null) {
482
+ errors.push({ field: "escalation", message: "Must be an object" });
483
+ return;
484
+ }
485
+ const obj = esc;
486
+ if (obj["irreversibleActions"] !== void 0 && !Array.isArray(obj["irreversibleActions"])) {
487
+ errors.push({ field: "escalation.irreversibleActions", message: "Must be an array of strings" });
488
+ }
489
+ }
490
+ function validateModelRequirements(mr, errors) {
491
+ if (typeof mr !== "object" || mr === null) {
492
+ errors.push({ field: "modelRequirements", message: "Must be an object" });
493
+ return;
494
+ }
495
+ const obj = mr;
496
+ if (obj["minimumContextWindow"] !== void 0 && typeof obj["minimumContextWindow"] !== "number") {
497
+ errors.push({ field: "modelRequirements.minimumContextWindow", message: "Must be a number" });
498
+ }
499
+ if (obj["localModelAcceptable"] !== void 0 && typeof obj["localModelAcceptable"] !== "boolean") {
500
+ errors.push({ field: "modelRequirements.localModelAcceptable", message: "Must be a boolean" });
501
+ }
502
+ }
503
+ function isSemver(s) {
504
+ return /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$/.test(s);
505
+ }
506
+ function isValidPackageName(s) {
507
+ return /^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(s);
508
+ }
509
+ function isValidCapability(token) {
510
+ if (STANDARD_CAPABILITIES.has(token)) return true;
511
+ if (/^memory:(read|write):([a-z_]+)$/.test(token)) {
512
+ const entityType = token.split(":")[2];
513
+ if (VALID_ENTITY_TYPES.includes(entityType)) return true;
514
+ }
515
+ if (/^entity:(create|modify|delete):([a-z_]+)$/.test(token)) {
516
+ const entityType = token.split(":")[2];
517
+ if (VALID_ENTITY_TYPES.includes(entityType)) return true;
518
+ }
519
+ if (/^connections:[a-z0-9-]+$/.test(token)) return true;
520
+ if (/^host:[a-z0-9-]+:[a-z0-9-:]+$/.test(token)) return true;
521
+ return false;
522
+ }
523
+ function isHostScopedCapability(token) {
524
+ return /^host:[a-z0-9-]+:[a-z0-9-:]+$/.test(token);
525
+ }
526
+ function isUnscopedMemoryCapability(token) {
527
+ return token === "memory:read" || token === "memory:write";
528
+ }
529
+ function isWildcardMemoryCapability(token) {
530
+ return token === "memory:read:*" || token === "memory:write:*";
531
+ }
532
+
533
+ // src/validation/signature.ts
534
+ var OWNER_IDENTITY_SUFFIXES = ["@joeybuilt-official", "@plexo-official"];
535
+ function verifySignature(manifest, meta) {
536
+ const declared = manifest.trust ?? "community";
537
+ if (!meta || meta.signatureType === "none" || !meta.signature) {
538
+ if (declared === "owner" || declared === "verified") {
539
+ return {
540
+ ok: false,
541
+ signatureType: "none",
542
+ signerIdentity: null,
543
+ effectiveTrust: "community",
544
+ message: `Manifest declares trust "${declared}" but is unsigned \u2014 downgraded to community.`
545
+ };
546
+ }
547
+ return {
548
+ ok: true,
549
+ signatureType: "none",
550
+ signerIdentity: null,
551
+ effectiveTrust: "community",
552
+ message: "Unsigned community extension."
553
+ };
554
+ }
555
+ if (meta.signatureType === "sigstore") {
556
+ const isOwner = OWNER_IDENTITY_SUFFIXES.some(
557
+ (suffix) => meta.signerIdentity.toLowerCase().endsWith(suffix)
558
+ );
559
+ const ceiling = isOwner ? "owner" : "verified";
560
+ const effectiveTrust = lowerOf(declared, ceiling);
561
+ return {
562
+ ok: true,
563
+ signatureType: "sigstore",
564
+ signerIdentity: meta.signerIdentity,
565
+ effectiveTrust,
566
+ message: `Sigstore signature accepted (stub v1) \u2014 signer ${meta.signerIdentity}.`
567
+ };
568
+ }
569
+ if (meta.signatureType === "ecdsa-p256") {
570
+ const effectiveTrust = lowerOf(declared, "verified");
571
+ return {
572
+ ok: true,
573
+ signatureType: "ecdsa-p256",
574
+ signerIdentity: meta.signerIdentity,
575
+ effectiveTrust,
576
+ message: `ECDSA signature accepted (stub v1) \u2014 signer ${meta.signerIdentity}.`
577
+ };
578
+ }
579
+ return {
580
+ ok: false,
581
+ signatureType: "none",
582
+ signerIdentity: null,
583
+ effectiveTrust: "community",
584
+ message: `Unknown signature type: ${String(meta.signatureType)}`
585
+ };
586
+ }
587
+ function lowerOf(a, b) {
588
+ const order = { community: 0, verified: 1, owner: 2 };
589
+ return order[a] <= order[b] ? a : b;
590
+ }
591
+ export {
592
+ PEX_VERSION,
593
+ TOPICS,
594
+ createChannelClient,
595
+ customTopic,
596
+ validateManifest,
597
+ verifySignature
598
+ };
599
+ //# sourceMappingURL=index.js.map