@jdscript/opencode-notify 0.1.1 → 0.1.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/VERIFICATION.md +22 -0
- package/dist/index.js +3 -1
- package/dist/rpc.d.ts +27 -0
- package/dist/rpc.js +4 -1
- package/dist/sender.d.ts +3 -0
- package/dist/sender.js +45 -6
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
package/VERIFICATION.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Verification — initial plugin / app integration
|
|
2
2
|
|
|
3
|
+
## 0.1.2 scoped Live Activity encryption
|
|
4
|
+
|
|
5
|
+
The `info` capability advertises `activityEncryption: "route-v1"`. An `attach`
|
|
6
|
+
can provide a dedicated recipient public key/authentication secret for that
|
|
7
|
+
activity; alerts retain the subscription key. Route encryption and its expiry
|
|
8
|
+
cannot change under an existing route ID. Live jobs are capped by route expiry.
|
|
9
|
+
|
|
10
|
+
Fourteen sender/crypto tests (85 assertions) pass, including cross-key rejection,
|
|
11
|
+
immutable activity keys/expiry, persisted activity encryption across restart,
|
|
12
|
+
and an explicit encrypted test update. The packed artifact passed the isolated
|
|
13
|
+
upstream SDK → Rust relay → Swift receiver pipeline with six envelopes. The
|
|
14
|
+
Swift activity path selects temporary route keys with a failing Keychain loader,
|
|
15
|
+
proving that rendering does not depend on direct Keychain access. Legacy attach
|
|
16
|
+
inputs without route encryption remain supported for older clients.
|
|
17
|
+
|
|
3
18
|
## 0.1.1 distribution check
|
|
4
19
|
|
|
5
20
|
`@opencode/plugin` is a runtime dependency. The 0.1.0 CLI-install path resolved
|
|
@@ -11,6 +26,13 @@ loopback registry served the packed artifact and the real upstream SDK resolved
|
|
|
11
26
|
its dependency graph in a fresh home. The complete Rust relay/Swift receiver
|
|
12
27
|
pipeline accepted six encrypted envelopes. No model or Apple push was used.
|
|
13
28
|
|
|
29
|
+
The published npm `@jdscript/opencode-notify@0.1.1` then passed both
|
|
30
|
+
`python3 ../scripts/mock-plugin-install.py --package @jdscript/opencode-notify@0.1.1`
|
|
31
|
+
and `python3 scripts/check-upstream.py --rust-relay --swift --package @jdscript/opencode-notify@0.1.1`.
|
|
32
|
+
These freshly downloaded the release, verified CLI installation and automatic
|
|
33
|
+
global RPC loading, and decrypted all six relay envelopes with the production
|
|
34
|
+
Swift receiver. Published tarball SHA-1: `0b98f8d23f93277114a7a16ed8a2f599918ea746`.
|
|
35
|
+
|
|
14
36
|
## Passed
|
|
15
37
|
|
|
16
38
|
- TypeScript typecheck and distributable build.
|
package/dist/index.js
CHANGED
|
@@ -33,7 +33,7 @@ function makePlugin() {
|
|
|
33
33
|
})();
|
|
34
34
|
const runtime = await shared.sender;
|
|
35
35
|
const registration = await ctx.rpc.register(Definition, {
|
|
36
|
-
info: async () => ({ version: 1, encoding: "aes128gcm", applicationVersion: 1, activityScopes: ["session", "server", "workspace"] }),
|
|
36
|
+
info: async () => ({ version: 1, encoding: "aes128gcm", applicationVersion: 1, activityScopes: ["session", "server", "workspace"], activityEncryption: "route-v1" }),
|
|
37
37
|
subscribe: async (input, context) => runtime
|
|
38
38
|
.subscribe(input)
|
|
39
39
|
.catch((error) => context.error("invalid", "Notification setup failed", { code: code(error) })),
|
|
@@ -54,6 +54,8 @@ function makePlugin() {
|
|
|
54
54
|
detach: async (input, context) => runtime
|
|
55
55
|
.detach(input.subscriptionID, input.activityID)
|
|
56
56
|
.catch((error) => context.error("invalid", "Activity removal failed", { code: code(error) })),
|
|
57
|
+
test: async (input, context) => runtime.testActivity(input.subscriptionID, input.activityID)
|
|
58
|
+
.catch((error) => context.error("invalid", "Activity test failed", { code: code(error) })),
|
|
57
59
|
});
|
|
58
60
|
const controller = new AbortController();
|
|
59
61
|
const events = (async () => {
|
package/dist/rpc.d.ts
CHANGED
|
@@ -24,6 +24,10 @@ export declare const Route: z.ZodObject<{
|
|
|
24
24
|
subscriptionID: z.ZodString;
|
|
25
25
|
activityID: z.ZodString;
|
|
26
26
|
}, z.core.$strict>;
|
|
27
|
+
export declare const ActivityEncryption: z.ZodObject<{
|
|
28
|
+
recipientPublicKey: z.ZodString;
|
|
29
|
+
authSecret: z.ZodString;
|
|
30
|
+
}, z.core.$strict>;
|
|
27
31
|
export declare const Attach: z.ZodObject<{
|
|
28
32
|
subscriptionID: z.ZodString;
|
|
29
33
|
activityID: z.ZodString;
|
|
@@ -35,6 +39,10 @@ export declare const Attach: z.ZodObject<{
|
|
|
35
39
|
workspace: "workspace";
|
|
36
40
|
}>>;
|
|
37
41
|
serverName: z.ZodOptional<z.ZodString>;
|
|
42
|
+
encryption: z.ZodOptional<z.ZodObject<{
|
|
43
|
+
recipientPublicKey: z.ZodString;
|
|
44
|
+
authSecret: z.ZodString;
|
|
45
|
+
}, z.core.$strict>>;
|
|
38
46
|
seedComplete: z.ZodOptional<z.ZodBoolean>;
|
|
39
47
|
seeds: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
40
48
|
sessionID: z.ZodString;
|
|
@@ -74,6 +82,7 @@ export declare const Definition: {
|
|
|
74
82
|
server: "server";
|
|
75
83
|
workspace: "workspace";
|
|
76
84
|
}>>>;
|
|
85
|
+
activityEncryption: z.ZodOptional<z.ZodLiteral<"route-v1">>;
|
|
77
86
|
}, z.core.$strict>;
|
|
78
87
|
readonly errors: {
|
|
79
88
|
invalid: z.ZodObject<{
|
|
@@ -161,6 +170,10 @@ export declare const Definition: {
|
|
|
161
170
|
workspace: "workspace";
|
|
162
171
|
}>>;
|
|
163
172
|
serverName: z.ZodOptional<z.ZodString>;
|
|
173
|
+
encryption: z.ZodOptional<z.ZodObject<{
|
|
174
|
+
recipientPublicKey: z.ZodString;
|
|
175
|
+
authSecret: z.ZodString;
|
|
176
|
+
}, z.core.$strict>>;
|
|
164
177
|
seedComplete: z.ZodOptional<z.ZodBoolean>;
|
|
165
178
|
seeds: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
166
179
|
sessionID: z.ZodString;
|
|
@@ -195,5 +208,19 @@ export declare const Definition: {
|
|
|
195
208
|
}, z.core.$strict>;
|
|
196
209
|
};
|
|
197
210
|
};
|
|
211
|
+
readonly test: {
|
|
212
|
+
readonly input: z.ZodObject<{
|
|
213
|
+
subscriptionID: z.ZodString;
|
|
214
|
+
activityID: z.ZodString;
|
|
215
|
+
}, z.core.$strict>;
|
|
216
|
+
readonly output: z.ZodObject<{
|
|
217
|
+
ok: z.ZodLiteral<true>;
|
|
218
|
+
}, z.core.$strict>;
|
|
219
|
+
readonly errors: {
|
|
220
|
+
invalid: z.ZodObject<{
|
|
221
|
+
code: z.ZodString;
|
|
222
|
+
}, z.core.$strict>;
|
|
223
|
+
};
|
|
224
|
+
};
|
|
198
225
|
};
|
|
199
226
|
};
|
package/dist/rpc.js
CHANGED
|
@@ -21,10 +21,12 @@ export const Subscribe = z
|
|
|
21
21
|
.strict();
|
|
22
22
|
export const Reference = z.object({ subscriptionID: UUID }).strict();
|
|
23
23
|
export const Route = z.object({ subscriptionID: UUID, activityID: UUID }).strict();
|
|
24
|
+
export const ActivityEncryption = z.object({ recipientPublicKey: z.string().length(87), authSecret: z.string().length(22) }).strict();
|
|
24
25
|
export const Attach = Route.extend({
|
|
25
26
|
sessionID: z.string().startsWith("ses").max(200), expiresAt: seconds,
|
|
26
27
|
scope: z.enum(["session", "server", "workspace"]).optional(),
|
|
27
28
|
serverName: z.string().max(160).optional(),
|
|
29
|
+
encryption: ActivityEncryption.optional(),
|
|
28
30
|
seedComplete: z.boolean().optional(),
|
|
29
31
|
seeds: z.array(z.object({ sessionID: z.string().startsWith("ses").max(200), running: z.boolean(), waiting: z.array(z.string().max(200)).max(64) }).strict()).max(512).optional(),
|
|
30
32
|
shells: z.array(z.object({ id: z.string().max(200), sessionID: z.string().startsWith("ses").max(200).optional() }).strict()).max(512).optional(),
|
|
@@ -47,7 +49,7 @@ export const Definition = Rpc.define({
|
|
|
47
49
|
info: {
|
|
48
50
|
input: z.object({}).strict(),
|
|
49
51
|
output: z
|
|
50
|
-
.object({ version: z.literal(1), encoding: z.literal("aes128gcm"), applicationVersion: z.literal(1), activityScopes: z.array(z.enum(["session", "server", "workspace"])).optional() })
|
|
52
|
+
.object({ version: z.literal(1), encoding: z.literal("aes128gcm"), applicationVersion: z.literal(1), activityScopes: z.array(z.enum(["session", "server", "workspace"])).optional(), activityEncryption: z.literal("route-v1").optional() })
|
|
51
53
|
.strict(),
|
|
52
54
|
errors,
|
|
53
55
|
},
|
|
@@ -56,5 +58,6 @@ export const Definition = Rpc.define({
|
|
|
56
58
|
status: { input: Reference, output: Status, errors },
|
|
57
59
|
attach: { input: Attach, output: ok, errors },
|
|
58
60
|
detach: { input: Route, output: ok, errors },
|
|
61
|
+
test: { input: Route, output: ok, errors },
|
|
59
62
|
},
|
|
60
63
|
});
|
package/dist/sender.d.ts
CHANGED
|
@@ -51,6 +51,9 @@ export declare class Sender {
|
|
|
51
51
|
detach(subscriptionID: string, activityID: string): Promise<{
|
|
52
52
|
ok: true;
|
|
53
53
|
}>;
|
|
54
|
+
testActivity(subscriptionID: string, activityID: string): Promise<{
|
|
55
|
+
ok: true;
|
|
56
|
+
}>;
|
|
54
57
|
/** Semantic events only: no reasoning/text deltas, transcripts, tool arguments or outputs. */
|
|
55
58
|
onEvent(event: Event): Promise<void>;
|
|
56
59
|
private node;
|
package/dist/sender.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createECDH, createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { Subscribe } from "./rpc.js";
|
|
4
|
+
import { ActivityEncryption, Subscribe } from "./rpc.js";
|
|
5
5
|
import { decode, encrypt } from "./crypto.js";
|
|
6
6
|
const StoredSubscription = z.object({
|
|
7
7
|
input: Subscribe,
|
|
@@ -20,6 +20,7 @@ const StoredActivity = z.object({
|
|
|
20
20
|
lastSent: z.number(),
|
|
21
21
|
scope: z.enum(["session", "server", "workspace"]).optional(),
|
|
22
22
|
serverName: z.string().optional(),
|
|
23
|
+
encryption: ActivityEncryption.optional(),
|
|
23
24
|
});
|
|
24
25
|
const StoredJob = z.object({
|
|
25
26
|
subscriptionID: z.string(),
|
|
@@ -163,6 +164,21 @@ export class Sender {
|
|
|
163
164
|
throw new SenderError("activity_binding_changed");
|
|
164
165
|
if (old && (old.scope ?? "session") !== (input.scope ?? "session"))
|
|
165
166
|
throw new SenderError("activity_binding_changed");
|
|
167
|
+
if (old && (old.encryption?.recipientPublicKey !== input.encryption?.recipientPublicKey || old.encryption?.authSecret !== input.encryption?.authSecret))
|
|
168
|
+
throw new SenderError("activity_encryption_changed");
|
|
169
|
+
if (old?.encryption && old.expiresAt !== input.expiresAt)
|
|
170
|
+
throw new SenderError("activity_expiry_changed");
|
|
171
|
+
if (input.encryption) {
|
|
172
|
+
const probe = createECDH("prime256v1");
|
|
173
|
+
probe.generateKeys();
|
|
174
|
+
try {
|
|
175
|
+
probe.computeSecret(decode(input.encryption.recipientPublicKey, 65));
|
|
176
|
+
decode(input.encryption.authSecret, 16);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
throw new SenderError("invalid_activity_encryption");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
166
182
|
if (input.scope === "server" || input.scope === "workspace") {
|
|
167
183
|
if (input.sessionID !== "ses_workspace")
|
|
168
184
|
throw new SenderError("invalid_workspace_binding");
|
|
@@ -192,7 +208,7 @@ export class Sender {
|
|
|
192
208
|
}
|
|
193
209
|
const route = {
|
|
194
210
|
subscriptionID: input.subscriptionID, activityID: input.activityID, sessionID: input.sessionID,
|
|
195
|
-
expiresAt: input.expiresAt, scope: input.scope, serverName: input.serverName,
|
|
211
|
+
expiresAt: input.expiresAt, scope: input.scope, serverName: input.serverName, encryption: input.encryption,
|
|
196
212
|
ended: false,
|
|
197
213
|
unavailable: false,
|
|
198
214
|
lastTimestamp: old?.lastTimestamp ?? 0,
|
|
@@ -228,6 +244,28 @@ export class Sender {
|
|
|
228
244
|
return { ok: true };
|
|
229
245
|
});
|
|
230
246
|
}
|
|
247
|
+
testActivity(subscriptionID, activityID) {
|
|
248
|
+
return this.serial(async () => {
|
|
249
|
+
const sub = this.requireSubscription(subscriptionID, true);
|
|
250
|
+
const route = this.activities.get(routeKey(subscriptionID, activityID));
|
|
251
|
+
if (!route || route.ended || route.unavailable || route.expiresAt <= now())
|
|
252
|
+
throw new SenderError("activity_unavailable");
|
|
253
|
+
if ((sub.billingUntil ?? 0) > now())
|
|
254
|
+
throw new SenderError("entitlement_required");
|
|
255
|
+
const timestamp = now();
|
|
256
|
+
if (timestamp <= route.lastTimestamp)
|
|
257
|
+
throw new SenderError("activity_update_pending");
|
|
258
|
+
const root = isOverview(route) ? undefined : await this.node(route.sessionID);
|
|
259
|
+
const content = root ? this.content(root) : this.workspaceContent(route);
|
|
260
|
+
content.detail = "Test update";
|
|
261
|
+
await this.queue("activity/" + routeKey(subscriptionID, activityID), sub, root ? { id: root.info.id, directory: root.info.location.directory } : { id: "ses_workspace", directory: "/" }, content, { activityID, event: "update", timestamp }, route.scope === "workspace" ? false : undefined, route);
|
|
262
|
+
route.lastTimestamp = timestamp;
|
|
263
|
+
route.lastSent = timestamp;
|
|
264
|
+
await this.save("activity/" + routeKey(subscriptionID, activityID), route);
|
|
265
|
+
this.schedule();
|
|
266
|
+
return { ok: true };
|
|
267
|
+
});
|
|
268
|
+
}
|
|
231
269
|
/** Semantic events only: no reasoning/text deltas, transcripts, tool arguments or outputs. */
|
|
232
270
|
onEvent(event) {
|
|
233
271
|
return this.serial(async () => {
|
|
@@ -549,7 +587,7 @@ export class Sender {
|
|
|
549
587
|
activityID: route.activityID,
|
|
550
588
|
event,
|
|
551
589
|
timestamp,
|
|
552
|
-
}, route.scope === "workspace" ? settled : undefined);
|
|
590
|
+
}, route.scope === "workspace" ? settled : undefined, route);
|
|
553
591
|
route.lastTimestamp = timestamp;
|
|
554
592
|
route.lastSent = timestamp;
|
|
555
593
|
this.dirty.delete(routeKey(route.subscriptionID, route.activityID));
|
|
@@ -557,7 +595,7 @@ export class Sender {
|
|
|
557
595
|
this.dirty.set(routeKey(route.subscriptionID, route.activityID), false);
|
|
558
596
|
await this.save("activity/" + routeKey(route.subscriptionID, route.activityID), route);
|
|
559
597
|
}
|
|
560
|
-
async queue(key, sub, binding, content, activity, settled) {
|
|
598
|
+
async queue(key, sub, binding, content, activity, settled, route) {
|
|
561
599
|
if (this.jobs.size >= 512 && !this.jobs.has(key))
|
|
562
600
|
throw new SenderError("outbox_full");
|
|
563
601
|
const eventID = randomUUID();
|
|
@@ -570,7 +608,7 @@ export class Sender {
|
|
|
570
608
|
kind: activity ? "live_activity" : "alert",
|
|
571
609
|
producer: "server",
|
|
572
610
|
issuedAt: now(),
|
|
573
|
-
expiresAt: Math.min(now() + 3600, sub.input.expiresAt),
|
|
611
|
+
expiresAt: Math.min(now() + 3600, sub.input.expiresAt, route?.expiresAt ?? Number.MAX_SAFE_INTEGER),
|
|
574
612
|
sessionID: binding.id,
|
|
575
613
|
directory: binding.directory,
|
|
576
614
|
content,
|
|
@@ -579,7 +617,8 @@ export class Sender {
|
|
|
579
617
|
const bytes = Buffer.from(JSON.stringify(payload));
|
|
580
618
|
if (bytes.length > 2400 || Buffer.byteLength(payload.directory) > 1024)
|
|
581
619
|
throw new SenderError("content_too_large");
|
|
582
|
-
const
|
|
620
|
+
const recipient = route?.encryption ?? sub.input;
|
|
621
|
+
const ciphertext = encrypt(bytes, recipient.recipientPublicKey, recipient.authSecret);
|
|
583
622
|
const job = {
|
|
584
623
|
subscriptionID: sub.input.subscriptionID,
|
|
585
624
|
expiresAt: payload.expiresAt,
|
package/dist/types.d.ts
CHANGED