@filefreak.io/sdk 0.3.0 → 0.3.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/dist/client.d.ts CHANGED
@@ -53,6 +53,11 @@ export declare class FileFreak {
53
53
  };
54
54
  readonly uploads: Api["uploads"];
55
55
  readonly usage: Api["usage"];
56
+ readonly me: Api["me"];
57
+ /** Share links, which only a device signed in with filefreak login may make, list or remove. */
58
+ readonly shares: Api["shares"];
59
+ /** Static sites, which only a device signed in with filefreak login may deploy. */
60
+ readonly sites: Api["sites"];
56
61
  readonly tokens: Api["tokens"];
57
62
  constructor(options: ClientOptions);
58
63
  /** Any endpoint, by its operation name. The functions above are this, typed. */
package/dist/client.js CHANGED
@@ -48,6 +48,11 @@ export class FileFreak {
48
48
  changes;
49
49
  uploads;
50
50
  usage;
51
+ me;
52
+ /** Share links, which only a device signed in with filefreak login may make, list or remove. */
53
+ shares;
54
+ /** Static sites, which only a device signed in with filefreak login may deploy. */
55
+ sites;
51
56
  tokens;
52
57
  constructor(options) {
53
58
  this.transport = new Transport(options);
@@ -58,6 +63,9 @@ export class FileFreak {
58
63
  this.changes = { ...api.changes, follow: (cursor, followOptions = {}) => follow(api.changes.list, cursor ?? null, followOptions) };
59
64
  this.uploads = api.uploads;
60
65
  this.usage = api.usage;
66
+ this.me = api.me;
67
+ this.shares = api.shares;
68
+ this.sites = api.sites;
61
69
  this.tokens = api.tokens;
62
70
  }
63
71
  /** Any endpoint, by its operation name. The functions above are this, typed. */
@@ -436,8 +436,8 @@ export type UsageGetAnswer = {
436
436
  plan: string;
437
437
  storageUsedBytes: number;
438
438
  storageIncludedBytes: number;
439
- /** Where uploads are refused: what the plan includes, plus what the account's spending limit allows beyond it. */
440
- storageLimitBytes: number;
439
+ /** Where uploads are refused: what the plan includes, plus what the account's spending limit allows beyond it. Null when nothing is: Pro that FileFreak has given the account has no storage limit. */
440
+ storageLimitBytes: number | null;
441
441
  downloadedBytesThisMonth: number;
442
442
  downloadsIncludedBytes: number;
443
443
  apiCallsThisMonth: number;
package/dist/index.d.ts CHANGED
@@ -2,6 +2,6 @@ export { FileFreak, Walk, type DownloadOptions, type FollowOptions } from "./cli
2
2
  export * from "./generated.js";
3
3
  export { get, type GetOptions, type Got, type Into, type LinkedFile } from "./links.js";
4
4
  export { type Sink } from "./ranges.js";
5
- export { openFragment, sealFragment, SHARD_OVERHEAD, SHARD_VERSION, ShardError, shardKey, ShardLayout, type FragmentAt, type ShardErrorCode, type ShardKey, } from "./shards.js";
5
+ export { checkOutline, fileKeys, MOST_BLOCK_BYTES, openBlock, openFragment, outlineMac, sealBlock, sealFragment, SHARD_OVERHEAD, SHARD_VERSION, SHARD_VERSION_2, ShardError, shardKey, ShardLayout, type FileKeys, type FragmentAt, type Outline, type OutlineBlock, type ShardErrorCode, type ShardKey, } from "./shards.js";
6
6
  export { FileFreakError, type CallOptions, type ClientOptions, type TokenSource } from "./transport.js";
7
7
  export { UploadStopped, type Source, type UploadOptions } from "./uploads.js";
package/dist/index.js CHANGED
@@ -12,6 +12,6 @@
12
12
  export { FileFreak, Walk } from "./client.js";
13
13
  export * from "./generated.js";
14
14
  export { get } from "./links.js";
15
- export { openFragment, sealFragment, SHARD_OVERHEAD, SHARD_VERSION, ShardError, shardKey, ShardLayout, } from "./shards.js";
15
+ export { checkOutline, fileKeys, MOST_BLOCK_BYTES, openBlock, openFragment, outlineMac, sealBlock, sealFragment, SHARD_OVERHEAD, SHARD_VERSION, SHARD_VERSION_2, ShardError, shardKey, ShardLayout, } from "./shards.js";
16
16
  export { FileFreakError } from "./transport.js";
17
17
  export { UploadStopped } from "./uploads.js";
package/dist/shards.d.ts CHANGED
@@ -4,9 +4,10 @@ export declare const SHARD_OVERHEAD = 29;
4
4
  /**
5
5
  * Why a fragment or a description was refused. A reader checks in this
6
6
  * order, and says the first that fails: short, version, length,
7
- * authentication. layout is a description of a file no fragment can match.
7
+ * authentication, and in version 2 id. layout is a description of a file no
8
+ * fragment can match; outline is a version-2 outline whose MAC does not hold.
8
9
  */
9
- export type ShardErrorCode = "short" | "version" | "length" | "authentication" | "layout";
10
+ export type ShardErrorCode = "short" | "version" | "length" | "authentication" | "id" | "layout" | "outline";
10
11
  export declare class ShardError extends Error {
11
12
  readonly code: ShardErrorCode;
12
13
  constructor(code: ShardErrorCode, message: string);
@@ -57,3 +58,61 @@ export declare function sealWithNonce(key: ShardKey, at: FragmentAt, plaintext:
57
58
  * returned: WebCrypto gives no plaintext until the tag has been checked.
58
59
  */
59
60
  export declare function openFragment(key: ShardKey, at: FragmentAt, blob: Uint8Array): Promise<Uint8Array<ArrayBuffer>>;
61
+ export declare const SHARD_VERSION_2 = 2;
62
+ /** The most bytes of a file one block holds, as the API takes blocks. */
63
+ export declare const MOST_BLOCK_BYTES: number;
64
+ /** A block id: the HMAC-SHA256 of its bytes under the file's block key. */
65
+ export declare const BLOCK_ID_BYTES = 32;
66
+ /**
67
+ * A file's key in version 2, as the three keys derived from it: never the
68
+ * key itself, so that sealing, naming blocks and vouching for the outline
69
+ * cannot be played against each other.
70
+ */
71
+ export interface FileKeys {
72
+ seal: ShardKey;
73
+ block: ShardKey;
74
+ outline: ShardKey;
75
+ }
76
+ /** One block as an outline gives it: its id and how many bytes of the file it holds. */
77
+ export interface OutlineBlock {
78
+ id: Uint8Array;
79
+ length: number;
80
+ }
81
+ /** What a content in version 2 is, as its outline gives it, before any block is fetched. */
82
+ export interface Outline {
83
+ contentId: string;
84
+ size: number;
85
+ blocks: OutlineBlock[];
86
+ }
87
+ /** The three keys of a file's key: HKDF-SHA256 with no salt, each named by its use. */
88
+ export declare function fileKeys(raw: Uint8Array): Promise<FileKeys>;
89
+ /** A block's id: what the map holds in place of a hash anybody could make of the same bytes. */
90
+ export declare function blockId(keys: FileKeys, plaintext: Uint8Array): Promise<Uint8Array<ArrayBuffer>>;
91
+ /** The bytes a block's tag also covers: the label and its id, and nothing about where it is, so that a later version can keep it as it is. */
92
+ export declare function blockAssociatedData(id: Uint8Array): Uint8Array<ArrayBuffer>;
93
+ /** One block, encrypted and ready to store under a nonce of its own, with its id. */
94
+ export declare function sealBlock(keys: FileKeys, plaintext: Uint8Array): Promise<{
95
+ id: Uint8Array<ArrayBuffer>;
96
+ blob: Uint8Array<ArrayBuffer>;
97
+ }>;
98
+ /**
99
+ * sealBlock with the id and the nonce chosen by the caller, which only the
100
+ * test files may do, as with sealWithNonce. Not exported from the package.
101
+ */
102
+ export declare function sealBlockWithNonce(keys: FileKeys, id: Uint8Array, plaintext: Uint8Array, nonce: Uint8Array): Promise<Uint8Array<ArrayBuffer>>;
103
+ /**
104
+ * A stored block's bytes of the file, once it is known to be the block the
105
+ * outline names, unchanged: in this order, short, version, length,
106
+ * authentication (the tag, which binds it to its id), and id (that its
107
+ * bytes are the ones the id was made of). Nothing of a block that fails is
108
+ * returned.
109
+ */
110
+ export declare function openBlock(keys: FileKeys, block: OutlineBlock, blob: Uint8Array): Promise<Uint8Array<ArrayBuffer>>;
111
+ /** The outline's MAC, which the writer sends with the upload's completion and every reader checks before it releases a byte. */
112
+ export declare function outlineMac(keys: FileKeys, outline: Outline): Promise<Uint8Array<ArrayBuffer>>;
113
+ /**
114
+ * Refuse an outline unless its MAC holds under the file's key: whoever has
115
+ * the map and not the key cannot reorder, drop, repeat or swap a content's
116
+ * blocks, or give one content's blocks as another's.
117
+ */
118
+ export declare function checkOutline(keys: FileKeys, outline: Outline, mac: Uint8Array): Promise<void>;
package/dist/shards.js CHANGED
@@ -1,9 +1,13 @@
1
- // The shard format, version 1: how a file under Shard Protection is kept, as
2
- // fragments each encrypted on its own. docs/reference/shard-format.md is the
3
- // definition, and docs/reference/shard-format-v1.json holds the test files
4
- // this is checked against (test/shards.test.ts), as the CLI's copy is: a
5
- // writer here whose bytes differ by one from the CLI's makes files the CLI
6
- // cannot read, and the reverse.
1
+ // The shard format: how a file under Shard Protection is kept, as fragments
2
+ // each encrypted on its own. Version 1 cuts a content into fragments of one
3
+ // size, each sealed to its content's own key, id, place and count. Version 2
4
+ // cuts it into blocks by content, sealed under a key the file's versions
5
+ // share, so that a block a new version still has can be kept as it is.
6
+ // docs/reference/shard-format.md is the definition, and
7
+ // docs/reference/shard-format-v1.json and -v2.json hold the test files this
8
+ // is checked against (test/shards.test.ts), as the CLI's copy is: a writer
9
+ // here whose bytes differ by one from the CLI's makes files the CLI cannot
10
+ // read, and the reverse.
7
11
  //
8
12
  // WebCrypto, so the same code runs in a page, in a worker and in Node.
9
13
  export const SHARD_VERSION = 1;
@@ -128,6 +132,158 @@ export async function openFragment(key, at, blob) {
128
132
  }
129
133
  return new Uint8Array(plain);
130
134
  }
135
+ // ---------------------------------------------------------------------------
136
+ // Version 2: blocks cut by content, under a file's key that versions share
137
+ // ---------------------------------------------------------------------------
138
+ export const SHARD_VERSION_2 = 2;
139
+ /** The most bytes of a file one block holds, as the API takes blocks. */
140
+ export const MOST_BLOCK_BYTES = 64 * 1024 * 1024;
141
+ /** A block id: the HMAC-SHA256 of its bytes under the file's block key. */
142
+ export const BLOCK_ID_BYTES = 32;
143
+ const LABEL_2 = new TextEncoder().encode("filefreak-shard-v2");
144
+ const OUTLINE_LABEL = new TextEncoder().encode("filefreak-outline-v2");
145
+ /** The three keys of a file's key: HKDF-SHA256 with no salt, each named by its use. */
146
+ export async function fileKeys(raw) {
147
+ if (raw.length !== 32)
148
+ throw new RangeError(`A shard key is 32 bytes, not ${raw.length}.`);
149
+ const base = await crypto.subtle.importKey("raw", new Uint8Array(raw), "HKDF", false, ["deriveBits"]);
150
+ const derive = (use) => crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: new TextEncoder().encode(`filefreak-shard-v2 ${use}`) }, base, 256);
151
+ const [seal, block, outline] = await Promise.all([derive("seal"), derive("block"), derive("outline")]);
152
+ const hmac = (raw) => crypto.subtle.importKey("raw", raw, { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
153
+ return {
154
+ seal: await crypto.subtle.importKey("raw", seal, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]),
155
+ block: await hmac(block),
156
+ outline: await hmac(outline),
157
+ };
158
+ }
159
+ /** A block's id: what the map holds in place of a hash anybody could make of the same bytes. */
160
+ export async function blockId(keys, plaintext) {
161
+ return new Uint8Array(await crypto.subtle.sign("HMAC", keys.block, new Uint8Array(plaintext)));
162
+ }
163
+ /** The bytes a block's tag also covers: the label and its id, and nothing about where it is, so that a later version can keep it as it is. */
164
+ export function blockAssociatedData(id) {
165
+ if (id.length !== BLOCK_ID_BYTES)
166
+ throw new RangeError(`A block id is ${BLOCK_ID_BYTES} bytes, not ${id.length}.`);
167
+ const out = new Uint8Array(LABEL_2.length + BLOCK_ID_BYTES);
168
+ out.set(LABEL_2, 0);
169
+ out.set(id, LABEL_2.length);
170
+ return out;
171
+ }
172
+ /** One block, encrypted and ready to store under a nonce of its own, with its id. */
173
+ export async function sealBlock(keys, plaintext) {
174
+ const id = await blockId(keys, plaintext);
175
+ return { id, blob: await sealBlockWithNonce(keys, id, plaintext, crypto.getRandomValues(new Uint8Array(NONCE_BYTES))) };
176
+ }
177
+ /**
178
+ * sealBlock with the id and the nonce chosen by the caller, which only the
179
+ * test files may do, as with sealWithNonce. Not exported from the package.
180
+ */
181
+ export async function sealBlockWithNonce(keys, id, plaintext, nonce) {
182
+ if (plaintext.length > MOST_BLOCK_BYTES)
183
+ throw new ShardError("length", `A block holds at most ${MOST_BLOCK_BYTES} bytes of the file, not ${plaintext.length}.`);
184
+ if (nonce.length !== NONCE_BYTES)
185
+ throw new RangeError(`A nonce is ${NONCE_BYTES} bytes, not ${nonce.length}.`);
186
+ const iv = new Uint8Array(nonce);
187
+ const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData: blockAssociatedData(id), tagLength: 128 }, keys.seal, new Uint8Array(plaintext));
188
+ const out = new Uint8Array(HEADER_BYTES + sealed.byteLength);
189
+ out[0] = SHARD_VERSION_2;
190
+ out.set(iv, 1);
191
+ out.set(new Uint8Array(sealed), HEADER_BYTES);
192
+ return out;
193
+ }
194
+ /**
195
+ * A stored block's bytes of the file, once it is known to be the block the
196
+ * outline names, unchanged: in this order, short, version, length,
197
+ * authentication (the tag, which binds it to its id), and id (that its
198
+ * bytes are the ones the id was made of). Nothing of a block that fails is
199
+ * returned.
200
+ */
201
+ export async function openBlock(keys, block, blob) {
202
+ const named = hexOf(block.id).slice(0, 12);
203
+ if (blob.length < SHARD_OVERHEAD) {
204
+ throw new ShardError("short", `Block ${named} is ${blob.length} bytes, shorter than any block can be (${SHARD_OVERHEAD}).`);
205
+ }
206
+ if (blob[0] !== SHARD_VERSION_2) {
207
+ throw new ShardError("version", `Block ${named} is in version ${blob[0]} of the format, where version ${SHARD_VERSION_2} was expected.`);
208
+ }
209
+ if (blob.length !== block.length + SHARD_OVERHEAD) {
210
+ throw new ShardError("length", `Block ${named} is ${blob.length} bytes, and holding ${block.length} bytes of the file it would be ${block.length + SHARD_OVERHEAD}.`);
211
+ }
212
+ let plain;
213
+ try {
214
+ plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv: new Uint8Array(blob.subarray(1, HEADER_BYTES)), additionalData: blockAssociatedData(block.id), tagLength: 128 }, keys.seal, new Uint8Array(blob.subarray(HEADER_BYTES)));
215
+ }
216
+ catch (e) {
217
+ if (e instanceof DOMException && e.name === "OperationError") {
218
+ throw new ShardError("authentication", `Block ${named} is not what was written as this file's block ${named}: it has been changed, or it belongs to another file or block.`);
219
+ }
220
+ throw e;
221
+ }
222
+ const bytes = new Uint8Array(plain);
223
+ // The tag says somebody with the key sealed these bytes under this id; this says the id is theirs.
224
+ if (!(await crypto.subtle.verify("HMAC", keys.block, new Uint8Array(block.id), bytes))) {
225
+ throw new ShardError("id", `Block ${named} opens, but its bytes are not the ones its id was made of.`);
226
+ }
227
+ return bytes;
228
+ }
229
+ /** Refuse an outline that describes no file: no blocks, a block too long, an empty block in a file that is not empty, or lengths that do not add up. */
230
+ function checkShape(outline) {
231
+ const { size, blocks } = outline;
232
+ if (!Number.isSafeInteger(size) || size < 0)
233
+ throw new ShardError("layout", `A file's size is a whole number of bytes, not ${size}.`);
234
+ if (blocks.length === 0)
235
+ throw new ShardError("layout", "An outline names at least one block: an empty file is one empty block.");
236
+ if (blocks.length > MOST_FRAGMENTS)
237
+ throw new ShardError("layout", `An outline of ${blocks.length} blocks is more than the format can number.`);
238
+ let total = 0;
239
+ for (const [place, b] of blocks.entries()) {
240
+ if (b.id.length !== BLOCK_ID_BYTES)
241
+ throw new ShardError("layout", `Block ${place}'s id is ${b.id.length} bytes, not ${BLOCK_ID_BYTES}.`);
242
+ if (!Number.isSafeInteger(b.length) || b.length < 0 || b.length > MOST_BLOCK_BYTES) {
243
+ throw new ShardError("layout", `Block ${place} holds ${b.length} bytes, outside 0 to ${MOST_BLOCK_BYTES}.`);
244
+ }
245
+ if (b.length === 0 && size > 0)
246
+ throw new ShardError("layout", `Block ${place} is empty, and only an empty file has an empty block.`);
247
+ total += b.length;
248
+ }
249
+ if (total !== size)
250
+ throw new ShardError("layout", `The blocks hold ${total} bytes, and the file is ${size}.`);
251
+ }
252
+ /** The bytes the outline's MAC is over: the label, the content id, the size, the count, and each block's id and length in order. */
253
+ function outlineBytes(outline) {
254
+ const head = OUTLINE_LABEL.length + 16 + 8 + 4;
255
+ const out = new Uint8Array(head + outline.blocks.length * (BLOCK_ID_BYTES + 4));
256
+ const view = new DataView(out.buffer);
257
+ out.set(OUTLINE_LABEL, 0);
258
+ out.set(uuidBytes(outline.contentId), OUTLINE_LABEL.length);
259
+ view.setBigUint64(OUTLINE_LABEL.length + 16, BigInt(outline.size), false);
260
+ view.setUint32(OUTLINE_LABEL.length + 24, outline.blocks.length, false);
261
+ for (const [i, b] of outline.blocks.entries()) {
262
+ const at = head + i * (BLOCK_ID_BYTES + 4);
263
+ out.set(b.id, at);
264
+ view.setUint32(at + BLOCK_ID_BYTES, b.length, false);
265
+ }
266
+ return out;
267
+ }
268
+ /** The outline's MAC, which the writer sends with the upload's completion and every reader checks before it releases a byte. */
269
+ export async function outlineMac(keys, outline) {
270
+ checkShape(outline);
271
+ return new Uint8Array(await crypto.subtle.sign("HMAC", keys.outline, outlineBytes(outline)));
272
+ }
273
+ /**
274
+ * Refuse an outline unless its MAC holds under the file's key: whoever has
275
+ * the map and not the key cannot reorder, drop, repeat or swap a content's
276
+ * blocks, or give one content's blocks as another's.
277
+ */
278
+ export async function checkOutline(keys, outline, mac) {
279
+ checkShape(outline);
280
+ if (!(await crypto.subtle.verify("HMAC", keys.outline, new Uint8Array(mac), outlineBytes(outline)))) {
281
+ throw new ShardError("outline", "The file's outline is not what was written for it: its blocks have been reordered, dropped or swapped, or it is another file's.");
282
+ }
283
+ }
284
+ function hexOf(b) {
285
+ return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
286
+ }
131
287
  function uuidBytes(id) {
132
288
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) {
133
289
  throw new RangeError(`A content id is a UUID, and ${JSON.stringify(id)} is not one.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@filefreak.io/sdk",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "FileFreak's developer API for Node and the browser: every endpoint, uploads straight to storage in parts, downloads in ranges, and the changes to follow.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://filefreak.io/developers/reference",