@edgeone/pages-blob 0.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 ADDED
@@ -0,0 +1,154 @@
1
+ # @edgeone/pages-blob
2
+
3
+ Blob storage SDK for EdgeOne Pages Functions, providing persistent key-value storage across deployments.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @edgeone/pages-blob
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Inside Pages Functions (automatic authentication)
14
+
15
+ ```js
16
+ import { getStore } from "@edgeone/pages-blob";
17
+
18
+ const store = getStore("my-store");
19
+
20
+ // Write
21
+ await store.set("key", "value");
22
+ await store.setJSON("config", { theme: "dark" });
23
+
24
+ // Read
25
+ const text = await store.get("key");
26
+ const json = await store.get("config", { type: "json" });
27
+
28
+ // Delete
29
+ await store.delete("key");
30
+
31
+ // List
32
+ const { blobs } = await store.list({ prefix: "users/" });
33
+ ```
34
+
35
+ ### External access (API Token mode)
36
+
37
+ When calling from outside Pages Functions, you must supply **both** `token` and `projectId`:
38
+
39
+ ```js
40
+ const store = getStore({
41
+ name: "my-store",
42
+ projectId: "pages-urtsvuwmfvli", // required
43
+ token: "c+KH5...", // required
44
+ });
45
+ ```
46
+
47
+ Omitting `projectId` throws `MissingProjectIdError`.
48
+
49
+ ### Strong consistency (read-after-write)
50
+
51
+ By default reads use eventual consistency (CDN-cached, lower latency). To
52
+ guarantee read-after-write consistency, use `"strong"`:
53
+
54
+ ```js
55
+ // Store-level default — all reads use strong consistency
56
+ const store = getStore({ name: "my-store", consistency: "strong" });
57
+
58
+ // Per-call override
59
+ await store.get("key", { consistency: "strong" });
60
+ await store.getMetadata("key", { consistency: "strong" });
61
+ await store.list({ consistency: "strong" });
62
+ ```
63
+
64
+ `"eventual"` (the default) has lower latency; `"strong"` guarantees the
65
+ freshest value at the cost of bypassing the cache layer.
66
+
67
+ ### List all stores
68
+
69
+ ```js
70
+ import { listStores } from "@edgeone/pages-blob";
71
+
72
+ // Inside Pages Functions
73
+ const { stores } = await listStores();
74
+
75
+ // External access (token mode) - both fields required
76
+ const { stores } = await listStores({
77
+ projectId: "pages-urtsvuwmfvli",
78
+ token: "c+KH5...",
79
+ });
80
+ ```
81
+
82
+ ## API
83
+
84
+ ### `getStore(name | options)`
85
+
86
+ Get a Store instance.
87
+
88
+ | Parameter | Type | Description |
89
+ | --------------------- | ------------------------ | ----------------------------------------------- |
90
+ | `name` | `string` | Store name |
91
+ | `options.name` | `string` | Store name |
92
+ | `options.projectId` | `string` | Project ID (**required** for external access) |
93
+ | `options.token` | `string` | Access token (**required** for external access) |
94
+ | `options.consistency` | `"eventual" \| "strong"` | Default read consistency (default `"eventual"`) |
95
+
96
+ ### `store.set(key, value, options?)`
97
+
98
+ Write a Blob. `value` supports `string | ArrayBuffer | Blob | ReadableStream`.
99
+
100
+ - `options.onlyIfNew`: only write if the key does not already exist
101
+ - `options.cacheControl`: custom `Cache-Control` header for the stored object (default: `"max-age=0, stale-while-revalidate=60"`; pass `null` to omit)
102
+
103
+ ### `store.setJSON(key, value, options?)`
104
+
105
+ Write JSON (serialized automatically). Accepts the same `options` as `store.set`.
106
+
107
+ ### `store.get(key, options?)`
108
+
109
+ Read a Blob. Returns `null` if it does not exist.
110
+
111
+ - `options.type`: `"text"` (default) | `"json"` | `"arrayBuffer"` | `"blob"` | `"stream"`
112
+ - `options.consistency`: `"eventual"` | `"strong"` (overrides Store-level default)
113
+
114
+ ### `store.getMetadata(key, options?)`
115
+
116
+ Read blob metadata without downloading the body. Returns `null` if the key does not exist.
117
+
118
+ Returns: `{ cacheControl?, contentType?, etag?, headers? }`
119
+
120
+ - `options.consistency`: `"eventual"` | `"strong"`
121
+
122
+ ### `store.getWithHeaders(key, options?)`
123
+
124
+ Read a blob along with all response headers. Returns `null` if the key does not exist.
125
+
126
+ Returns: `{ body: string, headers: Record<string, string> }`
127
+
128
+ - `options.consistency`: `"eventual"` | `"strong"`
129
+
130
+ ### `store.delete(key)`
131
+
132
+ Delete a Blob.
133
+
134
+ ### `store.list(options?)`
135
+
136
+ List Blobs. Automatically aggregates all pages by default.
137
+
138
+ - `options.prefix`: prefix filter
139
+ - `options.directories`: group by directory
140
+ - `options.paginate`: set to `false` for manual pagination
141
+ - `options.cursor`: resume from a previous pagination cursor
142
+ - `options.consistency`: `"eventual"` | `"strong"`
143
+
144
+ Returns: `{ blobs: Array<{ key, etag }>, directories: string[] }`
145
+
146
+ ### `listStores(options?)`
147
+
148
+ List all Stores under the project.
149
+
150
+ - `options.projectId`: Project ID (**required** for external access)
151
+ - `options.token`: Access token (**required** for external access)
152
+ - `options.consistency`: `"eventual"` | `"strong"`
153
+
154
+ Returns: `{ stores: Array<{ name: string }> }`
@@ -0,0 +1,205 @@
1
+ // ─── Public Types ───
2
+
3
+ /**
4
+ * Read consistency mode
5
+ * - "eventual" (default): uses the CDN-cached domain, lower latency
6
+ * - "strong": uses the no-cache domain, guarantees read-after-write
7
+ */
8
+ export type ConsistencyMode = "eventual" | "strong";
9
+
10
+ export interface StoreOptions {
11
+ name: string;
12
+ /** Project ID, required when using token mode (external access) */
13
+ projectId: string;
14
+ /** API Token, required for external access */
15
+ token: string;
16
+ /**
17
+ * Default consistency mode applied to all read operations (get / getMetadata / getWithHeaders / list)
18
+ * Can be overridden per-call via options.consistency
19
+ */
20
+ consistency?: ConsistencyMode;
21
+ }
22
+
23
+ export type BlobInput = string | ArrayBuffer | Blob | ReadableStream;
24
+
25
+ export interface SetOptions {
26
+ /** Conditional write: only write if the key does not already exist */
27
+ onlyIfNew?: boolean;
28
+ /**
29
+ * Cache-Control response header for writes
30
+ * - If omitted: uses SDK default "max-age=0, stale-while-revalidate=60"
31
+ * - If string: overrides with this value
32
+ * - If null: does not send Cache-Control at all
33
+ */
34
+ cacheControl?: string | null;
35
+ }
36
+
37
+ export interface GetOptions {
38
+ type?: BlobResponseType;
39
+ /** Consistency mode for this read, overrides the Store-level default */
40
+ consistency?: ConsistencyMode;
41
+ }
42
+
43
+ export type BlobResponseType = "text" | "json" | "arrayBuffer" | "blob" | "stream";
44
+
45
+ export interface ListOptions {
46
+ prefix?: string;
47
+ directories?: boolean;
48
+ cursor?: string;
49
+ /** Defaults to true, automatically aggregates all pages */
50
+ paginate?: boolean;
51
+ /** Consistency mode for this listing, overrides the Store-level default */
52
+ consistency?: ConsistencyMode;
53
+ }
54
+
55
+ export interface ListResult {
56
+ blobs: BlobInfo[];
57
+ directories: string[];
58
+ }
59
+
60
+ export interface BlobInfo {
61
+ key: string;
62
+ etag: string;
63
+ }
64
+
65
+ export interface StoreListResult {
66
+ stores: { name: string }[];
67
+ }
68
+
69
+ // ─── Store Class ───
70
+
71
+ /**
72
+ * Store instance providing blob storage read/write operations.
73
+ * Obtained via getStore(), supports persistence across deployments.
74
+ */
75
+ export declare class Store {
76
+ /**
77
+ * Write a blob
78
+ */
79
+ set(key: string, value: BlobInput, options?: SetOptions): Promise<void>;
80
+
81
+ /**
82
+ * Write JSON (convenience method)
83
+ */
84
+ setJSON(key: string, value: unknown, options?: SetOptions): Promise<void>;
85
+
86
+ /**
87
+ * Read a blob
88
+ *
89
+ * Return type depends on the type parameter:
90
+ * - "text" (default): string
91
+ * - "json": any
92
+ * - "arrayBuffer": ArrayBuffer
93
+ * - "blob": Blob
94
+ * - "stream": ReadableStream
95
+ *
96
+ * Returns null if the key does not exist.
97
+ */
98
+ get(key: string): Promise<string | null>;
99
+ get(key: string, options: { type?: "text"; consistency?: ConsistencyMode }): Promise<string | null>;
100
+ get(key: string, options: { type: "json"; consistency?: ConsistencyMode }): Promise<any | null>;
101
+ get(key: string, options: { type: "arrayBuffer"; consistency?: ConsistencyMode }): Promise<ArrayBuffer | null>;
102
+ get(key: string, options: { type: "blob"; consistency?: ConsistencyMode }): Promise<Blob | null>;
103
+ get(key: string, options: { type: "stream"; consistency?: ConsistencyMode }): Promise<ReadableStream | null>;
104
+
105
+ /**
106
+ * Read blob metadata (without downloading body)
107
+ *
108
+ * Returns null if the key does not exist.
109
+ */
110
+ getMetadata(key: string, options?: { consistency?: ConsistencyMode }): Promise<{
111
+ cacheControl?: string;
112
+ contentType?: string;
113
+ etag?: string;
114
+ headers?: Record<string, string>;
115
+ } | null>;
116
+
117
+ /**
118
+ * Read object with all response headers (for verifying CDN behavior)
119
+ *
120
+ * Returns null if the key does not exist.
121
+ */
122
+ getWithHeaders(key: string, options?: { consistency?: ConsistencyMode }): Promise<{
123
+ body: string;
124
+ headers: Record<string, string>;
125
+ } | null>;
126
+
127
+ /**
128
+ * Delete a blob
129
+ */
130
+ delete(key: string): Promise<void>;
131
+
132
+ /**
133
+ * List blobs
134
+ *
135
+ * Automatically aggregates all pages by default. Set paginate: false for manual pagination.
136
+ */
137
+ list(options?: ListOptions): Promise<ListResult>;
138
+ }
139
+
140
+ // ─── Error Classes ───
141
+
142
+ /**
143
+ * Pages Blob error base class
144
+ */
145
+ export declare class PagesBlobError extends Error {
146
+ code: string;
147
+ constructor(code: string, message: string);
148
+ }
149
+
150
+ export declare class InvalidKeyError extends PagesBlobError {
151
+ constructor(detail: string);
152
+ }
153
+
154
+ export declare class InvalidStoreNameError extends PagesBlobError {
155
+ constructor(detail: string);
156
+ }
157
+
158
+ export declare class QuotaExceededError extends PagesBlobError {
159
+ constructor();
160
+ }
161
+
162
+ export declare class RateLimitedError extends PagesBlobError {
163
+ constructor();
164
+ }
165
+
166
+ export declare class MissingProjectIdError extends PagesBlobError {
167
+ constructor();
168
+ }
169
+
170
+ export declare class PreconditionFailedError extends PagesBlobError {
171
+ constructor();
172
+ }
173
+
174
+ // ─── Public Functions ───
175
+
176
+ /**
177
+ * Get a Store instance
178
+ *
179
+ * Automatically authenticated inside Pages Functions, no configuration needed:
180
+ * ```js
181
+ * const store = getStore("my-store");
182
+ * ```
183
+ *
184
+ * External access (API token mode) requires both projectId and token:
185
+ * ```js
186
+ * const store = getStore({
187
+ * name: "my-store",
188
+ * projectId: "pages-urtsvuwmfvli", // required
189
+ * token: "c+KH5...", // required
190
+ * });
191
+ * ```
192
+ */
193
+ export declare function getStore(name: string): Store;
194
+ export declare function getStore(options: StoreOptions): Store;
195
+
196
+ /**
197
+ * List all stores under the project
198
+ *
199
+ * External access (API token mode) requires both projectId and token.
200
+ */
201
+ export declare function listStores(options?: {
202
+ projectId: string;
203
+ token: string;
204
+ consistency?: ConsistencyMode;
205
+ }): Promise<StoreListResult>;
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";var D=Object.defineProperty;var te=Object.getOwnPropertyDescriptor;var ne=Object.getOwnPropertyNames;var re=Object.prototype.hasOwnProperty;var se=(t,e)=>{for(var r in e)D(t,r,{get:e[r],enumerable:!0})},oe=(t,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of ne(e))!re.call(t,o)&&o!==r&&D(t,o,{get:()=>e[o],enumerable:!(n=te(e,o))||n.enumerable});return t};var ie=t=>oe(D({},"__esModule",{value:!0}),t);var ke={};se(ke,{InvalidKeyError:()=>y,InvalidStoreNameError:()=>m,MissingProjectIdError:()=>C,PagesBlobError:()=>f,PreconditionFailedError:()=>p,QuotaExceededError:()=>P,RateLimitedError:()=>R,Store:()=>x,getStore:()=>Te,listStores:()=>Ie});module.exports=ie(ke);var f=class extends Error{code;constructor(e,r){super(`PagesBlob: ${r}`),this.name="PagesBlobError",this.code=e}},y=class extends f{constructor(e){super("INVALID_KEY",e)}},m=class extends f{constructor(e){super("INVALID_STORE_NAME",e)}},S=class extends f{constructor(e){super("MISSING_ENVIRONMENT",`Environment not configured for Pages Blob. Missing: ${e.join(", ")}. Supply these properties when creating a store, or ensure the function is running in a Pages environment.`)}},P=class extends f{constructor(){super("QUOTA_EXCEEDED","storage quota exceeded")}},R=class extends f{constructor(){super("RATE_LIMITED","request rate limited, please retry later")}},C=class extends f{constructor(){super("MISSING_PROJECT_ID","projectId is required when using API token mode. Please supply { name, projectId, token } to getStore() / listStores().")}},h=class extends f{constructor(e){super("CREDENTIAL_ERROR",e)}},l=class extends f{constructor(e,r){super("COS_ERROR",`COS returned ${e}: ${r}`)}},p=class extends f{constructor(){super("PRECONDITION_FAILED","conditional write failed (key already exists)")}};function w(t){if(t==="")throw new y("Blob key must not be empty.");if(t.startsWith("/")||t.startsWith("%2F"))throw new y("Blob key must not start with forward slash (/).");if(new TextEncoder().encode(t).length>600)throw new y("Blob key must be a sequence of Unicode characters whose UTF-8 encoding is at most 600 bytes long.")}function $(t){if(t==="")throw new m("Store name must not be empty.");if(t.includes("/")||t.includes(":"))throw new m("Store name must not contain forward slashes (/) or colons (:).");if(!/^[a-zA-Z0-9_-]+$/.test(t))throw new m("Store name must only contain letters, digits, underscores, and hyphens.");if(new TextEncoder().encode(t).length>64)throw new m("Store name must be a sequence of Unicode characters whose UTF-8 encoding is at most 64 bytes long.")}var x=class{cosClient;storeName;defaultConsistency;constructor(e,r,n="eventual"){this.cosClient=e,this.storeName=r,this.defaultConsistency=n}resolveConsistency(e){return e??this.defaultConsistency}async set(e,r,n){w(e);let o=await this.cosClient.putObject(this.storeName,e,r,{onlyIfNew:n?.onlyIfNew,cacheControl:n?.cacheControl});if(n?.onlyIfNew&&o.statusCode===412)throw new p}async setJSON(e,r,n){w(e);let o=JSON.stringify(r),s=await this.cosClient.putObject(this.storeName,e,o,{onlyIfNew:n?.onlyIfNew,contentType:"application/json",cacheControl:n?.cacheControl});if(n?.onlyIfNew&&s.statusCode===412)throw new p}async get(e,r){w(e);let n=this.resolveConsistency(r?.consistency),o=await this.cosClient.getObject(this.storeName,e,n);if(o===null)return null;let{body:s}=o,i=r?.type??"text",a=new TextDecoder("utf-8");switch(i){case"text":return a.decode(s);case"json":return JSON.parse(a.decode(s));case"arrayBuffer":return s.buffer.slice(s.byteOffset,s.byteOffset+s.byteLength);case"blob":return new Blob([s]);case"stream":return new ReadableStream({start(c){c.enqueue(s),c.close()}});default:return a.decode(s)}}async getMetadata(e,r){w(e);let n=this.resolveConsistency(r?.consistency);return this.cosClient.headObject(this.storeName,e,n)}async getWithHeaders(e,r){w(e);let n=this.resolveConsistency(r?.consistency),o=await this.cosClient.getObject(this.storeName,e,n);return o?{body:new TextDecoder("utf-8").decode(o.body),headers:o.headers||{}}:null}async delete(e){w(e),await this.cosClient.deleteObject(this.storeName,e)}async list(e){let r=e?.paginate!==!1,n=[],o=[],s=this.resolveConsistency(e?.consistency),i=e?.cursor||"",a=!0;for(;a;){let c=await this.cosClient.listObjects(this.storeName,{prefix:e?.prefix,delimiter:e?.directories?"/":void 0,marker:i||void 0,maxKeys:1e3,consistency:s});for(let d of c.contents)n.push({key:d.key,etag:d.etag});o.push(...c.commonPrefixes),!r||!c.isTruncated?a=!1:i=c.nextMarker}return{blobs:n,directories:o}}};var ae=new TextEncoder;function N(t){let e=ae.encode(t),r=new ArrayBuffer(e.byteLength),n=new Uint8Array(r);return n.set(e),n}function H(t){let e=t instanceof Uint8Array?t:new Uint8Array(t),r="";for(let n=0;n<e.length;n++)r+=e[n].toString(16).padStart(2,"0");return r}async function L(t,e){let r=await crypto.subtle.importKey("raw",N(t),{name:"HMAC",hash:"SHA-1"},!1,["sign"]),n=await crypto.subtle.sign("HMAC",r,N(e));return H(n)}async function ce(t){let e=await crypto.subtle.digest("SHA-1",N(t));return H(e)}function v(t){return encodeURIComponent(t).replace(/[!'()*]/g,e=>"%"+e.charCodeAt(0).toString(16).toUpperCase())}function de(t){return t.split("/").map(e=>v(e)).join("/")}var le=new Set(["cache-control","content-disposition","content-encoding","content-length","content-md5","content-type","expect","expires","if-match","if-modified-since","if-none-match","if-unmodified-since","origin","range","transfer-encoding"]);function ue(t){return t==="host"||t==="x-cos-security-token"?!1:!!(le.has(t)||t.startsWith("x-cos-"))}function K(t){if(!t)return[];let e=[];for(let[r,n]of Object.entries(t))n!=null&&e.push([r.toLowerCase(),String(n)]);return e.sort(([r],[n])=>r<n?-1:r>n?1:0),e}function q(t){return t.map(([e,r])=>`${v(e)}=${v(r)}`).join("&")}function U(t){return t.map(([e])=>v(e)).join(";")}async function ge(t){let e=t.method.toLowerCase(),r=t.pathname.startsWith("/")?t.pathname:`/${t.pathname}`,n=Math.floor(Date.now()/1e3),o=n+(t.expireSeconds??3600),s=`${n};${o}`,a=K(t.headers).filter(([j])=>ue(j)),c=U(a),d=q(a),u=K(t.query),g=U(u),M=q(u),O=`${e}
2
+ ${r}
3
+ ${M}
4
+ ${d}
5
+ `,V=`sha1
6
+ ${s}
7
+ ${await ce(O)}
8
+ `,J=await L(t.secretKey,s),Q=await L(J,V),Z=["q-sign-algorithm=sha1",`q-ak=${t.secretId}`,`q-sign-time=${s}`,`q-key-time=${s}`,`q-header-list=${c}`,`q-url-param-list=${g}`,`q-signature=${Q}`].join("&"),_={};for(let[j,ee]of a)_[j]=ee;return{authorization:Z,signedHeaders:_}}async function b(t){let e=new URL(t.domain),r=t.key?`/${de(t.key)}`:"/";if(e.pathname=r,t.query)for(let[s,i]of Object.entries(t.query))i!=null&&e.searchParams.set(s,String(i));let{authorization:n}=await ge({method:t.method,pathname:r,query:t.query,headers:t.headers,secretId:t.credential.secretId,secretKey:t.credential.secretKey}),o=new Headers;if(t.headers)for(let[s,i]of Object.entries(t.headers))i!=null&&o.set(s,String(i));return o.set("Authorization",n),t.credential.sessionToken&&o.set("x-cos-security-token",t.credential.sessionToken),fetch(e.toString(),{method:t.method,headers:o,body:t.body??void 0,signal:t.signal})}var fe="blob.edgeone.site",he="blob-nocache.edgeone.site",k=class{credentialManager;bucket="";region="";keyPrefix="";cachedDomain="";uncachedDomain="";initialized=!1;constructor(e){this.credentialManager=e}computeSubdomain(e){let r=[];if(e.appId&&r.push(e.appId),e.zoneId&&r.push(e.zoneId),e.projectId&&r.push(e.projectId),r.length===3)return r.join("-");if(e.resourcePrefix){let o=e.resourcePrefix.replace(/\/?\*$/,"").split("/").filter(Boolean);if(o.length>=3)return o.slice(0,3).join("-")}return""}async ensureInitialized(){if(this.initialized)return;let e=await this.credentialManager.getCredential();!this.keyPrefix&&e.resourcePrefix&&(this.keyPrefix=e.resourcePrefix.replace(/\/?\*$/,""));let r=e.edgeRegion==="CN",n=e.cosMainland,o=e.cosOverseas,s=r?n||o:o||n;!this.bucket&&s&&(this.bucket=s.bucket,this.region=s.region);let i=this.computeSubdomain(e);if(!i)throw new l(0,"unable to derive tenant subdomain from credential; missing appId/zoneId/projectId or resourcePrefix");this.cachedDomain=`https://${i}.${fe}`,this.uncachedDomain=`https://${i}.${he}`,this.initialized=!0}async resolveDomain(e){return await this.ensureInitialized(),e==="strong"?this.uncachedDomain:this.cachedDomain}async resolveCredential(){let e=await this.credentialManager.getCredential();return{secretId:e.tmpSecretId,secretKey:e.tmpSecretKey,sessionToken:e.sessionToken}}buildCosKey(e,r){return`${this.keyPrefix}/${e}/${r}`}async getDomains(){return await this.ensureInitialized(),{cached:this.cachedDomain,uncached:this.uncachedDomain}}async putObject(e,r,n,o){let s=await this.resolveDomain("eventual"),i=await this.resolveCredential(),a=this.buildCosKey(e,r),d=o?.cacheControl===null?void 0:o?.cacheControl??"max-age=0, stale-while-revalidate=60",u={};o?.onlyIfNew&&(u["If-None-Match"]="*"),d&&(u["Cache-Control"]=d),o?.contentType&&(u["Content-Type"]=o.contentType);try{let g=await b({domain:s,method:"PUT",key:a,headers:u,body:n,credential:i});if(g.status===412)return await g.arrayBuffer().catch(()=>{}),{etag:"",statusCode:412};if(!g.ok){let O=await T(g);throw new l(g.status,O||`putObject failed: ${g.status}`)}let M=g.headers.get("etag")||"";return await g.arrayBuffer().catch(()=>{}),{etag:M,statusCode:g.status}}catch(g){throw g instanceof l?g:new l(0,g.message||String(g))}}async getObject(e,r,n){let o=await this.resolveDomain(n),s=await this.resolveCredential(),i=this.buildCosKey(e,r);try{let a=await b({domain:o,method:"GET",key:i,credential:s});if(a.status===404)return await a.arrayBuffer().catch(()=>{}),null;if(!a.ok){let u=await T(a);throw new l(a.status,u||`getObject failed: ${a.status}`)}let c=new Uint8Array(await a.arrayBuffer()),d=z(a.headers);return{body:c,contentType:d["content-type"],headers:d}}catch(a){throw a instanceof l?a:new l(0,a.message||String(a))}}async headObject(e,r,n){let o=await this.resolveDomain(n),s=await this.resolveCredential(),i=this.buildCosKey(e,r);try{let a=await b({domain:o,method:"HEAD",key:i,credential:s});if(a.status===404)return null;if(!a.ok){let d=await T(a);throw new l(a.status,d||`headObject failed: ${a.status}`)}let c=z(a.headers);return{cacheControl:c["cache-control"],contentType:c["content-type"],etag:c.etag,headers:c}}catch(a){throw a instanceof l?a:new l(0,a.message||String(a))}}async deleteObject(e,r){let n=await this.resolveDomain("eventual"),o=await this.resolveCredential(),s=this.buildCosKey(e,r);try{let i=await b({domain:n,method:"DELETE",key:s,credential:o});if(i.status===204||i.status===404||i.ok){await i.arrayBuffer().catch(()=>{});return}let a=await T(i);throw new l(i.status,a||`deleteObject failed: ${i.status}`)}catch(i){throw i instanceof l?i:new l(0,i.message||String(i))}}async listObjects(e,r){let n=`${this.keyPrefix}/${e}/`,o=r?.prefix?n+r.prefix:n,s=await this.getBucketRaw({prefix:o,delimiter:r?.delimiter,marker:r?.marker,maxKeys:r?.maxKeys,consistency:r?.consistency}),i=s.contents.map(c=>{let d=c.key,u=d.startsWith(n)?d.slice(n.length):d;return u?{key:u,etag:c.etag}:null}).filter(c=>c!==null),a=s.commonPrefixes.map(c=>c.startsWith(n)?c.slice(n.length):c).filter(c=>!!c);return{contents:i,commonPrefixes:a,isTruncated:s.isTruncated,nextMarker:s.nextMarker}}async listStores(e){let r=[],n="",o=!0;for(;o;){await this.ensureInitialized();let s=`${this.keyPrefix}/`,i=await this.getBucketRaw({prefix:s,delimiter:"/",maxKeys:1e3,marker:n||void 0,consistency:e});for(let a of i.commonPrefixes){let c=a.startsWith(s)?a.slice(s.length,-1):a.slice(0,-1);c&&r.push(c)}if(o=i.isTruncated,n=i.nextMarker,!o||!n)break}return r}async getBucketRaw(e){let r=await this.resolveDomain(e.consistency),n=await this.resolveCredential(),o={prefix:e.prefix};e.delimiter&&(o.delimiter=e.delimiter),e.marker&&(o.marker=e.marker),e.maxKeys&&(o["max-keys"]=e.maxKeys);try{let s=await b({domain:r,method:"GET",query:o,credential:n});if(!s.ok){let a=await T(s);throw new l(s.status,a||`getBucket failed: ${s.status}`)}let i=await s.text();return me(i)}catch(s){throw s instanceof l?s:new l(0,s.message||String(s))}}};async function T(t){try{return await t.text()}catch{return""}}function z(t){let e={};return t.forEach((r,n)=>{e[n.toLowerCase()]=r}),e}function me(t){let e=[],r=/<Contents>([\s\S]*?)<\/Contents>/g,n;for(;(n=r.exec(t))!==null;){let d=n[1],u=I(d,"Key"),g=I(d,"ETag");u!==null&&e.push({key:B(u),etag:g||""})}let o=[],s=/<CommonPrefixes>([\s\S]*?)<\/CommonPrefixes>/g;for(;(n=s.exec(t))!==null;){let d=n[1],u=I(d,"Prefix");u!==null&&o.push(B(u))}let a=I(t,"IsTruncated")==="true",c=I(t,"NextMarker")||"";return{contents:e,commonPrefixes:o,isTruncated:a,nextMarker:B(c)}}function I(t,e){let n=new RegExp(`<${e}>([\\s\\S]*?)<\\/${e}>`).exec(t);return n?n[1]:null}function B(t){return t.replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&quot;/g,'"').replace(/&apos;/g,"'").replace(/&amp;/g,"&")}var ye="X-RateLimit-Reset";async function A(t,e,r=5){e.signal?.throwIfAborted?.();try{let n=await fetch(t,e);if(r>0&&(n.status===429||n.status>=500)){let o=Y(n.headers.get(ye));return await X(o,e.signal),A(t,e,r-1)}return n}catch(n){if(r===0||n instanceof DOMException&&n.name==="AbortError")throw n;let o=Y();return await X(o,e.signal),A(t,e,r-1)}}function Y(t){return t?Math.max(Number(t)*1e3-Date.now(),1e3):5e3}function X(t,e){return new Promise((r,n)=>{if(e?.aborted)return n(e.reason);let o=setTimeout(()=>{e?.removeEventListener("abort",s),r()},t),s=()=>{clearTimeout(o),n(e.reason)};e?.addEventListener("abort",s,{once:!0})})}var F="prod";var pe=300,we="https://blob-sts.edgeone.site/",E=class{authToken;projectId;cached=null;constructor(e,r){this.authToken=e,this.projectId=r}async getCredential(){if(this.cached&&!this.isExpired(this.cached))return this.cached;let e=await this.fetchCredential();return this.cached=e,e}clearCache(){this.cached=null}isExpired(e){let r=Math.floor(Date.now()/1e3);return e.expiredTime-r<pe}async fetchCredential(){for(let r=1;r<=3;r++){let n=await A(we,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.authToken}`,"X-Env":F},body:JSON.stringify(this.projectId?{ProjectId:this.projectId}:{})});if(n.status===413)throw new h("storage quota exceeded");if(n.status===429)throw new h("rate limited, please retry later");if(!n.ok){let i=await n.text().catch(()=>"unknown error");throw new h(`failed to obtain STS credential: ${n.status} ${i}`)}let o=await n.json(),s=o.data&&typeof o.data=="object"?o.data:o;if(s.tmpSecretId&&s.tmpSecretKey&&s.sessionToken&&s.expiredTime){let i=s.cosMainland,a=s.cosOverseas,c=n.headers.get("X-Edge-Region")||void 0;return{tmpSecretId:s.tmpSecretId,tmpSecretKey:s.tmpSecretKey,sessionToken:s.sessionToken,expiredTime:s.expiredTime,appId:s.appId||void 0,zoneId:s.zoneId||void 0,projectId:s.projectId||void 0,resourcePrefix:s.resourcePrefix||void 0,cosMainland:i||void 0,cosOverseas:a||void 0,edgeRegion:c}}if(s.code!==void 0&&s.code!==0){let i=s.msg||s.message||"unknown error";throw new h(`credential exchange failed (code=${s.code}): ${i}`)}if(o.code!==void 0&&o.code!==0){let i=o.msg||o.message||"unknown error";throw new h(`credential exchange failed (code=${o.code}): ${i}`)}if(r<3){await Ce(1e3*r);continue}throw new h("invalid STS credential response")}throw new h("invalid STS credential response")}};function Ce(t){return new Promise(e=>setTimeout(e,t))}var xe="{{PAGES_BLOB_DEPLOY_CREDENTIAL}}";function W(){let t=be();if(t)return{deployCredential:t};let e=Se("PAGES_BLOB_DEPLOY_CREDENTIAL");return e?{deployCredential:e}:{}}function be(){let t=xe;if(!(t.startsWith("{{")&&t.endsWith("}}")))return t||void 0}function Se(t){if(typeof process<"u"&&process.env)return process.env[t]}function Te(t){let e=typeof t=="string"?t:t.name;$(e);let r=G(typeof t=="string"?void 0:t),n=new E(r.authToken,r.projectId),o=new k(n);return new x(o,e,r.consistency??"eventual")}async function Ie(t){let e=G(t?{name:"__list__",projectId:t.projectId,token:t.token,consistency:t.consistency}:void 0),r=new E(e.authToken,e.projectId);return{stores:(await new k(r).listStores(e.consistency)).map(s=>({name:s}))}}function G(t){if(t?.token){if(!t.projectId)throw new C;return{authToken:t.token,projectId:t.projectId,consistency:t.consistency}}if(t?.projectId&&!t.token)throw new S(["token"]);let e=W();if(!e.deployCredential)throw new S(["deployCredential"]);return{authToken:e.deployCredential,consistency:t?.consistency}}0&&(module.exports={InvalidKeyError,InvalidStoreNameError,MissingProjectIdError,PagesBlobError,PreconditionFailedError,QuotaExceededError,RateLimitedError,Store,getStore,listStores});
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@edgeone/pages-blob",
3
+ "version": "0.0.1",
4
+ "description": "Blob storage SDK for EdgeOne Pages functions",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "clean": "rm -rf dist",
21
+ "build": "tsup && cp src/public-types.d.ts dist/index.d.ts",
22
+ "dev": "tsup --watch",
23
+ "test": "npm run build && cd __tests__/npm-install-test && npm install && npm test",
24
+ "typecheck": "tsc --noEmit",
25
+ "prepack": "npm run clean && npm run build && npm run typecheck"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^20.11.0",
29
+ "tsup": "^8.0.0",
30
+ "typescript": "^5.3.0",
31
+ "vitest": "^3.0.0"
32
+ },
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ },
36
+ "license": "MIT"
37
+ }