@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:AccessKeyheader,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 runobjectStoreContract(@krizaka/storage/testkit). - Rules every upload obeys (
createStorage): the size limit is passed in, the prefix must be declared in adefineLayout(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;storeFromSettingstakes them as parameters and refuses a provider chosen without what it needs, instead of falling back to another. An admin-typed S3 endpoint goes throughcheckEndpoint(https only, no credentials, no local or private address). - Encryption at rest, the vault, regions and
where.encryptedStore(inner, key)(AES-256-GCM over anyObjectStore; 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;residencyOfConfigsays « canada » only when the region and the endpoint agree, never for a Bunny-shaped zone),where(kind, active, { fixed, devOnly })andstorageNotice(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 ownDeploymentStorage): the admin'sstorage.*wins, the environment gives the secrets and the fallback when the admin leftlocal. If the environment saysnoneand the admin turns keeping on, it throwsStorageErrorSTORAGE_KEEPING_OFF(call it when saving the setting to refuse early): keeping is turned on in the environment, never by a setting alone.openResolvedbuilds the store, encrypted when a key is given. - Provider registry (ADR-005).
PROVIDER_KINDSis the table of kinds as data (bunny,s3with presets DigitalOcean Spaces, AWS S3, OVHcloud and generic,dropbox, reservedgoogle-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)devOnlykinds inprod,folderoutsidedev, an unknown or unimplemented kind and a missing field, and never falls back. It returns the store and aResidencyLabel(PROVINCE_OF_REGION: « Canada (Québec) », « Canada (Ontario) »…) for the screen to show before saving.testConnection(probe write, read, compare, delete) andcheckEndpointDns(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-driveandonedriveare implemented at user scope (throughopendalStoreandCLOUD_CONNECTORS: the access token is renewed from the sealed refresh token, a rotated one comes back throughOpenContext.onRotateand is sealed again by the service);azblob,gcs,b2andwebdavat admin scope (their field names are OpenDAL's own options; a typedurlfield passescheckEndpointand the DNS check).openProvideris 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 })(orcreateProviderRegistry({ kinds, adapters })) returns a new registry; pass it toopenProvider(…, registry)andcreateProviderService({ registry }). A duplicate name is refused, a secret may not be a coordinate, a credential must be asecretfield (sealed), and the runtime,devOnly, required-field and url rules apply to private kinds as to the built-in ones. Keyringis a port ({ keyId, wrap, unwrap, openLegacy? }).localKeyring({ keyId, key, ring })is the in-process default (the oldKeyringshape{ keys, legacy }is nowLocalKeys);sealWith,openWithandrewrapWithtake 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 aKeyring. The storedv2.<key_id>…format is unchanged: moving to a KMS isrotateVaultKey, 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 akey_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).
KeyServiceis the KMS port (currentKeyId,wrap,unwrapbound to a{ tenant }context, optionalrotate); the root key never leaves the KMS. Adapters:memoryKeyService(tests and a developer's machine: versions,rotate,destroy) andcloudKeyServiceforaws-kms(the KMS JSON API signed with SigV4, the tenant in the encryption context,RotateKeyOnDemand) andokms(OVHcloud KMS service keys; the access certificate rides on thefetchthe product passes in; rotation = a new key named in the settings, then the rewrap job), over an injectedfetch, no SDK.tenantVault({ kms, keys, clock, audit })keeps one KEK per tenant and rotation, wrapped in the database (TenantKeyStore:dbTenantKeyStoreonstorageSchemav2,memoryTenantKeyStore), and a fresh AES-256-GCM DEK per object version.tenantStore(inner, vault, tenantId)is anObjectStoreundert/<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;rewrapRootafter a root change;shredTenantdestroys the keys (tombstone first, race-free) and every copy, replica and backups included, answersSTORAGE_TENANT_SHREDDED. Every key event is an audit line. Contracts:keyServiceContract,tenantKeyStoreContract. - The managed provider (ADR-020 Decisions 2 and 7).
managedis one more kind of the registry (admin scope): any S3-compatible bucket (presets OVHcloud withbhsandca-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, orlocalin development only, refused in production), every key a sealed credential the admin types once and never sees again.openProvider("managed", …, { managed: { keys, audit, mtlsFetch? } })returnsforTenant(tenantId)(an encryptedtenantStoreundert/<tenantId>/) and a probe store, so the provider screen's « test the connection » proves bucket, KMS and key table end to end;managedResidencysays « Canada (Québec); replica: Canada (Ontario) » from what the admin declared. The admin's controls (@krizaka/storage/settings):managedStorageSettings(on/off, allowed providersbyo/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, andMANAGED_PRICE_IDS(the catalogue entries; no amount typed). Switching a tenant's provider ismoveBatch+readDualbetween 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(tablesstorage.providerandstorage.default_provider),dbProviderStore/memoryProviderStore(contract:providerStoreContract), andcreateProviderService: create (adraft, secrets sealed in an envelope), test (only a green test verifies),setDefault(verified only; the previous default becomesretiring),changeKey(the candidate is tested first),disable/retire(refused while the product still holds objects there),rotateVaultKey,bootstrap(imports the oldMEDIA_*environment once as anactiveprovider,source: env). Secrets come back asset/not setonly; every change goes to the injectedauditwith secret fields aschanged.moveBatch(get, check the SHA-256, put, read back and compare, then switch the row in the product transaction; failures are returned, never switched) andreadDual(the row provider, then the retiring ones) are the move and the dual read. - Users' own clouds (ADR-005 slice 4).
dropboxStore(dropboxkind, 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,revokeDropboxare the OAuth 2 PKCE steps; the refresh token is sealed like any credential, and a revoked or expired one answersSTORAGE_DISCONNECTED(nothing is moved or deleted). A user's own S3 is the sames3kind atuser:<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 ofOPENDAL_SERVICESopen;fs/memoryneedallowLocal; a typed endpoint passescheckEndpointand the DNS check first. OpenDAL's errors map ontoSTORAGE_*(PermissionDeniedon an OAuth cloud →STORAGE_DISCONNECTED), andreadStream(key)streams large files.opendalis 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,revokeConnectionrun Arctic's authorization code + PKCE S256, andconnectionAccessTokenfeedsopendalStorea fresh access token and hands a rotated refresh token back to be resealed.dropboxStoreand 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.