@gmickel/gno 1.0.0 → 1.0.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.
package/README.md
CHANGED
|
@@ -653,6 +653,11 @@ Upload the artifact at [gno.sh/studio](https://gno.sh/studio) and pick a visibil
|
|
|
653
653
|
|
|
654
654
|
Republishing a public, secret-link, or invite-only artifact updates the same URL. Encrypted shares should be replaced from a fresh local export so the server never needs your plaintext.
|
|
655
655
|
|
|
656
|
+
Encrypted source-backed publish on `gno.sh` is intentionally disabled. For encrypted shares, use:
|
|
657
|
+
|
|
658
|
+
- `gno publish export --visibility encrypted --passphrase ...`, or
|
|
659
|
+
- the browser-side encrypted markdown upload path in `gno.sh/studio`
|
|
660
|
+
|
|
656
661
|
> **Full story**: [gno.sh/publish](https://gno.sh/publish) · **Try it**: [gno.sh/studio](https://gno.sh/studio)
|
|
657
662
|
|
|
658
663
|
---
|
package/package.json
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
import {
|
|
16
16
|
adapterError,
|
|
17
17
|
corruptError,
|
|
18
|
+
permissionError,
|
|
18
19
|
timeoutError,
|
|
19
20
|
tooLargeError,
|
|
20
21
|
} from "../../errors";
|
|
@@ -33,6 +34,15 @@ const SUPPORTED_MIMES = [
|
|
|
33
34
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
34
35
|
];
|
|
35
36
|
|
|
37
|
+
const PDF_SIGNATURE = new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d]);
|
|
38
|
+
const CFB_SIGNATURE = new Uint8Array([
|
|
39
|
+
0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1,
|
|
40
|
+
]);
|
|
41
|
+
const ENCRYPTION_INFO = utf16le("EncryptionInfo");
|
|
42
|
+
const ENCRYPTED_PACKAGE = utf16le("EncryptedPackage");
|
|
43
|
+
const MAX_MESSAGE_LENGTH = 200;
|
|
44
|
+
const PASSWORD_ERROR_REGEX = /password(?:-protected)?|no password given/i;
|
|
45
|
+
|
|
36
46
|
/**
|
|
37
47
|
* Create zero-copy Buffer view of Uint8Array.
|
|
38
48
|
* Assumes input.bytes is immutable (contract requirement).
|
|
@@ -41,6 +51,103 @@ function toBuffer(bytes: Uint8Array): Buffer {
|
|
|
41
51
|
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
42
52
|
}
|
|
43
53
|
|
|
54
|
+
function utf16le(value: string): Uint8Array {
|
|
55
|
+
return new Uint8Array(Buffer.from(value, "utf16le"));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hasPrefix(bytes: Uint8Array, prefix: Uint8Array): boolean {
|
|
59
|
+
if (bytes.length < prefix.length) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (let index = 0; index < prefix.length; index += 1) {
|
|
64
|
+
if (bytes[index] !== prefix[index]) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function includesBytes(bytes: Uint8Array, needle: Uint8Array): boolean {
|
|
73
|
+
if (needle.length === 0 || bytes.length < needle.length) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
outer: for (
|
|
78
|
+
let index = 0;
|
|
79
|
+
index <= bytes.length - needle.length;
|
|
80
|
+
index += 1
|
|
81
|
+
) {
|
|
82
|
+
for (let offset = 0; offset < needle.length; offset += 1) {
|
|
83
|
+
if (bytes[index + offset] !== needle[offset]) {
|
|
84
|
+
continue outer;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isPasswordProtectedPdf(bytes: Uint8Array): boolean {
|
|
94
|
+
if (!hasPrefix(bytes, PDF_SIGNATURE)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const tailStart = Math.max(0, bytes.length - 64 * 1024);
|
|
99
|
+
const tail = Buffer.from(bytes.subarray(tailStart)).toString("latin1");
|
|
100
|
+
return /\/Encrypt\b/.test(tail);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isPasswordProtectedXlsx(bytes: Uint8Array): boolean {
|
|
104
|
+
return (
|
|
105
|
+
hasPrefix(bytes, CFB_SIGNATURE) &&
|
|
106
|
+
includesBytes(bytes, ENCRYPTION_INFO) &&
|
|
107
|
+
includesBytes(bytes, ENCRYPTED_PACKAGE)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isPasswordProtected(input: ConvertInput): boolean {
|
|
112
|
+
if (input.ext === ".pdf") {
|
|
113
|
+
return isPasswordProtectedPdf(input.bytes);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (input.ext === ".xlsx") {
|
|
117
|
+
return isPasswordProtectedXlsx(input.bytes);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sanitizeErrorMessage(message: string, input: ConvertInput): string {
|
|
124
|
+
const normalized = message.trim();
|
|
125
|
+
|
|
126
|
+
let hasControlChars = false;
|
|
127
|
+
for (const char of normalized) {
|
|
128
|
+
const code = char.charCodeAt(0);
|
|
129
|
+
if (
|
|
130
|
+
(code >= 0 && code <= 8) ||
|
|
131
|
+
code === 11 ||
|
|
132
|
+
code === 12 ||
|
|
133
|
+
(code >= 14 && code <= 31)
|
|
134
|
+
) {
|
|
135
|
+
hasControlChars = true;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
normalized.length === 0 ||
|
|
142
|
+
normalized.length > MAX_MESSAGE_LENGTH ||
|
|
143
|
+
hasControlChars
|
|
144
|
+
) {
|
|
145
|
+
return `Could not convert ${input.ext} file to markdown`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return normalized;
|
|
149
|
+
}
|
|
150
|
+
|
|
44
151
|
export const markitdownAdapter: Converter = {
|
|
45
152
|
id: CONVERTER_ID,
|
|
46
153
|
version: CONVERTER_VERSION,
|
|
@@ -55,6 +162,21 @@ export const markitdownAdapter: Converter = {
|
|
|
55
162
|
return { ok: false, error: tooLargeError(input, CONVERTER_ID) };
|
|
56
163
|
}
|
|
57
164
|
|
|
165
|
+
// 1b. Detect password-protected documents before calling markitdown-ts.
|
|
166
|
+
// markitdown-ts logs dependency stack traces to stderr for these files.
|
|
167
|
+
if (isPasswordProtected(input)) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
error: permissionError(
|
|
171
|
+
input,
|
|
172
|
+
CONVERTER_ID,
|
|
173
|
+
"File is password-protected",
|
|
174
|
+
undefined,
|
|
175
|
+
{ protection: input.ext.slice(1) }
|
|
176
|
+
),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
58
180
|
// 2. Setup timeout handling
|
|
59
181
|
// Note: markitdown-ts doesn't support AbortSignal, so underlying
|
|
60
182
|
// work may continue after timeout (known limitation; process isolation future work)
|
|
@@ -128,14 +250,27 @@ export const markitdownAdapter: Converter = {
|
|
|
128
250
|
}
|
|
129
251
|
|
|
130
252
|
// Map adapter errors
|
|
253
|
+
const message =
|
|
254
|
+
err instanceof Error
|
|
255
|
+
? sanitizeErrorMessage(err.message, input)
|
|
256
|
+
: `Could not convert ${input.ext} file to markdown`;
|
|
257
|
+
|
|
258
|
+
if (PASSWORD_ERROR_REGEX.test(message)) {
|
|
259
|
+
return {
|
|
260
|
+
ok: false,
|
|
261
|
+
error: permissionError(
|
|
262
|
+
input,
|
|
263
|
+
CONVERTER_ID,
|
|
264
|
+
"File is password-protected",
|
|
265
|
+
err,
|
|
266
|
+
{ protection: input.ext.slice(1) }
|
|
267
|
+
),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
131
271
|
return {
|
|
132
272
|
ok: false,
|
|
133
|
-
error: adapterError(
|
|
134
|
-
input,
|
|
135
|
-
CONVERTER_ID,
|
|
136
|
-
err instanceof Error ? err.message : "Unknown error",
|
|
137
|
-
err
|
|
138
|
-
),
|
|
273
|
+
error: adapterError(input, CONVERTER_ID, message, err),
|
|
139
274
|
};
|
|
140
275
|
}
|
|
141
276
|
},
|
package/src/converters/errors.ts
CHANGED
|
@@ -179,6 +179,29 @@ export function corruptError(
|
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Create an error for permission-gated files (for example password-protected documents).
|
|
184
|
+
*/
|
|
185
|
+
export function permissionError(
|
|
186
|
+
input: Pick<ConvertInput, "sourcePath" | "mime" | "ext">,
|
|
187
|
+
converterId: string,
|
|
188
|
+
message: string,
|
|
189
|
+
cause?: unknown,
|
|
190
|
+
details?: Record<string, unknown>
|
|
191
|
+
): ConvertError {
|
|
192
|
+
return convertError("PERMISSION", {
|
|
193
|
+
message,
|
|
194
|
+
retryable: false,
|
|
195
|
+
fatal: false,
|
|
196
|
+
converterId,
|
|
197
|
+
sourcePath: input.sourcePath,
|
|
198
|
+
mime: input.mime,
|
|
199
|
+
ext: input.ext,
|
|
200
|
+
cause,
|
|
201
|
+
details,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
182
205
|
/**
|
|
183
206
|
* Create an error for adapter-level failures.
|
|
184
207
|
*/
|
|
@@ -2,8 +2,6 @@ import { randomBytes, webcrypto } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
import type { EncryptedArtifactPayload, PublishArtifactNote } from "./artifact";
|
|
4
4
|
|
|
5
|
-
import { slugify } from "./artifact";
|
|
6
|
-
|
|
7
5
|
const { subtle } = webcrypto;
|
|
8
6
|
|
|
9
7
|
const PBKDF2_ITERATIONS = 210_000;
|
|
@@ -19,6 +17,7 @@ type MetadataEntry = {
|
|
|
19
17
|
|
|
20
18
|
type NoteBlock =
|
|
21
19
|
| { type: "paragraph"; text: string }
|
|
20
|
+
| { type: "markdown"; markdown: string }
|
|
22
21
|
| { type: "heading"; depth: 2 | 3; id: string; text: string }
|
|
23
22
|
| { type: "list"; items: string[]; style: "ordered" | "unordered" }
|
|
24
23
|
| { code: string; language: string; type: "code" }
|
|
@@ -108,120 +107,12 @@ const filterMetadata = (
|
|
|
108
107
|
}));
|
|
109
108
|
};
|
|
110
109
|
|
|
111
|
-
const parseMarkdownBlocks = (markdown: string): NoteBlock[] =>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const flushParagraph = () => {
|
|
119
|
-
if (paragraph.length === 0) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
blocks.push({
|
|
124
|
-
type: "paragraph",
|
|
125
|
-
text: paragraph.join(" ").trim(),
|
|
126
|
-
});
|
|
127
|
-
paragraph = [];
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const flushList = () => {
|
|
131
|
-
if (listItems.length === 0) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
blocks.push({
|
|
136
|
-
type: "list",
|
|
137
|
-
style: "unordered",
|
|
138
|
-
items: listItems,
|
|
139
|
-
});
|
|
140
|
-
listItems = [];
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
for (const line of lines) {
|
|
144
|
-
if (codeFence) {
|
|
145
|
-
if (line.startsWith("```")) {
|
|
146
|
-
blocks.push({
|
|
147
|
-
type: "code",
|
|
148
|
-
language: codeFence.language || "text",
|
|
149
|
-
code: codeFence.code.join("\n"),
|
|
150
|
-
});
|
|
151
|
-
codeFence = null;
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
codeFence.code.push(line);
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const trimmed = line.trim();
|
|
160
|
-
if (!trimmed) {
|
|
161
|
-
flushParagraph();
|
|
162
|
-
flushList();
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const codeMatch = trimmed.match(/^```([\w-]*)$/);
|
|
167
|
-
if (codeMatch) {
|
|
168
|
-
flushParagraph();
|
|
169
|
-
flushList();
|
|
170
|
-
codeFence = { code: [], language: codeMatch[1] || "text" };
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const headingMatch = trimmed.match(/^(#{1,3})\s+(.*)$/);
|
|
175
|
-
if (headingMatch) {
|
|
176
|
-
flushParagraph();
|
|
177
|
-
flushList();
|
|
178
|
-
const headingText = headingMatch[2] ?? "";
|
|
179
|
-
blocks.push({
|
|
180
|
-
type: "heading",
|
|
181
|
-
depth: headingMatch[1] === "###" ? 3 : 2,
|
|
182
|
-
id: slugify(headingText),
|
|
183
|
-
text: headingText,
|
|
184
|
-
});
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const imageMatch = trimmed.match(/^!\[(.*?)\]\((.*?)\)$/);
|
|
189
|
-
if (imageMatch) {
|
|
190
|
-
flushParagraph();
|
|
191
|
-
flushList();
|
|
192
|
-
const alt = imageMatch[1] ?? "";
|
|
193
|
-
const src = imageMatch[2] ?? "";
|
|
194
|
-
blocks.push({
|
|
195
|
-
type: "image",
|
|
196
|
-
alt,
|
|
197
|
-
src,
|
|
198
|
-
caption: alt,
|
|
199
|
-
});
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const listMatch = trimmed.match(/^[-*]\s+(.*)$/);
|
|
204
|
-
if (listMatch) {
|
|
205
|
-
flushParagraph();
|
|
206
|
-
listItems.push(listMatch[1] ?? "");
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
paragraph.push(trimmed);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
flushParagraph();
|
|
214
|
-
flushList();
|
|
215
|
-
|
|
216
|
-
if (blocks.length === 0) {
|
|
217
|
-
blocks.push({
|
|
218
|
-
type: "paragraph",
|
|
219
|
-
text: markdown.trim(),
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return blocks;
|
|
224
|
-
};
|
|
110
|
+
const parseMarkdownBlocks = (markdown: string): NoteBlock[] => [
|
|
111
|
+
{
|
|
112
|
+
type: "markdown",
|
|
113
|
+
markdown: stripFrontmatter(markdown).trim(),
|
|
114
|
+
},
|
|
115
|
+
];
|
|
225
116
|
|
|
226
117
|
const getOutline = (blocks: NoteBlock[]) =>
|
|
227
118
|
blocks.flatMap((block) =>
|