@pithy-sh/storage 0.1.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 +21 -0
- package/README.md +17 -0
- package/package.json +52 -0
- package/pithy.manifest.json +70 -0
- package/src/capability.ts +104 -0
- package/src/cloudflare-test.d.ts +14 -0
- package/src/config/config.ts +105 -0
- package/src/data/share.ts +36 -0
- package/src/data/storageObject.ts +81 -0
- package/src/data/tables.ts +37 -0
- package/src/error/errors.ts +140 -0
- package/src/http/guard.ts +32 -0
- package/src/http/handlers.ts +684 -0
- package/src/http/routes.ts +313 -0
- package/src/http/schemas.ts +214 -0
- package/src/http/serve.ts +288 -0
- package/src/index.ts +42 -0
- package/src/migrations/0001_objects.ts +86 -0
- package/src/object/cloudflare.ts +55 -0
- package/src/object/key.ts +40 -0
- package/src/object/multipart.ts +166 -0
- package/src/object/store.ts +371 -0
- package/src/provision/provisionStorage.ts +192 -0
- package/src/provision/resolveStorageConfig.ts +80 -0
- package/src/quota/quota.ts +241 -0
- package/src/secret/registry.ts +118 -0
- package/src/seeds/example.ts +120 -0
- package/src/test-utils/liveStorage.ts +261 -0
- package/src/version.generated.ts +16 -0
- package/src/workflows/retryPolicy.ts +43 -0
- package/src/workflows/specs.ts +85 -0
- package/src/workflows/sweep.ts +226 -0
- package/src/workflows/worker.ts +103 -0
- package/src/workflows/wrangler.jsonc +54 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { PithyError } from "@pithy-sh/core/src/error/pithyError";
|
|
5
|
+
import type { MessageParams } from "@pithy-sh/core/src/i18n/catalog";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `@pithy-sh/storage` throw sugar. The `storage/*` codes live in core's closed `KitErrorPayload` union
|
|
9
|
+
* (CLAUDE.md §Errors); these subclasses are the package-local vehicles that set one of those members.
|
|
10
|
+
* Runtime code in this package throws one of these, never a plain `new Error`.
|
|
11
|
+
*
|
|
12
|
+
* Public text goes in `message` and `action`; throw-site context goes in `detail`, which the HTTP
|
|
13
|
+
* codec strips. That matters more here than in most capabilities: object keys, upload ids, and byte
|
|
14
|
+
* counts are exactly the kind of internal shape a client should never be handed.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface StorageErrorArgs {
|
|
18
|
+
message?: string;
|
|
19
|
+
action?: string;
|
|
20
|
+
detail?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Values a translating client interpolates into its own wording for this code. Client-facing, so —
|
|
23
|
+
* unlike `action` and `detail` — these cross the boundary with `message`.
|
|
24
|
+
*/
|
|
25
|
+
params?: MessageParams;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class StorageNotFoundError extends PithyError {
|
|
29
|
+
constructor(args: StorageErrorArgs = {}, options?: { cause?: unknown }) {
|
|
30
|
+
super(
|
|
31
|
+
{
|
|
32
|
+
code: "storage/not_found",
|
|
33
|
+
status: 404,
|
|
34
|
+
message: args.message ?? "That file does not exist.",
|
|
35
|
+
action: args.action ?? "Check the object id. A file you do not own reads as missing, not as forbidden.",
|
|
36
|
+
detail: args.detail,
|
|
37
|
+
params: args.params,
|
|
38
|
+
},
|
|
39
|
+
options,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class StorageForbiddenError extends PithyError {
|
|
45
|
+
constructor(args: StorageErrorArgs = {}, options?: { cause?: unknown }) {
|
|
46
|
+
super(
|
|
47
|
+
{
|
|
48
|
+
code: "storage/forbidden",
|
|
49
|
+
status: 403,
|
|
50
|
+
message: args.message ?? "That file is not yours.",
|
|
51
|
+
action: args.action ?? "Sign in as the owner, or ask them for a share link.",
|
|
52
|
+
detail: args.detail,
|
|
53
|
+
params: args.params,
|
|
54
|
+
},
|
|
55
|
+
options,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class StorageQuotaExceededError extends PithyError {
|
|
61
|
+
constructor(args: StorageErrorArgs = {}, options?: { cause?: unknown }) {
|
|
62
|
+
super(
|
|
63
|
+
{
|
|
64
|
+
code: "storage/quota_exceeded",
|
|
65
|
+
status: 413,
|
|
66
|
+
message: args.message ?? "That upload would put you over your storage quota.",
|
|
67
|
+
action: args.action ?? "Delete something first, or raise the quota in pithy.config.ts.",
|
|
68
|
+
detail: args.detail,
|
|
69
|
+
params: args.params,
|
|
70
|
+
},
|
|
71
|
+
options,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class StorageUploadIncompleteError extends PithyError {
|
|
77
|
+
constructor(args: StorageErrorArgs = {}, options?: { cause?: unknown }) {
|
|
78
|
+
super(
|
|
79
|
+
{
|
|
80
|
+
code: "storage/upload_incomplete",
|
|
81
|
+
status: 409,
|
|
82
|
+
message: args.message ?? "That file has not finished uploading.",
|
|
83
|
+
action: args.action ?? "Upload every part, then complete the upload.",
|
|
84
|
+
detail: args.detail,
|
|
85
|
+
params: args.params,
|
|
86
|
+
},
|
|
87
|
+
options,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export class StorageMultipartFailedError extends PithyError {
|
|
93
|
+
constructor(args: StorageErrorArgs = {}, options?: { cause?: unknown }) {
|
|
94
|
+
super(
|
|
95
|
+
{
|
|
96
|
+
code: "storage/multipart_failed",
|
|
97
|
+
status: 500,
|
|
98
|
+
// The recovery route is the caller's own answer, so it is in `message`: an `action` is the
|
|
99
|
+
// operator's sentence and the HTTP codec strips it (#344). A client that cannot see how to
|
|
100
|
+
// finish an upload it half-made has been told nothing useful.
|
|
101
|
+
message: args.message ?? "The upload could not be assembled. GET /storage/<id>/parts to see what is missing.",
|
|
102
|
+
detail: args.detail,
|
|
103
|
+
params: args.params,
|
|
104
|
+
},
|
|
105
|
+
options,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class StorageShareExpiredError extends PithyError {
|
|
111
|
+
constructor(args: StorageErrorArgs = {}, options?: { cause?: unknown }) {
|
|
112
|
+
super(
|
|
113
|
+
{
|
|
114
|
+
code: "storage/share_expired",
|
|
115
|
+
status: 410,
|
|
116
|
+
message: args.message ?? "That share link has expired.",
|
|
117
|
+
action: args.action ?? "Ask the owner for a new one.",
|
|
118
|
+
detail: args.detail,
|
|
119
|
+
params: args.params,
|
|
120
|
+
},
|
|
121
|
+
options,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class StorageShareRevokedError extends PithyError {
|
|
127
|
+
constructor(args: StorageErrorArgs = {}, options?: { cause?: unknown }) {
|
|
128
|
+
super(
|
|
129
|
+
{
|
|
130
|
+
code: "storage/share_revoked",
|
|
131
|
+
status: 410,
|
|
132
|
+
message: args.message ?? "That share link was revoked.",
|
|
133
|
+
action: args.action ?? "Ask the owner for a new one.",
|
|
134
|
+
detail: args.detail,
|
|
135
|
+
params: args.params,
|
|
136
|
+
},
|
|
137
|
+
options,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import type { PithyHonoEnv } from "@pithy-sh/core/src/capability/capability";
|
|
5
|
+
import { UnauthorizedError } from "@pithy-sh/core/src/error/pithyError";
|
|
6
|
+
import type { MiddlewareHandler } from "hono";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The storage routes' identity gate.
|
|
10
|
+
*
|
|
11
|
+
* **Copied, not imported.** This is deliberately a local copy of the same three lines
|
|
12
|
+
* `@pithy-sh/media` and `@pithy-sh/ledger` each carry, rather than an import from `@pithy-sh/auth`.
|
|
13
|
+
* Importing it would make auth a hard dependency of storage, and a hard dependency is the wrong
|
|
14
|
+
* shape twice over: storage works perfectly well for system-owned objects with no auth capability
|
|
15
|
+
* composed, and — far more importantly — a package that *imports* its authorization from another
|
|
16
|
+
* package fails open when that package is absent. Depending on the core `AuthContext` seam instead
|
|
17
|
+
* means `c.var.auth` is simply `null` with no auth capability composed, and every owner-scoped
|
|
18
|
+
* route **denies**. Failing closed is not a side effect here; it is the reason for the copy.
|
|
19
|
+
*
|
|
20
|
+
* So `dependsOn` stays free of auth and the manifest lists it under `optionalCapabilities`.
|
|
21
|
+
*/
|
|
22
|
+
export function requireAuth(): MiddlewareHandler<PithyHonoEnv> {
|
|
23
|
+
return async (c, next) => {
|
|
24
|
+
if (!c.var.auth) {
|
|
25
|
+
throw new UnauthorizedError({
|
|
26
|
+
message: "Authentication required.",
|
|
27
|
+
action: "Sign in and retry with a valid session or bearer token.",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
await next();
|
|
31
|
+
};
|
|
32
|
+
}
|