陈仓的文字消息不经过任何服务器。语音、图片、视频先在手机上加密,再以密文形式经这里中转;App 内 24 小时后显示「已过期」,云端按 S3 生命周期约 1–2 天内自动删除。服务器拿不到任何密钥,也不记录 IP。下面就是线上运行的全部代码。
运营者声明的线上部署包 SHA-256:56fc10b0b7cc81c439f08753062c5171d8215cd320ff730685037c8fc98b0c9c。可核对方法:下载 lambda/index.mjs 与 lambda/sign.mjs,按 deploy.sh 第 1 步打包后计算 SHA-256,与此值以及 curl -sI "https://d3nnqcgewrb4f0.cloudfront.net/api/upload" | grep x-cc-source-sha256 的结果比对——一致说明这份公开源码与运营者的声明相符,但这是一条可核对的声明,不是密码学证明(Lambda 真正执行代码的哈希只有账号所有者能看到)。
// 陈仓媒体中转 · Lambda 入口(Function URL,经 CloudFront /api/upload 访问)。
import { validate, presign } from './sign.mjs';
const json = (statusCode, body) => ({
statusCode,
headers: {
'content-type': 'application/json',
'cache-control': 'no-store',
'x-cc-source-sha256': process.env.SOURCE_SHA256 ?? 'unknown',
},
body: JSON.stringify(body),
});
export async function handler(event) {
if (event?.requestContext?.http?.method !== 'GET') return json(405, { error: 'GET only' });
const v = validate(event.queryStringParameters ?? {});
if (!v.ok) return json(v.status, { error: v.error });
const url = await presign({ bucket: process.env.BUCKET, region: process.env.AWS_REGION, blobId: v.blobId, byteLen: v.byteLen });
return json(200, { url, expires_in: 300 });
}
// 陈仓媒体中转 · 上传签名。
// 服务器只签发「往 b/<blob_id> 放一个指定长度的新对象」的 5 分钟许可;
// 它看不到任何密钥,也不记录任何请求。全文公开在各分发的 /source 页。
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
export const LIMITS = { 1: 2097152, 2: 2097152, 3: 31457280 }; // 语音 / 图片 / 视频(.cca 总字节)
export const MIN_BYTE_LEN = 50; // 最小合法 .cca:34 字节头 + 16 字节 AEAD tag(= core payload::MEDIA_MIN_BLOB_LEN)
const BLOB_ID = /^[A-Za-z0-9_-]{22}$/;
const POSITIVE_INT = /^[1-9][0-9]{0,9}$/;
export function validate(q = {}) {
if (typeof q.blob_id !== 'string' || !BLOB_ID.test(q.blob_id)) return { ok: false, status: 400, error: 'bad blob_id' };
if (!['1', '2', '3'].includes(q.kind)) return { ok: false, status: 400, error: 'bad kind' };
if (typeof q.byte_len !== 'string' || !POSITIVE_INT.test(q.byte_len)) return { ok: false, status: 400, error: 'bad byte_len' };
const kind = Number(q.kind);
const byteLen = Number(q.byte_len);
if (byteLen < MIN_BYTE_LEN) return { ok: false, status: 400, error: 'too small' };
if (byteLen > LIMITS[kind]) return { ok: false, status: 413, error: 'too large' };
return { ok: true, blobId: q.blob_id, byteLen, kind };
}
export async function presign({ bucket, region, blobId, byteLen, credentials }) {
const client = new S3Client({ region, ...(credentials ? { credentials } : {}) });
const cmd = new PutObjectCommand({
Bucket: bucket,
Key: `b/${blobId}`,
ContentLength: byteLen,
ContentType: 'application/octet-stream',
IfNoneMatch: '*',
});
return getSignedUrl(client, cmd, {
expiresIn: 300,
signableHeaders: new Set(['content-length', 'content-type', 'if-none-match']),
unhoistableHeaders: new Set(['if-none-match']),
});
}
AWSTemplateFormatVersion: '2010-09-09'
Description: "chencang rich-media relay (bucket + upload signer). Source: see the source page served by the distributions (/source)"
Parameters:
CodeBucket: { Type: String }
CodeKey: { Type: String }
SourceSha256: { Type: String }
DistributionArns: { Type: CommaDelimitedList } # three CloudFront distributions (ARNs, comma separated)
Resources:
MediaBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: chencang-media
PublicAccessBlockConfiguration: { BlockPublicAcls: true, BlockPublicPolicy: true, IgnorePublicAcls: true, RestrictPublicBuckets: true }
OwnershipControls: { Rules: [ { ObjectOwnership: BucketOwnerEnforced } ] }
BucketEncryption: { ServerSideEncryptionConfiguration: [ { ServerSideEncryptionByDefault: { SSEAlgorithm: AES256 } } ] }
LifecycleConfiguration:
Rules:
- { Id: expire-1d, Status: Enabled, Prefix: b/, ExpirationInDays: 1, AbortIncompleteMultipartUpload: { DaysAfterInitiation: 1 } }
MediaBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref MediaBucket
PolicyDocument:
Statement:
- Effect: Allow
Principal: { Service: cloudfront.amazonaws.com }
Action: s3:GetObject
Resource: !Sub '${MediaBucket.Arn}/b/*'
Condition: { StringEquals: { AWS:SourceArn: !Ref DistributionArns } }
SignerRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement: [ { Effect: Allow, Principal: { Service: lambda.amazonaws.com }, Action: sts:AssumeRole } ]
Policies:
- PolicyName: put-media-only
PolicyDocument:
Statement: [ { Effect: Allow, Action: s3:PutObject, Resource: !Sub '${MediaBucket.Arn}/b/*' } ]
# 故意不挂 AWSLambdaBasicExecutionRole:函数没有写 CloudWatch 日志的权限,想记也记不了。
Signer:
Type: AWS::Lambda::Function
Properties:
FunctionName: chencang-media-signer
Runtime: nodejs22.x
Handler: index.handler
Role: !GetAtt SignerRole.Arn
MemorySize: 256
Timeout: 5
Code: { S3Bucket: !Ref CodeBucket, S3Key: !Ref CodeKey }
Environment: { Variables: { BUCKET: !Ref MediaBucket, SOURCE_SHA256: !Ref SourceSha256 } }
SignerUrl:
Type: AWS::Lambda::Url
Properties: { TargetFunctionArn: !Ref Signer, AuthType: AWS_IAM }
SignerUrlPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref Signer
Action: lambda:InvokeFunctionUrl
Principal: cloudfront.amazonaws.com
# SourceAccount instead of SourceArn: admits any CloudFront distribution in this account
# (needed so all three mirror distributions can invoke the signer without per-ARN permissions).
SourceAccount: !Ref AWS::AccountId
FunctionUrlAuthType: AWS_IAM
SignerInvokePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref Signer
Action: lambda:InvokeFunction
Principal: cloudfront.amazonaws.com
# SourceAccount instead of SourceArn: admits any CloudFront distribution in this account
# (needed so all three mirror distributions can invoke the signer without per-ARN permissions).
SourceAccount: !Ref AWS::AccountId
MediaOac:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig: { Name: chencang-media-s3, OriginAccessControlOriginType: s3, SigningBehavior: always, SigningProtocol: sigv4 }
SignerOac:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig: { Name: chencang-media-signer, OriginAccessControlOriginType: lambda, SigningBehavior: always, SigningProtocol: sigv4 }
# /b/* response headers: force every downloaded blob to be treated as an
# opaque, non-executable download — no matter what Content-Type the
# uploader's client claimed when it PUT the object (see F1: sign.mjs pins
# Content-Type into the presigned request, this policy pins it again on
# the way out so a stale/pre-fix object can't serve as text/html either).
MediaResponseHeaders:
Type: AWS::CloudFront::ResponseHeadersPolicy
Properties:
ResponseHeadersPolicyConfig:
Name: chencang-media-blob
Comment: "/b/* only: opaque octet-stream download, never renderable as HTML/JS"
SecurityHeadersConfig:
ContentTypeOptions: { Override: true }
ContentSecurityPolicy:
ContentSecurityPolicy: "sandbox; default-src 'none'"
Override: true
CustomHeadersConfig:
Items:
- { Header: Content-Type, Value: application/octet-stream, Override: true }
- { Header: Content-Disposition, Value: attachment, Override: true }
# /b/* cache TTL: short and fixed (not CachingOptimized's viewer-driven
# long TTL) — blob objects are one-shot and expire out of S3 in ~1-2 days
# anyway (see F3), so there is no reason to cache them for the default
# policy's up-to-a-year ceiling.
MediaCachePolicy:
Type: AWS::CloudFront::CachePolicy
Properties:
CachePolicyConfig:
Name: chencang-media-blob-cache
Comment: "/b/* only: fixed 1h TTL, no cookies/headers/query in the cache key"
DefaultTTL: 3600
MaxTTL: 3600
MinTTL: 0
ParametersInCacheKeyAndForwardedToOrigin:
CookiesConfig: { CookieBehavior: none }
HeadersConfig: { HeaderBehavior: none }
QueryStringsConfig: { QueryStringBehavior: none }
EnableAcceptEncodingGzip: false
EnableAcceptEncodingBrotli: false
Outputs:
BucketRegionalDomain: { Value: !GetAtt MediaBucket.RegionalDomainName }
SignerUrlDomain: { Value: !Select [ 2, !Split [ '/', !GetAtt SignerUrl.FunctionUrl ] ] }
MediaOacId: { Value: !GetAtt MediaOac.Id }
SignerOacId: { Value: !GetAtt SignerOac.Id }
MediaResponseHeadersPolicyId: { Value: !GetAtt MediaResponseHeaders.Id }
MediaCachePolicyId: { Value: !GetAtt MediaCachePolicy.Id }
AWSTemplateFormatVersion: '2010-09-09'
Description: "chencang media upload rate limit (CloudFront scope, us-east-1). Source: see /source on the distributions"
Resources:
Acl:
Type: AWS::WAFv2::WebACL
Properties:
Name: chencang-media-upload
Scope: CLOUDFRONT
DefaultAction: { Allow: {} }
VisibilityConfig: { SampledRequestsEnabled: false, CloudWatchMetricsEnabled: true, MetricName: chencang-media-upload }
Rules:
- Name: upload-rate-per-ip
Priority: 0
Action: { Block: { CustomResponse: { ResponseCode: 429 } } }
VisibilityConfig: { SampledRequestsEnabled: false, CloudWatchMetricsEnabled: true, MetricName: upload-rate-per-ip }
Statement:
RateBasedStatement:
Limit: 60
EvaluationWindowSec: 300
AggregateKeyType: IP
ScopeDownStatement:
ByteMatchStatement:
FieldToMatch: { UriPath: {} }
PositionalConstraint: STARTS_WITH
SearchString: /api/upload
TextTransformations: [ { Priority: 0, Type: NONE } ]
Outputs:
AclArn: { Value: !GetAtt Acl.Arn }
// CloudFront Function(viewer-request):落地页与源码页的静态路由。
function handler(event) {
var r = event.request;
if (r.uri === '/m' || r.uri.indexOf('/m/') === 0) { r.uri = '/m/index.html'; }
else if (r.uri === '/p' || r.uri.indexOf('/p/') === 0) { r.uri = '/p/index.html'; }
else if (r.uri === '/source' || r.uri === '/source/') { r.uri = '/source/index.html'; }
return r;
}
.DistributionConfig
| .WebACLId = $acl
| .Origins.Items = ([.Origins.Items[] | select(.Id != "s3-chencang-media" and .Id != "lambda-media-signer")] + [
{ Id: "s3-chencang-media", DomainName: $bd, OriginPath: "", CustomHeaders: {Quantity: 0},
S3OriginConfig: {OriginAccessIdentity: ""}, OriginAccessControlId: $mo,
ConnectionAttempts: 3, ConnectionTimeout: 10, OriginShield: {Enabled: false} },
{ Id: "lambda-media-signer", DomainName: $sd, OriginPath: "", CustomHeaders: {Quantity: 0},
CustomOriginConfig: {HTTPPort: 80, HTTPSPort: 443, OriginProtocolPolicy: "https-only",
OriginSslProtocols: {Quantity: 1, Items: ["TLSv1.2"]}, OriginReadTimeout: 10, OriginKeepaliveTimeout: 5},
OriginAccessControlId: $so, ConnectionAttempts: 3, ConnectionTimeout: 10, OriginShield: {Enabled: false} }
])
| .Origins.Quantity = (.Origins.Items | length)
| def beh($p; $origin; $cache; $methods; $fn; $rhp):
{ PathPattern: $p, TargetOriginId: $origin, ViewerProtocolPolicy: "redirect-to-https",
AllowedMethods: {Quantity: ($methods|length), Items: $methods, CachedMethods: {Quantity: 2, Items: ["GET","HEAD"]}},
Compress: false, SmoothStreaming: false, FieldLevelEncryptionId: "",
CachePolicyId: $cache,
ResponseHeadersPolicyId: $rhp,
OriginRequestPolicyId: (if $origin == "lambda-media-signer" then "b689b0a8-53d0-40ab-baf2-68738e2966ac" else null end),
TrustedSigners: {Enabled: false, Quantity: 0}, TrustedKeyGroups: {Enabled: false, Quantity: 0},
LambdaFunctionAssociations: {Quantity: 0},
FunctionAssociations: (if $fn then {Quantity: 1, Items: [{EventType: "viewer-request", FunctionARN: $fn}]} else {Quantity: 0} end) }
| with_entries(select(.value != null));
.CacheBehaviors.Items = ([(.CacheBehaviors.Items // [])[] | select(.PathPattern | IN("/b/*","/api/upload","/m/*","/p","/p/*","/source*") | not)] + [
beh("/api/upload"; "lambda-media-signer"; "4135ea2d-6df8-44a3-9df3-4b5a84be39ad"; ["GET","HEAD"]; null; null),
beh("/b/*"; "s3-chencang-media"; $cp; ["GET","HEAD"]; null; $rhp),
beh("/m/*"; "s3-chencang-site"; "658327ea-f89d-4fab-a63d-7e88639e58f6"; ["GET","HEAD"]; $fn; null),
beh("/p"; "s3-chencang-site"; "658327ea-f89d-4fab-a63d-7e88639e58f6"; ["GET","HEAD"]; $fn; null),
beh("/p/*"; "s3-chencang-site"; "658327ea-f89d-4fab-a63d-7e88639e58f6"; ["GET","HEAD"]; $fn; null),
beh("/source*"; "s3-chencang-site"; "4135ea2d-6df8-44a3-9df3-4b5a84be39ad"; ["GET","HEAD"]; $fn; null)
])
| .CacheBehaviors.Quantity = (.CacheBehaviors.Items | length)
#!/usr/bin/env bash
# 陈仓媒体中转部署。全文公开在各分发的 /source 页(如 https://d3nnqcgewrb4f0.cloudfront.net/source)。
set -euo pipefail
cd "$(dirname "$0")"
REGION=ap-northeast-1
# 三个同配置分发:H1(主)、H2、H3(镜像,见 create-mirror-distribution.sh)
DIST_IDS=(E2TRN129L7BYBK E3MDHL18OSE5DL E3P8OZYVOPB9EJ)
ACCOUNT=186180737711
DIST_ARNS=""; ARN_JSON="[]"
for id in "${DIST_IDS[@]}"; do
arn="arn:aws:cloudfront::${ACCOUNT}:distribution/${id}"
DIST_ARNS="${DIST_ARNS:+$DIST_ARNS,}$arn"
ARN_JSON=$(jq -c --arg a "$arn" '. + [$a]' <<<"$ARN_JSON")
done
CODE_BUCKET=chencang-site # 复用已有私有桶存放部署包,键在 _deploy/ 下(CloudFront 默认行为可读,内容本就公开)
# 1) 可复现打包:固定 mtime + 固定顺序 + 去掉额外属性
rm -rf build && mkdir -p build/pkg
cp lambda/index.mjs lambda/sign.mjs build/pkg/
TZ=UTC touch -t 198001010000 build/pkg/*
(cd build/pkg && TZ=UTC zip -X -D -q ../lambda.zip index.mjs sign.mjs)
SHA=$(shasum -a 256 build/lambda.zip | cut -d' ' -f1)
echo "$SHA" > build/SOURCE_SHA256
KEY="_deploy/media-signer-${SHA}.zip"
aws s3 cp build/lambda.zip "s3://${CODE_BUCKET}/${KEY}" --region "$REGION" >/dev/null
# 2) 主栈 + WAF 栈
aws cloudformation deploy --region "$REGION" --stack-name chencang-media --template-file template.yaml \
--capabilities CAPABILITY_IAM --no-fail-on-empty-changeset \
--parameter-overrides CodeBucket="$CODE_BUCKET" CodeKey="$KEY" SourceSha256="$SHA" DistributionArns="$DIST_ARNS"
aws cloudformation deploy --region us-east-1 --stack-name chencang-media-waf --template-file waf.yaml --no-fail-on-empty-changeset
out() { aws cloudformation describe-stacks --region "$1" --stack-name "$2" --query "Stacks[0].Outputs[?OutputKey=='$3'].OutputValue" --output text; }
BUCKET_DOM=$(out "$REGION" chencang-media BucketRegionalDomain)
SIGNER_DOM=$(out "$REGION" chencang-media SignerUrlDomain)
MEDIA_OAC=$(out "$REGION" chencang-media MediaOacId)
SIGNER_OAC=$(out "$REGION" chencang-media SignerOacId)
MEDIA_RHP=$(out "$REGION" chencang-media MediaResponseHeadersPolicyId)
MEDIA_CP=$(out "$REGION" chencang-media MediaCachePolicyId)
ACL_ARN=$(out us-east-1 chencang-media-waf AclArn)
# 3) CloudFront Function(发布 LIVE)
FN=chencang-site-router
if aws cloudfront describe-function --name "$FN" >/dev/null 2>&1; then
ETAG=$(aws cloudfront describe-function --name "$FN" --query ETag --output text)
ETAG=$(aws cloudfront update-function --name "$FN" --if-match "$ETAG" --function-config Comment=chencang,Runtime=cloudfront-js-2.0 --function-code fileb://cf-router.js --query ETag --output text)
else
ETAG=$(aws cloudfront create-function --name "$FN" --function-config Comment=chencang,Runtime=cloudfront-js-2.0 --function-code fileb://cf-router.js --query ETag --output text)
fi
aws cloudfront publish-function --name "$FN" --if-match "$ETAG" >/dev/null
FN_ARN=$(aws cloudfront describe-function --name "$FN" --stage LIVE --query FunctionSummary.FunctionMetadata.FunctionARN --output text)
# 4a) chencang-site 桶策略(不在模板里):幂等地让三个分发都能读,保留其他语句
SITE_POLICY=$(aws s3api get-bucket-policy --bucket "$CODE_BUCKET" --query Policy --output text)
jq --argjson arns "$ARN_JSON" '
.Statement |= map(if .Sid == "AllowCloudFrontServicePrincipalReadOnly"
then .Condition.StringEquals["AWS:SourceArn"] = $arns else . end)' <<<"$SITE_POLICY" > build/site-policy.json
jq -e '[.Statement[] | select(.Sid == "AllowCloudFrontServicePrincipalReadOnly")] | length == 1' build/site-policy.json >/dev/null \
|| { echo "site bucket policy lacks the AllowCloudFrontServicePrincipalReadOnly statement" >&2; exit 1; }
aws s3api put-bucket-policy --bucket "$CODE_BUCKET" --policy file://build/site-policy.json
# 4b) 幂等地给每个分发追加两个源站 + 六条行为 + WAF(默认行为、别名、证书不动)
for DIST_ID in "${DIST_IDS[@]}"; do
aws cloudfront get-distribution-config --id "$DIST_ID" > "build/dist-${DIST_ID}.json"
DETAG=$(jq -r .ETag "build/dist-${DIST_ID}.json")
jq --arg bd "$BUCKET_DOM" --arg sd "$SIGNER_DOM" --arg mo "$MEDIA_OAC" --arg so "$SIGNER_OAC" \
--arg fn "$FN_ARN" --arg acl "$ACL_ARN" --arg rhp "$MEDIA_RHP" --arg cp "$MEDIA_CP" \
-f dist-patch.jq "build/dist-${DIST_ID}.json" > "build/dist-new-${DIST_ID}.json"
aws cloudfront update-distribution --id "$DIST_ID" --if-match "$DETAG" --distribution-config "file://build/dist-new-${DIST_ID}.json" >/dev/null
done
for DIST_ID in "${DIST_IDS[@]}"; do aws cloudfront wait distribution-deployed --id "$DIST_ID"; done
# 4c) 三台一致性核对:规范化后 diff,非空即失败
NORM='.DistributionConfig | {
WebACLId,
Origins: [.Origins.Items[] | {Id, DomainName, OAC: .OriginAccessControlId}] | sort_by(.Id),
CacheBehaviors: [(.CacheBehaviors.Items // [])[] | {PathPattern, TargetOriginId, CachePolicyId, ResponseHeadersPolicyId, FunctionAssociations}] | sort_by(.PathPattern),
Default: .DefaultCacheBehavior.TargetOriginId }'
for DIST_ID in "${DIST_IDS[@]}"; do
aws cloudfront get-distribution-config --id "$DIST_ID" | jq -S "$NORM" > "build/norm-${DIST_ID}.json"
echo "== ${DIST_ID}"; cat "build/norm-${DIST_ID}.json"
done
for DIST_ID in "${DIST_IDS[@]:1}"; do
diff "build/norm-${DIST_IDS[0]}.json" "build/norm-${DIST_ID}.json" || { echo "DISTRIBUTION CONFIG DRIFT: ${DIST_IDS[0]} vs ${DIST_ID}" >&2; exit 1; }
done
echo "distributions identical"
# 5) 发布源码页与落地页
node gen-source-page.mjs
aws s3 sync build/source "s3://chencang-site/source" --delete --cache-control "no-cache" --region "$REGION"
aws s3 cp build/source/files "s3://chencang-site/source/files" --recursive --content-type "text/plain; charset=utf-8" --cache-control "no-cache" --region "$REGION"
aws s3 cp ../../site/m/index.html "s3://chencang-site/m/index.html" --content-type "text/html; charset=utf-8" --cache-control "max-age=300" --region "$REGION"
aws s3 cp ../../site/p/index.html "s3://chencang-site/p/index.html" --content-type "text/html; charset=utf-8" --cache-control "max-age=300" --region "$REGION"
echo "deployed; SOURCE_SHA256=$SHA"
#!/usr/bin/env bash
# 克隆主分发 E2TRN129L7BYBK 为一个镜像分发:源站、行为、策略、函数、WAF 全同,
# 只去掉自定义域名、改用 *.cloudfront.net 默认证书。用法:./create-mirror-distribution.sh <N>
# 建好后把新 ID 加进 deploy.sh 的 DIST_IDS,再跑 ./deploy.sh 让三台配置对齐。
set -euo pipefail
N=${1:?usage: create-mirror-distribution.sh <N>}
SRC=E2TRN129L7BYBK
TMP=$(mktemp)
trap 'rm -f "$TMP"' EXIT
aws cloudfront get-distribution-config --id "$SRC" --query DistributionConfig --output json \
| jq --arg ref "chencang-mirror-${N}-$(date +%s)" --arg c "chencang mirror ${N}" '
.CallerReference = $ref
| .Comment = $c
| .Aliases = {Quantity: 0}
| .ViewerCertificate = {CloudFrontDefaultCertificate: true, MinimumProtocolVersion: "TLSv1", CertificateSource: "cloudfront"}' \
> "$TMP"
aws cloudfront create-distribution --distribution-config "file://$TMP" \
--query 'Distribution.{Id:Id,Domain:DomainName,Status:Status}' --output table
// 生成 https://d3nnqcgewrb4f0.cloudfront.net/source :逐文件展示媒体中转的全部源码。
import { readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
const FILES = ['lambda/index.mjs', 'lambda/sign.mjs', 'template.yaml', 'waf.yaml', 'cf-router.js', 'dist-patch.jq', 'deploy.sh', 'create-mirror-distribution.sh', 'gen-source-page.mjs'];
const sha = readFileSync(join(here, 'build/SOURCE_SHA256'), 'utf8').trim();
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
const out = join(here, 'build/source');
mkdirSync(join(out, 'files/lambda'), { recursive: true });
const sections = FILES.map((f) => {
copyFileSync(join(here, f), join(out, 'files', f));
const body = readFileSync(join(here, f), 'utf8');
return `<section><h2 id="${f}">${f} <a href="/source/files/${f}" download>下载</a></h2><pre><code>${esc(body)}</code></pre></section>`;
}).join('\n');
const html = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>陈仓中转源码</title><style>
:root{--bg:#fff;--fg:#1d1d1f;--muted:#6e6e73;--code:#f5f5f7}
@media (prefers-color-scheme:dark){:root{--bg:#111;--fg:#f5f5f7;--muted:#a1a1a6;--code:#1c1c1e}}
body{margin:0;padding:16px;max-width:880px;margin-inline:auto;background:var(--bg);color:var(--fg);font:15px/1.6 -apple-system,system-ui,sans-serif}
pre{background:var(--code);padding:12px;overflow-x:auto;border-radius:8px;font-size:12px}h2{font-size:15px;margin-top:32px}p{color:var(--muted)}
</style></head><body>
<h1>陈仓媒体中转 · 全部源码</h1>
<p>陈仓的文字消息不经过任何服务器。语音、图片、视频先在手机上加密,再以密文形式经这里中转;App 内 24 小时后显示「已过期」,云端按 S3 生命周期约 1–2 天内自动删除。服务器拿不到任何密钥,也不记录 IP。下面就是线上运行的全部代码。</p>
<p>运营者声明的线上部署包 SHA-256:<code>${sha}</code>。可核对方法:下载 lambda/index.mjs 与 lambda/sign.mjs,按 deploy.sh 第 1 步打包后计算 SHA-256,与此值以及 <code>curl -sI "https://d3nnqcgewrb4f0.cloudfront.net/api/upload" | grep x-cc-source-sha256</code> 的结果比对——一致说明这份公开源码与运营者的声明相符,但这是一条可核对的声明,不是密码学证明(Lambda 真正执行代码的哈希只有账号所有者能看到)。</p>
${sections}
</body></html>`;
writeFileSync(join(out, 'index.html'), html);
console.log(`wrote ${join(out, 'index.html')}`);