@schnsrw/casual-sheets 0.2.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/LICENSE +200 -0
- package/dist/embed.cjs +211 -0
- package/dist/embed.cjs.map +1 -0
- package/dist/embed.d.cts +193 -0
- package/dist/embed.d.ts +193 -0
- package/dist/embed.js +183 -0
- package/dist/embed.js.map +1 -0
- package/dist/index.cjs +899 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +864 -0
- package/dist/index.js.map +1 -0
- package/dist/signing.cjs +716 -0
- package/dist/signing.cjs.map +1 -0
- package/dist/signing.d.cts +141 -0
- package/dist/signing.d.ts +141 -0
- package/dist/signing.js +683 -0
- package/dist/signing.js.map +1 -0
- package/dist/types-s_O0u6Cg.d.cts +90 -0
- package/dist/types-s_O0u6Cg.d.ts +90 -0
- package/package.json +77 -0
- package/src/embed/EmbedTransport.ts +295 -0
- package/src/embed/EmbedTransport.unit.test.ts +161 -0
- package/src/embed/index.ts +40 -0
- package/src/embed/protocol.ts +161 -0
- package/src/index.ts +13 -0
- package/src/signing/SigningPane.tsx +374 -0
- package/src/signing/SigningProvider.tsx +126 -0
- package/src/signing/captures.tsx +316 -0
- package/src/signing/controller.ts +151 -0
- package/src/signing/controller.unit.test.ts +133 -0
- package/src/signing/index.ts +44 -0
- package/src/signing/types.ts +89 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmbedTransport contract tests. Mirrors the document/ repo's
|
|
3
|
+
* EmbedTransport.test.ts — wire shape is uniform across docs and
|
|
4
|
+
* sheet, only the `app` discriminator differs. When you change
|
|
5
|
+
* EmbedTransport.ts here, change it there too.
|
|
6
|
+
*/
|
|
7
|
+
import { strict as assert } from 'node:assert';
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
|
|
10
|
+
import { EmbedTransport, isCasualEnvelope } from './';
|
|
11
|
+
import type { CasualEnvelope } from './';
|
|
12
|
+
|
|
13
|
+
function fakeHostWindow() {
|
|
14
|
+
let handler: ((ev: unknown) => void) | null = null;
|
|
15
|
+
return {
|
|
16
|
+
addEventListener(_type: string, h: EventListenerOrEventListenerObject) {
|
|
17
|
+
handler = h as (ev: unknown) => void;
|
|
18
|
+
},
|
|
19
|
+
removeEventListener() {
|
|
20
|
+
handler = null;
|
|
21
|
+
},
|
|
22
|
+
fire(ev: unknown) {
|
|
23
|
+
if (handler) handler(ev);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fakeParent() {
|
|
29
|
+
const sent: Array<{ msg: unknown; origin: string; transfer?: Transferable[] }> = [];
|
|
30
|
+
return {
|
|
31
|
+
sent,
|
|
32
|
+
postMessage(msg: unknown, origin: string, transfer?: Transferable[]) {
|
|
33
|
+
sent.push({ msg, origin, transfer });
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test('isCasualEnvelope accepts a well-formed sheet envelope', () => {
|
|
39
|
+
assert.equal(
|
|
40
|
+
isCasualEnvelope({
|
|
41
|
+
type: 'casual.hello',
|
|
42
|
+
app: 'sheet',
|
|
43
|
+
v: 1,
|
|
44
|
+
data: {},
|
|
45
|
+
} satisfies CasualEnvelope),
|
|
46
|
+
true,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('isCasualEnvelope rejects foreign types', () => {
|
|
51
|
+
assert.equal(isCasualEnvelope({ type: 'other.hello', app: 'sheet', v: 1, data: {} }), false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('isCasualEnvelope rejects bad app values', () => {
|
|
55
|
+
assert.equal(isCasualEnvelope({ type: 'casual.hello', app: 'pdf', v: 1, data: {} }), false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('EmbedTransport drops messages from disallowed origins', async () => {
|
|
59
|
+
const host = fakeHostWindow();
|
|
60
|
+
const parent = fakeParent();
|
|
61
|
+
let called = false;
|
|
62
|
+
const transport = new EmbedTransport({
|
|
63
|
+
app: 'sheet',
|
|
64
|
+
hostOrigin: 'https://drive.example',
|
|
65
|
+
version: '1.0.0',
|
|
66
|
+
commit: 'abc',
|
|
67
|
+
capabilities: [],
|
|
68
|
+
parentWindow: parent,
|
|
69
|
+
hostWindow: host,
|
|
70
|
+
});
|
|
71
|
+
transport.on({
|
|
72
|
+
onHostHello: () => {
|
|
73
|
+
called = true;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
host.fire({
|
|
77
|
+
origin: 'https://evil.example',
|
|
78
|
+
data: { type: 'casual.hello', app: 'sheet', v: 1, data: { capabilities: [] } },
|
|
79
|
+
});
|
|
80
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
81
|
+
assert.equal(called, false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('EmbedTransport routes valid host.hello + replies with editor.ready', async () => {
|
|
85
|
+
const host = fakeHostWindow();
|
|
86
|
+
const parent = fakeParent();
|
|
87
|
+
let received: unknown = null;
|
|
88
|
+
const transport = new EmbedTransport({
|
|
89
|
+
app: 'sheet',
|
|
90
|
+
hostOrigin: 'https://drive.example',
|
|
91
|
+
version: '1.0.0',
|
|
92
|
+
commit: 'abc',
|
|
93
|
+
capabilities: [],
|
|
94
|
+
parentWindow: parent,
|
|
95
|
+
hostWindow: host,
|
|
96
|
+
});
|
|
97
|
+
transport.on({
|
|
98
|
+
onHostHello: (data) => {
|
|
99
|
+
received = data;
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
host.fire({
|
|
103
|
+
origin: 'https://drive.example',
|
|
104
|
+
data: {
|
|
105
|
+
type: 'casual.hello',
|
|
106
|
+
app: 'sheet',
|
|
107
|
+
v: 1,
|
|
108
|
+
data: { capabilities: ['saveDocument'] },
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
112
|
+
assert.deepEqual(received, { capabilities: ['saveDocument'] });
|
|
113
|
+
const lastSent = parent.sent[parent.sent.length - 1].msg as CasualEnvelope;
|
|
114
|
+
assert.equal(lastSent.type, 'casual.ready');
|
|
115
|
+
assert.equal(lastSent.app, 'sheet');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('sendHello emits an editor.hello envelope', () => {
|
|
119
|
+
const host = fakeHostWindow();
|
|
120
|
+
const parent = fakeParent();
|
|
121
|
+
const transport = new EmbedTransport({
|
|
122
|
+
app: 'sheet',
|
|
123
|
+
hostOrigin: 'https://drive.example',
|
|
124
|
+
version: '1.2.3',
|
|
125
|
+
commit: 'deadbee',
|
|
126
|
+
capabilities: ['save', 'load'],
|
|
127
|
+
parentWindow: parent,
|
|
128
|
+
hostWindow: host,
|
|
129
|
+
});
|
|
130
|
+
transport.sendHello();
|
|
131
|
+
assert.equal(parent.sent.length, 1);
|
|
132
|
+
const env = parent.sent[0].msg as CasualEnvelope;
|
|
133
|
+
assert.equal(env.type, 'casual.hello');
|
|
134
|
+
assert.equal((env.data as { version: string }).version, '1.2.3');
|
|
135
|
+
assert.deepEqual((env.data as { capabilities: string[] }).capabilities, ['save', 'load']);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('destroy detaches the listener', () => {
|
|
139
|
+
const host = fakeHostWindow();
|
|
140
|
+
const transport = new EmbedTransport({
|
|
141
|
+
app: 'sheet',
|
|
142
|
+
hostOrigin: 'https://drive.example',
|
|
143
|
+
version: '1.0.0',
|
|
144
|
+
commit: 'abc',
|
|
145
|
+
capabilities: [],
|
|
146
|
+
parentWindow: fakeParent(),
|
|
147
|
+
hostWindow: host,
|
|
148
|
+
});
|
|
149
|
+
let called = false;
|
|
150
|
+
transport.on({
|
|
151
|
+
onHostHello: () => {
|
|
152
|
+
called = true;
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
transport.destroy();
|
|
156
|
+
host.fire({
|
|
157
|
+
origin: 'https://drive.example',
|
|
158
|
+
data: { type: 'casual.hello', app: 'sheet', v: 1, data: { capabilities: [] } },
|
|
159
|
+
});
|
|
160
|
+
assert.equal(called, false);
|
|
161
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embed — iframe delivery surface.
|
|
3
|
+
*
|
|
4
|
+
* EmbedTransport bridges postMessage to React handlers. The
|
|
5
|
+
* envelope shapes (`./protocol.ts`) mirror the documentation
|
|
6
|
+
* contract in `docs/internal/13-iframe-protocol.md` and the
|
|
7
|
+
* signing payloads from `../signing/types.ts`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
EmbedTransport,
|
|
12
|
+
type EmbedTransportHandlers,
|
|
13
|
+
type EmbedTransportOptions,
|
|
14
|
+
} from './EmbedTransport';
|
|
15
|
+
export {
|
|
16
|
+
isCasualEnvelope,
|
|
17
|
+
type CasualApp,
|
|
18
|
+
type CasualEnvelope,
|
|
19
|
+
type EditorHelloData,
|
|
20
|
+
type HostHelloData,
|
|
21
|
+
type LoadRequestData,
|
|
22
|
+
type LoadResponseData,
|
|
23
|
+
type LoadResponseDataOk,
|
|
24
|
+
type LoadResponseDataErr,
|
|
25
|
+
type SaveRequestData,
|
|
26
|
+
type SaveResponseData,
|
|
27
|
+
type SaveResponseDataOk,
|
|
28
|
+
type SaveResponseDataErr,
|
|
29
|
+
type SelectionChangedData,
|
|
30
|
+
type TelemetryEventData,
|
|
31
|
+
type LockLostData,
|
|
32
|
+
type CommandSetReadOnlyData,
|
|
33
|
+
type CommandSetThemeData,
|
|
34
|
+
type CommandSetLocaleData,
|
|
35
|
+
type SignatureRequestData,
|
|
36
|
+
type SignatureRequestAckData,
|
|
37
|
+
type SignatureFieldSignedData,
|
|
38
|
+
type SignatureCompleteData,
|
|
39
|
+
type SignatureCancelData,
|
|
40
|
+
} from './protocol';
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Iframe protocol — wire envelopes that match
|
|
3
|
+
* `docs/internal/13-iframe-protocol.md`.
|
|
4
|
+
*
|
|
5
|
+
* Mirror updates to the doc whenever a new envelope shape lands.
|
|
6
|
+
* The discriminator on `type` (always starts with `casual.`) and
|
|
7
|
+
* the per-envelope `data` shape are the contract.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
CancelReason,
|
|
12
|
+
SignatureCompletePayload,
|
|
13
|
+
SignatureField,
|
|
14
|
+
SignatureMode,
|
|
15
|
+
SignedFieldPayload,
|
|
16
|
+
} from '../signing/types';
|
|
17
|
+
|
|
18
|
+
export type CasualApp = 'docs' | 'sheet';
|
|
19
|
+
|
|
20
|
+
/** Common envelope shape — every postMessage on the wire matches this. */
|
|
21
|
+
export interface CasualEnvelope<T = unknown> {
|
|
22
|
+
type: string;
|
|
23
|
+
app: CasualApp;
|
|
24
|
+
/** Per-request id for request/response correlation. Empty for fire-and-forget. */
|
|
25
|
+
id?: string;
|
|
26
|
+
/** Protocol version. Bumped only on breaking changes. */
|
|
27
|
+
v: 1;
|
|
28
|
+
data: T;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------
|
|
32
|
+
// Handshake
|
|
33
|
+
// ---------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export interface EditorHelloData {
|
|
36
|
+
capabilities: string[];
|
|
37
|
+
version: string;
|
|
38
|
+
commit: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface HostHelloData {
|
|
42
|
+
capabilities: string[];
|
|
43
|
+
authToken?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------
|
|
47
|
+
// Load + save (editor → host requests; host → editor responses)
|
|
48
|
+
// ---------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
export interface LoadRequestData {
|
|
51
|
+
docId: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface LoadResponseDataOk {
|
|
55
|
+
ok: true;
|
|
56
|
+
bytes: ArrayBuffer;
|
|
57
|
+
etag?: string;
|
|
58
|
+
fileName: string;
|
|
59
|
+
readOnly?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface LoadResponseDataErr {
|
|
63
|
+
ok: false;
|
|
64
|
+
code: string;
|
|
65
|
+
message?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type LoadResponseData = LoadResponseDataOk | LoadResponseDataErr;
|
|
69
|
+
|
|
70
|
+
export interface SaveRequestData {
|
|
71
|
+
docId: string;
|
|
72
|
+
bytes: ArrayBuffer;
|
|
73
|
+
baseEtag?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SaveResponseDataOk {
|
|
77
|
+
ok: true;
|
|
78
|
+
etag: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SaveResponseDataErr {
|
|
82
|
+
ok: false;
|
|
83
|
+
code: string;
|
|
84
|
+
etag?: string;
|
|
85
|
+
message?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type SaveResponseData = SaveResponseDataOk | SaveResponseDataErr;
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------
|
|
91
|
+
// Selection + telemetry + lock (editor → host notifications)
|
|
92
|
+
// ---------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
export interface SelectionChangedData {
|
|
95
|
+
docs?: { paraId: string; from: number; to: number; selectedText: string };
|
|
96
|
+
sheet?: { sheet: string; from: string; to: string };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface TelemetryEventData {
|
|
100
|
+
kind: string;
|
|
101
|
+
/** Arbitrary metric fields. */
|
|
102
|
+
[k: string]: unknown;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface LockLostData {
|
|
106
|
+
reason: 'taken_by_other' | 'expired' | 'host_revoked';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------
|
|
110
|
+
// Commands (host → editor)
|
|
111
|
+
// ---------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
export interface CommandSetReadOnlyData {
|
|
114
|
+
readOnly: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface CommandSetThemeData {
|
|
118
|
+
theme: 'light' | 'dark' | 'system';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface CommandSetLocaleData {
|
|
122
|
+
locale: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------
|
|
126
|
+
// Signing (uniform with the SDK `signing` prop)
|
|
127
|
+
// ---------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
export interface SignatureRequestData {
|
|
130
|
+
fields: SignatureField[];
|
|
131
|
+
mode: SignatureMode;
|
|
132
|
+
banner?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface SignatureRequestAckData {
|
|
136
|
+
ok: boolean;
|
|
137
|
+
code?: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type SignatureFieldSignedData = SignedFieldPayload;
|
|
141
|
+
export type SignatureCompleteData = SignatureCompletePayload;
|
|
142
|
+
|
|
143
|
+
export interface SignatureCancelData {
|
|
144
|
+
reason: CancelReason;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------
|
|
148
|
+
// Type guards
|
|
149
|
+
// ---------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
export function isCasualEnvelope(value: unknown): value is CasualEnvelope {
|
|
152
|
+
if (!value || typeof value !== 'object') return false;
|
|
153
|
+
const v = value as Record<string, unknown>;
|
|
154
|
+
return (
|
|
155
|
+
typeof v.type === 'string' &&
|
|
156
|
+
v.type.startsWith('casual.') &&
|
|
157
|
+
(v.app === 'docs' || v.app === 'sheet') &&
|
|
158
|
+
v.v === 1 &&
|
|
159
|
+
'data' in v
|
|
160
|
+
);
|
|
161
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @schnsrw/casual-sheets — Casual Sheets SDK
|
|
3
|
+
*
|
|
4
|
+
* Two surfaces shipped in v0.1.0:
|
|
5
|
+
* - `./signing` — anchored cell signatures (drawn / typed / uploaded)
|
|
6
|
+
* - `./embed` — iframe postMessage protocol for host integrations
|
|
7
|
+
*
|
|
8
|
+
* A `CasualSheets` React component that wraps the Univer-Sheets bootstrap
|
|
9
|
+
* is planned for a follow-up release.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export * from './signing';
|
|
13
|
+
export * from './embed';
|