opencode-session-port 0.0.0-stage → 0.1.1

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -2
  3. package/dist/tui.js +10276 -0
  4. package/package.json +48 -4
  5. package/src/core/contracts/crypto.ts +66 -0
  6. package/src/core/contracts/index.ts +13 -0
  7. package/src/core/contracts/json.ts +142 -0
  8. package/src/core/contracts/session.ts +12 -0
  9. package/src/core/crypto/container.ts +686 -0
  10. package/src/core/crypto/envelope.ts +232 -0
  11. package/src/core/crypto/errors.ts +52 -0
  12. package/src/core/crypto/gzip-stream.ts +388 -0
  13. package/src/core/crypto/index.ts +13 -0
  14. package/src/core/crypto/kdf.ts +101 -0
  15. package/src/core/crypto/policy.ts +202 -0
  16. package/src/core/crypto/zero.ts +33 -0
  17. package/src/core/restore/identity.ts +202 -0
  18. package/src/core/restore/index.ts +24 -0
  19. package/src/core/restore/planner.ts +1122 -0
  20. package/src/core/sessions/collector.ts +323 -0
  21. package/src/core/sessions/errors.ts +37 -0
  22. package/src/core/sessions/guards.ts +11 -0
  23. package/src/core/sessions/index.ts +29 -0
  24. package/src/core/sessions/policy.ts +80 -0
  25. package/src/core/sessions/port.ts +88 -0
  26. package/src/core/sessions/serialize.ts +15 -0
  27. package/src/core/sessions/unsettled.ts +35 -0
  28. package/src/integrations/omo/index.ts +28 -0
  29. package/src/tui/adapters/host-client.ts +254 -0
  30. package/src/tui/adapters/index.ts +72 -0
  31. package/src/tui/adapters/location-resolver.ts +281 -0
  32. package/src/tui/adapters/project-destinations.ts +271 -0
  33. package/src/tui/adapters/session-id-inventory.ts +378 -0
  34. package/src/tui/adapters/session-import.ts +189 -0
  35. package/src/tui/adapters/session-read.ts +115 -0
  36. package/src/tui/artifacts/files.ts +285 -0
  37. package/src/tui/orchestration/checkpoint.ts +160 -0
  38. package/src/tui/orchestration/index.ts +55 -0
  39. package/src/tui/orchestration/restore-auth.ts +94 -0
  40. package/src/tui/orchestration/restore-execution.ts +381 -0
  41. package/src/tui/orchestration/restore-preflight.ts +223 -0
  42. package/src/tui/orchestration/shared.ts +75 -0
  43. package/src/tui/ui/password-dialog.tsx +251 -0
  44. package/src/tui/ui/password-flow.ts +288 -0
  45. package/src/tui/ui/restore-project-selector.ts +62 -0
  46. package/src/tui/ui/restore-report-dialog.tsx +251 -0
  47. package/src/tui/ui/restore-report-flow.ts +94 -0
  48. package/src/tui/ui/sessionport-commands.tsx +65 -0
  49. package/src/tui/wiring/composition.ts +354 -0
  50. package/src/tui/wiring/experimental-profile.ts +86 -0
  51. package/src/tui/wiring/index.ts +51 -0
  52. package/src/tui/wiring/install.ts +108 -0
  53. package/src/tui/wiring/reports.ts +340 -0
  54. package/src/tui.ts +34 -0
  55. package/tsconfig.json +18 -0
  56. package/tui.ts +17 -0
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "opencode-session-port",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "license": "MIT",
6
+ "description": "SessionPort: experimental Windows-first OpenCode TUI plugin for password-encrypted session checkpoint and restore.",
7
+ "keywords": [
8
+ "opencode",
9
+ "opencode-plugin",
10
+ "tui",
11
+ "session",
12
+ "checkpoint",
13
+ "restore",
14
+ "encryption"
15
+ ],
16
+ "type": "module",
17
+ "files": [
18
+ "tui.ts",
19
+ "src/**",
20
+ "dist/**",
21
+ "tsconfig.json"
22
+ ],
23
+ "exports": {
24
+ "./tui": "./dist/tui.js"
25
+ },
26
+ "scripts": {
27
+ "typecheck": "tsc --noEmit",
28
+ "test": "bun test",
29
+ "verify:package": "bun run scripts/package-verify/verify.ts",
30
+ "build:tui": "bun run scripts/build-tui.ts",
31
+ "prepack": "bun run build:tui"
32
+ },
33
+ "dependencies": {
34
+ "@opencode/plugin": "2.0.22",
35
+ "libsodium-wrappers-sumo": "0.8.4",
36
+ "pako": "3.0.2"
37
+ },
38
+ "peerDependencies": {
39
+ "@opentui/core": "0.5.14",
40
+ "@opentui/solid": "0.5.14",
41
+ "solid-js": "1.9.12"
42
+ },
43
+ "devDependencies": {
44
+ "@opentui/core": "0.5.14",
45
+ "@opentui/solid": "0.5.14",
46
+ "@types/node": "24.12.2",
47
+ "solid-js": "1.9.12",
48
+ "typescript": "5.9.3"
49
+ }
50
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * One-shot byte source. Each yielded chunk is borrowed: producers must not
3
+ * mutate it while the consumer is processing it; consumers must copy a chunk
4
+ * if they retain it after advancing the source.
5
+ */
6
+ export type ByteSource = AsyncIterable<Uint8Array>;
7
+
8
+ /** Raw in-memory password bytes; text encoding/normalization remains an explicit open decision. */
9
+ export type PasswordMaterial = Uint8Array;
10
+
11
+ /**
12
+ * Fixed byte length of an `ArtifactIdentity` (128 bits). The owner-selected
13
+ * O-22 scheme (2026-10-09) generates one random identity per checkpoint
14
+ * artifact; its exact artifact placement remains PROVISIONAL (O-02).
15
+ */
16
+ export const ARTIFACT_IDENTITY_BYTES = 16;
17
+
18
+ /**
19
+ * Opaque per-artifact identity: exactly 16 random bytes (128 bits) generated
20
+ * once per `seal` and authenticated as part of the artifact. It is public
21
+ * artifact data, not a secret, and must never be confused with the Argon2
22
+ * salt. Callers must treat it as opaque and use it only as the stable input
23
+ * for deterministic remapped-ID derivation (O-22).
24
+ */
25
+ export type ArtifactIdentity = Uint8Array;
26
+
27
+ /** Opaque identifier for an explicitly selected, allow-listed crypto policy. */
28
+ export interface CryptoPolicy {
29
+ /** A code-selected profile identifier; never take policy values from an artifact. */
30
+ readonly id: string;
31
+ }
32
+
33
+ /**
34
+ * Fully authenticated and decompressed bytes, staged in bounded memory.
35
+ * This value is returned only after the complete stream (including final tag),
36
+ * clean gzip termination, and configured bounds have all passed. The caller
37
+ * still must parse/validate the payload and preflight every session before any
38
+ * import. Ownership of `bytes` transfers exclusively to the caller; callers
39
+ * MUST treat it as immutable and must not retain/mutate aliases before parsing
40
+ * and preflight. No partial plaintext stream/callback is exposed by this boundary.
41
+ */
42
+ export interface AuthenticatedPayload {
43
+ readonly bytes: Uint8Array;
44
+
45
+ /**
46
+ * The artifact's authenticated identity, surfaced under the same conditions
47
+ * as `bytes`: only after the complete stream (including the final tag),
48
+ * clean strict-gzip end-of-input, and every configured bound have passed.
49
+ * Caller-owned copy; opaque; never secret. Not exposed on any failure path.
50
+ */
51
+ readonly artifactIdentity: ArtifactIdentity;
52
+ }
53
+
54
+ /** Crypto service contract. `create` requires an explicit, allow-listed policy; no default profile is implied. */
55
+ export interface CheckpointCrypto {
56
+ /** Promise boundary permits asynchronous execution; it does not guarantee the synchronous KDF will not block. */
57
+ seal(plaintext: ByteSource, password: PasswordMaterial): Promise<ByteSource>;
58
+
59
+ /** Must reject until the entire artifact has authenticated and decompressed successfully. */
60
+ openAndStage(artifact: ByteSource, password: PasswordMaterial): Promise<AuthenticatedPayload>;
61
+ }
62
+
63
+ /** Public construction boundary: omitting policy is a type error; factories must reject unknown IDs. */
64
+ export interface CheckpointCryptoFactory {
65
+ create(policy: CryptoPolicy): Promise<CheckpointCrypto>;
66
+ }
@@ -0,0 +1,13 @@
1
+ export { encodeJson, snapshotJsonValue } from "./json.ts";
2
+ export type { JsonObject, JsonPrimitive, JsonValue } from "./json.ts";
3
+ export { ARTIFACT_IDENTITY_BYTES } from "./crypto.ts";
4
+ export type {
5
+ ArtifactIdentity,
6
+ AuthenticatedPayload,
7
+ ByteSource,
8
+ CheckpointCrypto,
9
+ CheckpointCryptoFactory,
10
+ CryptoPolicy,
11
+ PasswordMaterial,
12
+ } from "./crypto.ts";
13
+ export type { CheckpointPayload, SessionRecord } from "./session.ts";
@@ -0,0 +1,142 @@
1
+ /** JSON-compatible values accepted at the SessionPort payload boundary. */
2
+ export type JsonPrimitive = string | number | boolean | null;
3
+
4
+ export type JsonValue =
5
+ | JsonPrimitive
6
+ | readonly JsonValue[]
7
+ | { readonly [key: string]: JsonValue };
8
+
9
+ export type JsonObject = { readonly [key: string]: JsonValue };
10
+
11
+ type VisitFrame = {
12
+ kind: "visit";
13
+ value: unknown;
14
+ path: string;
15
+ assign: (copy: JsonValue) => void;
16
+ };
17
+
18
+ type LeaveFrame = { kind: "leave"; value: object };
19
+
20
+ /**
21
+ * Copies a JSON-only snapshot, rejecting data that JSON would omit or coerce.
22
+ * Copies use null prototypes so inherited `toJSON` hooks cannot change the
23
+ * serialized representation. The traversal is iterative to avoid JS call-stack
24
+ * recursion. Resource limits are enforced by higher layers.
25
+ *
26
+ * `-0` is rejected because JSON.stringify would serialize it as `0`.
27
+ */
28
+ export function snapshotJsonValue(value: unknown, rootPath = "$"): JsonValue {
29
+ let root: JsonValue | undefined;
30
+ const active = new WeakSet<object>();
31
+ const stack: Array<VisitFrame | LeaveFrame> = [
32
+ { kind: "visit", value, path: rootPath, assign: (copy) => (root = copy) },
33
+ ];
34
+
35
+ while (stack.length > 0) {
36
+ const frame = stack.pop()!;
37
+ if (frame.kind === "leave") {
38
+ active.delete(frame.value);
39
+ continue;
40
+ }
41
+
42
+ const current = frame.value;
43
+ if (current === null || typeof current === "string" || typeof current === "boolean") {
44
+ frame.assign(current);
45
+ continue;
46
+ }
47
+ if (typeof current === "number") {
48
+ if (!Number.isFinite(current)) throw new TypeError(`Non-finite number at ${frame.path}`);
49
+ if (Object.is(current, -0)) throw new TypeError(`Negative zero is not JSON-stable at ${frame.path}`);
50
+ frame.assign(current);
51
+ continue;
52
+ }
53
+ if (typeof current !== "object") {
54
+ throw new TypeError(`Non-JSON value (${typeof current}) at ${frame.path}`);
55
+ }
56
+ if (active.has(current)) throw new TypeError(`Cyclic value at ${frame.path}`);
57
+
58
+ if (Array.isArray(current)) {
59
+ const prototype = Object.getPrototypeOf(current);
60
+ if (prototype !== Array.prototype && prototype !== null) {
61
+ throw new TypeError(`Non-plain array at ${frame.path}`);
62
+ }
63
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(current, "length");
64
+ const length = lengthDescriptor?.value;
65
+ if (!Number.isSafeInteger(length) || length < 0) {
66
+ throw new TypeError(`Invalid array length at ${frame.path}`);
67
+ }
68
+
69
+ const values = new Map<number, unknown>();
70
+ for (const key of Reflect.ownKeys(current)) {
71
+ if (key === "length") continue;
72
+ if (typeof key !== "string" || !/^(0|[1-9]\d*)$/.test(key)) {
73
+ throw new TypeError(`Non-JSON array property at ${frame.path}`);
74
+ }
75
+ const index = Number(key);
76
+ if (!Number.isSafeInteger(index) || index >= length) {
77
+ throw new TypeError(`Unsupported array entry at ${frame.path}[${key}]`);
78
+ }
79
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
80
+ if (!descriptor?.enumerable || !("value" in descriptor)) {
81
+ throw new TypeError(`Accessor or hidden array entry at ${frame.path}[${key}]`);
82
+ }
83
+ values.set(index, descriptor.value);
84
+ }
85
+ if (values.size !== length) throw new TypeError(`Sparse array at ${frame.path}`);
86
+
87
+ const copy = new Array<JsonValue>(length);
88
+ Object.setPrototypeOf(copy, null);
89
+ frame.assign(copy);
90
+ active.add(current);
91
+ stack.push({ kind: "leave", value: current });
92
+ for (let index = length - 1; index >= 0; index--) {
93
+ stack.push({
94
+ kind: "visit",
95
+ value: values.get(index),
96
+ path: `${frame.path}[${index}]`,
97
+ assign: (child) => (copy[index] = child),
98
+ });
99
+ }
100
+ continue;
101
+ }
102
+
103
+ const prototype = Object.getPrototypeOf(current);
104
+ if (prototype !== Object.prototype && prototype !== null) {
105
+ throw new TypeError(`Non-plain object at ${frame.path}`);
106
+ }
107
+ const entries: Array<{ key: string; value: unknown }> = [];
108
+ for (const key of Reflect.ownKeys(current)) {
109
+ if (typeof key !== "string") throw new TypeError(`Symbol property at ${frame.path}`);
110
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
111
+ if (!descriptor?.enumerable || !("value" in descriptor)) {
112
+ throw new TypeError(`Non-enumerable or accessor property at ${frame.path}[${JSON.stringify(key)}]`);
113
+ }
114
+ entries.push({ key, value: descriptor.value });
115
+ }
116
+
117
+ const copy = Object.create(null) as { [key: string]: JsonValue };
118
+ frame.assign(copy);
119
+ active.add(current);
120
+ stack.push({ kind: "leave", value: current });
121
+ for (let index = entries.length - 1; index >= 0; index--) {
122
+ const entry = entries[index]!;
123
+ stack.push({
124
+ kind: "visit",
125
+ value: entry.value,
126
+ path: `${frame.path}[${JSON.stringify(entry.key)}]`,
127
+ assign: (child) => (copy[entry.key] = child),
128
+ });
129
+ }
130
+ }
131
+
132
+ if (root === undefined) throw new TypeError("JSON snapshot was not produced");
133
+ return root;
134
+ }
135
+
136
+ /** Serializes a detached JSON snapshot; inherited hooks and source mutation cannot rewrite it. */
137
+ export function encodeJson(value: unknown): Uint8Array {
138
+ const snapshot = snapshotJsonValue(value);
139
+ const encoded = JSON.stringify(snapshot);
140
+ if (encoded === undefined) throw new TypeError("Value could not be serialized as JSON");
141
+ return new TextEncoder().encode(encoded);
142
+ }
@@ -0,0 +1,12 @@
1
+ import type { JsonObject } from "./json.ts";
2
+
3
+ /** OpenCode transfer object retained as JSON without narrowing message variants. */
4
+ export interface SessionRecord {
5
+ readonly info: JsonObject;
6
+ readonly messages: readonly JsonObject[];
7
+ }
8
+
9
+ /** Payload shared between session collection, serialization, and restore preflight. */
10
+ export interface CheckpointPayload {
11
+ readonly sessions: readonly SessionRecord[];
12
+ }