@krizaka/storage

中文文档
0.11.1发布于 2026/11/01Apache-2.0TypeScriptES Module构建来源已验证

Files behind one port for every Krizaka product: put with a content-hashed name, get, head, delete, list, signed and public URLs; adapters over an injected fetch for an HTTP object store and any S3-compatible bucket (Signature V4, no SDK), and a local fol

原始使用文档

@krizaka/storage

Files behind one port for every Krizaka product. The port is named by what it does (ObjectStore), the provider is a value the admin chooses. No vendor SDK (plain HTTP through an injected fetch), no environment variable, no wall clock.

  • The port. put, get, head, delete, list(prefix), signedUrl(key, ttl) for a private zone, publicUrl(key) when a pull zone or CDN exists.
  • Adapters. remoteStore (an HTTP zone with the shape of Bunny Storage: AccessKey header, PUT/GET/DELETE, a directory listed by its path with a trailing slash, a pull zone with token-signed links), bucketStore (any S3-compatible service: Signature V4, presigned GET, ListObjectsV2, no SDK), folderStore (a folder, for development, tests and the Mac apps; a key never leaves the root), memoryObjectStore. All run objectStoreContract (@krizaka/storage/testkit).
  • Rules every upload obeys (createStorage): the size limit is passed in, the prefix must be declared in a defineLayout (an unknown prefix is refused), the bytes are checked by magic number (sniffType: png, jpeg, gif, webp, pdf, mp4, webm, svg), never by the extension, and a declared type that contradicts the bytes is refused. The stored name is content-hashed, <name>.<sha10>.<ext>, so a URL never changes content.
  • The admin chooses (@krizaka/storage/settings): storage.provider (local, http, s3), storage.maxMegabytes, storage.signedUrlSeconds, and the non-secret coordinates (endpoint, region, bucket, zone, prefix, pullHostname, publicBaseUrl). Keys stay in the environment; storeFromSettings takes them as parameters and refuses a provider chosen without what it needs, instead of falling back to another. An admin-typed S3 endpoint goes through checkEndpoint (https only, no credentials, no local or private address).
  • Encryption at rest, the vault, regions and where. encryptedStore(inner, key) (AES-256-GCM over any ObjectStore; the key is a parameter; the provider holds ciphertext; no link is handed out), seal/open (credentials in the database, v1. envelope), COUNTRY_OF_REGION (the one region table: an unlisted region is never a country; residencyOfConfig says « canada » only when the region and the endpoint agree, never for a Bunny-shaped zone), where(kind, active, { fixed, devOnly }) and storageNotice (the sentence key of the info panel, from the configuration).
  • Admin over environment (@krizaka/storage/resolve). resolveStorage(env, admin) serves every kind of data (documents, avatars: pass each its own DeploymentStorage): the admin's storage.* wins, the environment gives the secrets and the fallback when the admin left local. If the environment says none and the admin turns keeping on, it throws StorageError STORAGE_KEEPING_OFF (call it when saving the setting to refuse early): keeping is turned on in the environment, never by a setting alone. openResolved builds the store, encrypted when a key is given.
  • Provider registry (ADR-005). PROVIDER_KINDS is the table of kinds as data (bunny, s3 with presets DigitalOcean Spaces, AWS S3, OVHcloud and generic, dropbox, reserved google-drive/onedrive, folder, device): scopes, runtimes, devOnly, capabilities, coordinate and credential field schemas. openProvider(kind, coordinates, credentials, { runtime, environment, fetch, clock }) (/node) is the only factory: it refuses (STORAGE_BAD_CONFIG) devOnly kinds in prod, folder outside dev, an unknown or unimplemented kind and a missing field, and never falls back. It returns the store and a ResidencyLabel (PROVINCE_OF_REGION: « Canada (Québec) », « Canada (Ontario) »…) for the screen to show before saving. testConnection (probe write, read, compare, delete) and checkEndpointDns (the written form plus the DNS answer: a private address is refused) serve every product.
  • The registry opens the OpenDAL kinds and private kinds (ADR-005 amendment, krizaka-ts#239). google-drive and onedrive are implemented at user scope (through opendalStore and CLOUD_CONNECTORS: the access token is renewed from the sealed refresh token, a rotated one comes back through OpenContext.onRotate and is sealed again by the service); azblob, gcs, b2 and webdav at admin scope (their field names are OpenDAL's own options; a typed url field passes checkEndpoint and the DNS check). openProvider is now asynchronous. Each kind maps to its adapter in a table (BUILT_IN_ADAPTERS), never a branch. A private package adds a kind without touching the core: defaultProviderRegistry.with({ kind, open }) (or createProviderRegistry({ kinds, adapters })) returns a new registry; pass it to openProvider(…, registry) and createProviderService({ registry }). A duplicate name is refused, a secret may not be a coordinate, a credential must be a secret field (sealed), and the runtime, devOnly, required-field and url rules apply to private kinds as to the built-in ones.
  • Keyring is a port ({ keyId, wrap, unwrap, openLegacy? }). localKeyring({ keyId, key, ring }) is the in-process default (the old Keyring shape { keys, legacy } is now LocalKeys); sealWith, openWith and rewrapWith take any keyring, so a KMS (AWS KMS ca-central-1, OVHcloud KMS, Vault transit) wraps the data key and the key never reaches the app. createProviderService({ vault }) takes either the old { keyId, key, ring } or a Keyring. The stored v2.<key_id>… format is unchanged: moving to a KMS is rotateVaultKey, not a re-entry.
  • Envelope vault. sealEnvelope/openEnvelope/rewrapEnvelope: a data key per sealed value, wrapped by the key-encryption key (MEDIA_VAULT_KEY) named by a key_id (v2.<key_id>.<wrapped>.<sealed>); v1. values still open with the legacy key; rotating the key re-wraps the data keys only.
  • Per-tenant envelope encryption (ADR-020 Decision 3, for the managed document storage). KeyService is the KMS port (currentKeyId, wrap, unwrap bound to a { tenant } context, optional rotate); the root key never leaves the KMS. Adapters: memoryKeyService (tests and a developer's machine: versions, rotate, destroy) and cloudKeyService for aws-kms (the KMS JSON API signed with SigV4, the tenant in the encryption context, RotateKeyOnDemand) and okms (OVHcloud KMS service keys; the access certificate rides on the fetch the product passes in; rotation = a new key named in the settings, then the rewrap job), over an injected fetch, no SDK. tenantVault({ kms, keys, clock, audit }) keeps one KEK per tenant and rotation, wrapped in the database (TenantKeyStore: dbTenantKeyStore on storageSchema v2, memoryTenantKeyStore), and a fresh AES-256-GCM DEK per object version. tenantStore(inner, vault, tenantId) is an ObjectStore under t/<tenantId>/, the full key as associated data, so another tenant's or a renamed object never opens. rotateTenant + rewrapAll (the header only, never the bytes) + dropOldVersions; rewrapRoot after a root change; shredTenant destroys the keys (tombstone first, race-free) and every copy, replica and backups included, answers STORAGE_TENANT_SHREDDED. Every key event is an audit line. Contracts: keyServiceContract, tenantKeyStoreContract.
  • The managed provider (ADR-020 Decisions 2 and 7). managed is one more kind of the registry (admin scope): any S3-compatible bucket (presets OVHcloud with bhs and ca-east-tor, AWS S3, Google Cloud Storage in interoperability mode, DigitalOcean Spaces, generic for a local S3 server) plus a KMS (kms: okms, aws-kms, or local in development only, refused in production), every key a sealed credential the admin types once and never sees again. openProvider("managed", …, { managed: { keys, audit, mtlsFetch? } }) returns forTenant(tenantId) (an encrypted tenantStore under t/<tenantId>/) and a probe store, so the provider screen's « test the connection » proves bucket, KMS and key table end to end; managedResidency says « Canada (Québec); replica: Canada (Ontario) » from what the admin declared. The admin's controls (@krizaka/storage/settings): managedStorageSettings (on/off, allowed providers byo/managed/both, quota, longest link, Private folders, device cache, trash, retention bounds; global and per tenant, audited by @krizaka/settings), resolveManagedPolicy (tenant > plan > product > global; a self-contradicting policy is refused), providerAllowed (the one gate), linkSeconds, and MANAGED_PRICE_IDS (the catalogue entries; no amount typed). Switching a tenant's provider is moveBatch + readDual between any two stores, proven with two buckets on a real S3 server.
  • Providers in the database (@krizaka/storage/provider-admin, ADR-005 slice 2). storageSchema (tables storage.provider and storage.default_provider), dbProviderStore/memoryProviderStore (contract: providerStoreContract), and createProviderService: create (a draft, secrets sealed in an envelope), test (only a green test verifies), setDefault (verified only; the previous default becomes retiring), changeKey (the candidate is tested first), disable/retire (refused while the product still holds objects there), rotateVaultKey, bootstrap (imports the old MEDIA_* environment once as an active provider, source: env). Secrets come back as set/not set only; every change goes to the injected audit with secret fields as changed. moveBatch (get, check the SHA-256, put, read back and compare, then switch the row in the product transaction; failures are returned, never switched) and readDual (the row provider, then the retiring ones) are the move and the dual read.
  • Users' own clouds (ADR-005 slice 4). dropboxStore (dropbox kind, user scope): App folder only, upload sessions above 150 MB, no signed or public link (Dropbox links outlive our 900 s cap, so the application streams). pkcePair, dropboxAuthorizeUrl, exchangeDropboxCode, refreshDropboxToken, revokeDropbox are the OAuth 2 PKCE steps; the refresh token is sealed like any credential, and a revoked or expired one answers STORAGE_DISCONNECTED (nothing is moved or deleted). A user's own S3 is the same s3 kind at user:<id> scope, with the DNS check of its endpoint.
  • Every classic cloud through libraries (ADR-005 amendment, krizaka-ts#235). opendalStore (@krizaka/storage/opendal) is one adapter over Apache OpenDAL for Google Drive, OneDrive, Dropbox, Azure Blob, GCS, B2, Swift and WebDAV. Only the services of OPENDAL_SERVICES open; fs/memory need allowLocal; a typed endpoint passes checkEndpoint and the DNS check first. OpenDAL's errors map onto STORAGE_* (PermissionDenied on an OAuth cloud → STORAGE_DISCONNECTED), and readStream(key) streams large files. opendal is an optional peer (a native module of 59–82 MB, glibc ≥ 2.38 or musl), loaded on first use. CLOUD_CONNECTORS (/node) is the table of user clouds (Dropbox, Google Drive, OneDrive: endpoints, narrowest scope, revocation style). startConnection, finishConnection, refreshConnection, revokeConnection run Arctic's authorization code + PKCE S256, and connectionAccessToken feeds opendalStore a fresh access token and hands a rotated refresh token back to be resealed. dropboxStore and its helpers are superseded and stay until the products move.
  • Signatures. SigV4 is checked against AWS's published vectors. The token form of the HTTP zone's signed links (SHA-256 of key + path + expires, URL-safe base64) is the one Yomizaka runs against Bunny's pull zones; Bunny's documentation publishes no expected output for it.
import { createStorage, defineLayout, folderStore } from "@krizaka/storage";
import { limitsOf, readStorageSettings, storeFromSettings } from "@krizaka/storage/settings";

const settings = await readStorageSettings((key) => service.get(key, ctx));
const storage = createStorage({
  store: storeFromSettings(settings, { accessKeyId: env.MEDIA_STORAGE_ACCESS_KEY_ID, secretAccessKey: env.MEDIA_STORAGE_SECRET }, { fetch, clock }),
  layout: defineLayout(["people/", "site/home/"]),
  limits: () => readStorageSettings(read).then(limitsOf),
});
const { key, url } = await storage.upload({ prefix: "people/", name: "ana.jpg", bytes, allow: ["image/jpeg", "image/png"], declaredType: file.type });

Apache-2.0.

版本与发布记录

@krizaka/storage 共发布 14 个版本,首次发布于 2026/10/27;过去 12 个月发布了 14 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.11.1latest2026/11/011 天前
0.11.02026/10/312 天前
0.10.02026/10/313 天前
0.9.02026/10/313 天前
0.8.12026/10/304 天前
0.8.02026/10/295 天前

关于 @krizaka/storage 的常见问题

@krizaka/storage 是什么?

每个Krizaka产品的一个端口后面的文件:放入内容哈希的名称,get, head, delete, list,签名和公共url;为HTTP对象存储和任何s3兼容的桶(签名V4,没有SDK)以及本地文件夹进行注入获取的适配器

如何安装 @krizaka/storage?

在项目目录运行 npm install @krizaka/storage,或使用 pnpm add / yarn add / bun add。

@krizaka/storage 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@krizaka/storage 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@krizaka/storage 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@krizaka/storage 可以商用吗?

@krizaka/storage 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@krizaka/storage 还在维护吗?

最新版本 0.11.1 发布于 2026/11/01(1 天前);至今共发布 14 个版本,其中过去 12 个月发布了 14 个;首次发布于 2026/10/27。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。