ワンクリックで
awcms-micro-social-publishing
Kerjakan bagian mana pun dari epic social_publishing AWCMS-Micro (Issue
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Kerjakan bagian mana pun dari epic social_publishing AWCMS-Micro (Issue
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
| name | awcms-micro-social-publishing |
| description | Kerjakan bagian mana pun dari epic social_publishing AWCMS-Micro (Issue |
Epic social_publishing (#643-#647) menambah lapisan auto-posting
provider-neutral di atas blog_content (base module, sudah active) dan
news_portal (epic news_portal #631-#642/#649, sumber gambar R2
terverifikasi) — khusus deployment full-online yang mengaktifkan flag
SOCIAL_PUBLISHING_ENABLED/SOCIAL_PUBLISHING_PROFILE. Issue #643
(fondasi) selesai lebih dulu; #644 (Meta/Facebook+Instagram), #645
(LinkedIn), #646 (Telegram) masing-masing menambah SATU provider adapter
NYATA di atas fondasi ini; #647 (dokumentasi/SOP) butuh semua issue
sebelumnya sudah ada. Seluruh epic (#643-#647) kini selesai — lihat
§647 di bawah untuk lima dokumen yang ditambahkan issue penutup ini.
Skill ini melengkapi (bukan menggantikan) awcms-micro-new-endpoint,
awcms-micro-new-migration, awcms-micro-integration (pola outbox/circuit
breaker eksternal, ADR-0006), awcms-micro-idempotency (mutation
connect/disconnect/approve/cancel/retry), awcms-micro-abac-guard,
awcms-micro-audit-log, dan awcms-micro-sensitive-data (token
reference). Skill ini menyediakan konteks cross-cutting epic
spesifik — terutama keputusan "provider-neutral foundation dulu, tidak
ada adapter nyata sampai #644/#645/#646" yang wajib dipertahankan setiap
issue lanjutan.
| Issue | Scope | Status |
|---|---|---|
| #643 | Fondasi: schema 6 tabel, outbox/dispatcher, approval, retry/backoff, provider-adapter interface (kosong), admin UI, readiness | Selesai — lihat §643 di bawah |
| #644 | Adapter Meta (Facebook Page + Instagram Business) | Selesai — lihat §644 di bawah |
| #645 | Adapter LinkedIn (organization page) | Selesai — lihat §645 di bawah |
| #646 | Adapter Telegram (channel, bot token) | Selesai — lihat §646 di bawah |
| #647 | Dokumentasi/SOP lintas provider, butuh #643-#646 semua ada | Selesai — lihat §647 di bawah |
Urutan dependency (dari objective masing-masing issue): 643 -> {644, 645, 646 independen satu sama lain, masing-masing hanya butuh #643} -> 647 (butuh semuanya — seluruh prasyarat sudah terpenuhi, epic tuntas).
NEWS_PORTAL_ENABLEDSOCIAL_PUBLISHING_ENABLED (master switch) + SOCIAL_PUBLISHING_PROFILE
(wajib "full_online" bila enabled) — persis pola
AUTH_ONLINE_SECURITY_ENABLED/_PROFILE
(src/lib/auth/online-security-config.ts), BUKAN reuse
NEWS_PORTAL_ENABLED/_PROFILE milik epic news_portal (fitur
berbeda, keputusan deployment berbeda — tenant bisa full-online
news_portal tanpa pernah mau auto-posting sosial). Resolver:
src/modules/social-publishing/domain/social-publishing-config.ts's
isSocialPublishingDeploymentActive(env). Ditegakkan
config:validate (checkSocialPublishingProfileConfig,
scripts/validate-env.ts) dan security:readiness
(checkSocialPublishingProviderReadiness, critical,
scripts/security-readiness.ts).
Ini HANYA setengah dari acceptance criterion "Auto-posting can be
disabled globally and per tenant" — setengah lain (per-tenant) adalah
awcms_micro_social_publishing_settings (Keputusan kunci #2 di bawah).
awcms_micro_social_publishing_settings (tenant_id PK,
auto_publishing_enabled) adalah tabel keenam, di luar 5 "Core entities"
literal body issue #643. Sengaja tenant-writable lewat endpoint
ABAC-gated (GET/PATCH /api/v1/social-publishing/settings, permission
rules.configure) — ini BUKAN pengulangan anti-pattern Issue #636
(.claude/skills/awcms-micro-news-portal/SKILL.md §636): #636 butuh
sinyal yang TIDAK BOLEH tenant defeat sendiri (enforcement keamanan R2-
only). Toggle auto-posting per-tenant di sini justru MEMANG dimaksudkan
bisa diubah tenant sendiri (preferensi bisnis biasa, bukan kontrol
keamanan) — jadi tabel dedicated + RLS + ABAC endpoint biasa sudah
cukup, TIDAK perlu pola "zero generic write surface" #636. Jangan
disalahartikan sebagai kemunduran keamanan bila membaca kode ini setelah
membaca §636 — keduanya sengaja berbeda karena ancaman modelnya berbeda.
token_reference adalah REFERENSI, bukan token nyata; ada heuristic penolakanawcms_micro_social_accounts.token_reference adalah string buram (mis.
"secretsmanager:social/fb-page-42", "env:SOCIAL_TOKEN_X") yang
menunjuk ke secret storage eksternal — repo ini belum punya integrasi
secret-manager nyata (dicatat sebagai residual/follow-up, bukan
diselesaikan issue ini). social-account-validation.ts's
looksLikeRawSecretToken menolak (400) nilai yang BERBENTUK token asli
(JWT 3-segmen, prefix EAA/ya29./1///ghp_, token Bot API Telegram
<bot_id>:<35-char secret>, blob base64/hex panjang tanpa
prefix-referensi dikenal) — best-effort, BUKAN jaminan sempurna
(didokumentasikan eksplisit di komentar fungsi). token_reference
tidak pernah diselect kembali oleh query mana pun kecuali SATU fungsi
fetchSocialAccountTokenReferenceForDispatch (INTERNAL ONLY, dipanggil
dispatcher, tidak pernah dari route HTTP) — sama pola
tenant-domain-directory.ts's verification_token_hash. Disconnect
membersihkan token_reference ke NULL (bukan sekadar flip status).
Temuan security-auditor round 1 (PR #731, High, DITUTUP): cek
awal hanya punya 4 pola (JWT/EAA/ya29./gh[a-z]_) plus satu catch-all blob
64+ karakter yang mengecualikan SEMUA string berisi titik dua — token Bot
API Telegram asli (110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw, ~44
karakter) lolos: terlalu pendek untuk catch-all, dan bentuknya tidak cocok
4 pola lain. Ini gap nyata untuk provider BERIKUTNYA di epic ini (#646),
bukan hipotetis. Diperbaiki dengan (1) pola penolakan eksplisit
^\d{6,10}:[A-Za-z0-9_-]{30,45}$ sebelum pengecualian referensi, DAN (2)
KNOWN_SECRET_REFERENCE_PREFIX_PATTERN (allow-list prefix
secretsmanager/env/ref/vault/kms/ssm) menggantikan "string
apa pun berisi titik dua dikecualikan" — charset catch-all blob juga
diperluas mencakup : supaya pengecualian prefix ini benar-benar
teruji/reachable, bukan dead code. Wajib untuk #644/#645/#646: bila
provider kalian punya bentuk token yang juga bisa lolos dari 5 pola yang
ada sekarang, tambah pola penolakan eksplisit baru — jangan andalkan
catch-all blob generik saja untuk token yang PENDEK.
Wajib untuk #644/#645/#646: adapter nyata TIDAK boleh menyimpan
token/secret client-nya sendiri di kolom lain manapun — resolusi
tokenReference -> kredensial nyata adalah tanggung jawab adapter
sendiri (mis. baca env var bernama sesuai reference, atau panggil secret
manager beneran), bukan tanggung jawab fondasi ini.
provider_key BUKAN enum tetapdomain/social-provider-adapter.ts mendefinisikan SocialProviderAdapter
(providerKey, requiredEnvVars, publish(), verifyCredentials()).
infrastructure/social-provider-registry.ts adalah singleton Map yang
KOSONG di issue ini — TIDAK ADA satu pun panggilan HTTP nyata ke Meta/
LinkedIn/Telegram di seluruh modul ini. provider_key (kolom
awcms_micro_social_accounts/..._social_publish_jobs) sengaja HANYA
divalidasi FORMAT (^[a-z][a-z0-9_]{1,49}$), bukan CHECK enum tetap
seperti awcms_micro_news_portal_ad_placements.placement_key — supaya
#644/#645/#646 bisa mendaftarkan provider key baru tanpa migration baru.
Wajib untuk #644/#645/#646: panggil
registerSocialProviderAdapter(adapter) dari COMPOSITION ROOT milik
adapter itu sendiri (mis. sebuah file/script/module-init baru, BUKAN
dari dalam social-publishing/application/domain) — sama pola
setLogSink/setAuditExportHook's "registration adalah composition-
root concern". Gunakan providerKey yang konsisten dengan nama yang
tersirat body issue kalian sendiri (mis. facebook_page,
instagram_business, linkedin_organization, telegram_channel) —
tidak ada daftar resmi yang mengikat, tapi jaga konsistensi lintas ketiga
issue itu sendiri.
application/create-social-publish-jobs.ts's
createSocialPublishJobsForArticle HANYA melakukan INSERT baris job
(plain DB write, tanpa panggilan eksternal apa pun) — dipanggil DI DALAM
transaksi yang sama dengan transisi blog_content post ke published
(via SocialPublishingPort, lihat Keputusan kunci #6). Ini BENAR sesuai
pola outbox (menulis event/outbox row atomis dengan business event yang
memicunya) — bukan pelanggaran ADR-0006, karena ADR-0006 melarang
panggilan PROVIDER (bukan penulisan baris outbox) di dalam transaksi.
Panggilan provider NYATA hanya terjadi di
application/social-publish-dispatch.ts's dispatchSocialPublishQueue
(dipanggil bun run social-publishing:dispatch,
scripts/social-publish-dispatch.ts), 3-fase CLAIM/CALL/FINALIZE persis
sync-storage/application/object-dispatch.ts.
Wajib untuk #644/#645/#646: JANGAN pernah memanggil adapter.publish()
dari dalam kode yang juga menulis ke awcms_micro_blog_posts atau berjalan
di dalam transaksi manapun — hanya dispatcher yang boleh memanggilnya,
dan HARUS dibungkus withTimeout + circuit breaker per-provider
(getProviderCircuitBreaker(social-publishing:${providerKey})), sudah
disiapkan generik di social-publish-dispatch.ts — adapter baru TIDAK
perlu menambah circuit breaker sendiri.
SocialPublishingPort (blog_content konsumsi) dan NewsMediaPort (social_publishing konsumsi)Sama pola Issue #681 (_shared/ports/, news_media/public_content):
_shared/ports/social-publishing-port.ts (BARU, issue ini) adalah
capability yang blog_content KONSUMSI dari social_publishing
(onArticlePublished, dipanggil dari pages/api/v1/blog/posts/[id]/ publish.ts dan blog-content/application/blog-scheduled-publish.ts via
parameter socialPublishingPort opsional, diwire di
scripts/blog-scheduled-publish.ts). social_publishing sendiri
KONSUMSI news_portal's news_media capability (untuk resolusi URL
gambar R2 terverifikasi) — TIDAK mengimpor news-portal/application/ news-media-port-adapter.ts secara langsung dari dalam
social-publishing/application (itu justru anti-pattern yang sama
persis #681 perbaiki); factory
social-publishing-port-adapter.ts's createSocialPublishingPortAdapter(mediaPort)
menerima NewsMediaPort sebagai PARAMETER, hanya composition root
(route/script) yang mengimpor kedua adapter konkret dan merangkainya.
Catatan khusus: social-publishing-port-adapter.ts JUGA mengimpor
blog-content/application/public-route-settings.ts's
fetchEffectivePublicRouteSettings secara langsung (untuk
publicBasePath) — ini BUKAN pelanggaran boundary yang sama:
tests/unit/module-boundary.test.ts (Issue #681) HANYA mengatur pasangan
blog_content<->news_portal, tidak ada boundary setara antara
social_publishing dan blog_content hari ini. Arah ini jauh lebih
rendah risiko (satu fungsi getter read-only, bukan re-impor seluruh
domain modul lain) — didokumentasikan sengaja, bukan kelalaian.
url.originSetiap konstruksi canonical URL lain di repo ini (/news/[slug].ts,
sitemap-news.xml.ts) memakai url.origin dari REQUEST langsung — tidak
tersedia di sini karena job creation bisa dipicu dari worker terjadwal
tanpa request masuk sama sekali. application/article-canonical-url.ts's
resolvePrimaryVerifiedDomainHostname query LANGSUNG (raw SQL, tanpa
import TS) ke awcms_micro_tenant_domains (is_primary = true AND status = 'active') — pola yang SAMA blog-content/application/public-news- tenant-resolution.ts sudah pakai untuk tabel yang sama, jadi bukan
pelanggaran boundary baru. Bila tenant belum punya domain utama
terverifikasi, job creation SKIP dengan alasan terdokumentasi
(no_verified_domain) — TIDAK pernah menebak/fallback ke URL yang salah.
awcms_micro_social_publish_jobs_idempotency_key (UNIQUE
(tenant_id, idempotency_key)) + INSERT ... ON CONFLICT DO NOTHING —
idempotency_key dihitung deterministik dari
(tenantId, articleId, socialAccountId, providerKey, action) via
domain/social-publish-idempotency.ts's
buildSocialPublishIdempotencyKey. Ini terpisah dari _shared/ idempotency.ts's tabel HTTP Idempotency-Key generik (dipakai endpoint
connect/disconnect/approve/cancel/retry) — job creation dipicu event
internal, bukan request HTTP client, jadi butuh mekanisme idempotensi
sendiri di level baris DB.
PATCH /accounts/{id} (toggle auto-publish) digerbang rules.configure, BUKAN permission accounts.* baruDISENGAJA (temuan security-auditor M2, PR #731 review round 1): role yang
punya rules.configure tapi TIDAK punya accounts.connect/.disconnect
tetap bisa mengubah autoPublishEnabled sebuah akun yang bukan ia
hubungkan sendiri. Ini reuse dari 10 permission tetap yang sudah
disarankan issue #643 sendiri (lihat header migration 050) — bukan
menambah permission ke-11 (accounts.configure misalnya) hanya untuk satu
field boolean. Blast radius dibatasi pada "boleh menyalakan/mematikan
auto-publish akun yang SUDAH terhubung", tidak pernah menyentuh
kredensial/token (itu tetap di belakang accounts.connect/.disconnect).
Dicatat di sini SUPAYA terbaca sebagai tradeoff yang disadari, bukan
kelalaian, bila ditinjau ulang nanti — lihat juga komentar header
pages/api/v1/social-publishing/accounts/[id].ts's PATCH handler.
evaluateSocialPublishRetry/evaluateSocialPublishRateLimitRetryFormula sama sync-storage/domain/object-queue.ts's
evaluateObjectRetry (2^attemptCount menit, capped
SOCIAL_PUBLISH_MAX_RETRY_DELAY_MINUTES=240), TIDAK di-reuse langsung
(constants beda: job punya max_attempts per-baris, bukan konstanta
modul tetap). Rate-limit retry mempertimbangkan
retryAfterSeconds dari provider, tidak pernah lebih pendek dari lantai
eksponensial (mencegah provider memaksa retry loop rapat).
published: sukses, externalPostId/externalPostUrl terisi.pending/approved
(tergantung requires_approval snapshot) dengan next_attempt_at
backoff — TIDAK pernah balik ke requires_approval (approval tidak
perlu diulang).failed (terminal, TIDAK bisa retry manual lagi —
retrySocialPublishJob menolak attempt_count >= max_attempts).rate_limited: backoff dari retryAfterSeconds, tetap bisa habis
budget -> failed terminal juga.needs_reauth: TIDAK auto-retry, DAN awcms_micro_social_accounts
milik job itu ikut di-flip ke needs_reauth
(markSocialAccountNeedsReauth) — reconnect via POST .../accounts
(upsert) adalah SATU-SATUNYA jalur reauthorization, tidak ada endpoint
"reauthorize" terpisah.getSocialProviderAdapter return
undefined): failed terminal SEGERA (errorCode
provider_not_registered, retryable: false) — tidak masuk siklus
retry sama sekali (menambah adapter tidak akan pernah membuat retry
otomatis berhasil sebelum ada yang mem-retry manual).sql/053_awcms_micro_social_publishing_schema.sql (6 tabel + 10
permission seed).src/modules/identity-access/domain/access-control.ts (connect/
disconnect ditambah ke AccessAction + HIGH_RISK_ACTIONS).src/modules/social-publishing/domain/: social-publishing-config.ts,
social-provider-adapter.ts, social-publish-retry.ts,
social-publish-idempotency.ts, social-account-validation.ts,
social-publish-rule-validation.ts,
social-publish-template-validation.ts.src/modules/social-publishing/application/:
article-canonical-url.ts, social-account-directory.ts,
social-publish-rule-directory.ts,
social-publish-template-directory.ts,
social-publishing-settings-directory.ts,
create-social-publish-jobs.ts, social-publish-job-directory.ts,
social-publish-dispatch.ts, social-publishing-port-adapter.ts.src/modules/social-publishing/infrastructure/social-provider-registry.ts.src/modules/social-publishing/module.ts; registered in
src/modules/index.ts.src/modules/_shared/ports/social-publishing-port.ts (baru);
src/modules/blog-content/module.ts (capabilities.consumes +
social_publishing, optional).src/modules/blog-content/application/blog-scheduled-publish.ts
(parameter socialPublishingPort opsional).src/pages/api/v1/blog/posts/[id]/publish.ts (composition root,
memanggil socialPublishingPort.onArticlePublished).scripts/blog-scheduled-publish.ts (composition root, wiring port).src/pages/api/v1/social-publishing/{accounts,rules,templates,jobs,settings}/**.scripts/social-publish-dispatch.ts
(bun run social-publishing:dispatch).src/pages/admin/social-publishing/{accounts,rules,jobs}.astro.openapi/modules/social-publishing.openapi.yaml.asyncapi/awcms-micro-domain-events.asyncapi.yaml (15 channel/operation
baru, awcms-micro.social-publishing.*).scripts/validate-env.ts (checkSocialPublishingProfileConfig),
scripts/security-readiness.ts
(checkSocialPublishingProviderReadiness), src/lib/config/registry.ts,
.env.example, 18_configuration_env_reference.md.i18n/en.po, i18n/id.po, i18n/messages.pot (69 key baru,
admin.social_publishing.*/admin.layout.nav_social_publishing_*).tests/unit/social-publishing-config.test.ts,
tests/unit/social-publish-retry.test.ts,
tests/unit/social-publish-idempotency.test.ts,
tests/unit/social-account-validation.test.ts,
tests/unit/social-publish-template-validation.test.ts,
tests/modules/social-publishing-module.test.ts,
tests/integration/social-publishing.integration.test.ts; diperbarui:
tests/foundation.test.ts (migration list 050)..changeset/social-publishing-outbox-foundation-issue-643.md.token_reference ->
kredensial — hari ini murni konvensi/heuristic, bukan kode yang benar-
benar memanggil secret storage.manual_editor_action (dimodelkan penuh di awcms_micro_social_publish_rules.trigger_event,
tapi belum ada tombol "Post to X now" di editor artikel) — issue
lanjutan yang menambah UI itu harus tetap memakai
createSocialPublishJobsForArticle/SocialPublishingPort yang sudah
ada, bukan jalur baru.needs_reauth begitu akun reconnect — hari ini job
yang sudah needs_reauth harus di-retry manual via POST .../jobs/{id}/retry setelah akun reconnect, tidak otomatis.awcms_micro_social_publish_templates sebagai
halaman terpisah — digabung ke halaman rules (/admin/social- publishing/rules), bukan halaman sendiri (sama pola "cukup di satu
halaman config" beberapa modul lain di repo ini).GET /api/v1/social-publishing/jobs —
hari ini bounded LIMIT sederhana (maks 200), didokumentasikan sebagai
follow-up bila volume job jadi besar.Adapter provider NYATA pertama di epic ini. Registrasi TIDAK BERSYARAT
(selalu dipanggil saat social-provider-registry.ts di-import — lihat
Keputusan kunci di bawah), independen dari META_PROVIDER_ENABLED (yang
hanya menggerbang PERILAKU adapter saat dipanggil, bukan apakah ia
terdaftar).
meta_facebook_page (Facebook Page, link post ke /{page-id}/feed) dan
meta_instagram (Instagram Business, 2-call media container -> publish
ke /{ig-user-id}/media lalu .../media_publish) adalah DUA adapter
terpisah, masing-masing providerKey sendiri. TIDAK ada satu row
awcms_micro_social_accounts yang mewakili "koneksi Meta" gabungan —
tenant menghubungkan SATU row per tujuan publish (satu untuk Page, satu
lagi untuk IG bila keduanya diinginkan), providerAccountId untuk
meta_facebook_page adalah Facebook Page ID, untuk meta_instagram
adalah Instagram Business Account ID. Sengaja TIDAK menambah migration
baru untuk field metadata "account.metadata" yang disebut body issue
(facebook_page_id, facebook_page_name, instagram_business_account_id,
instagram_username, permissions_json, token_expires_at,
last_verified_at) — SEMUA field itu sudah punya padanan 1:1 di kolom
generik #643 (provider_account_id/_name untuk dua field pertama tiap
providerKey, scopes_json untuk permissions_json, expires_at/
last_verified_at untuk dua field terakhir). Jangan menambah kolom
metadata baru untuk ini kecuali benar-benar menemukan kebutuhan yang
TIDAK bisa dipetakan ke skema #643 yang sudah ada.
provider_account_type untuk KEDUA provider key ini selalu "page" —
lihat SocialProviderAdapter.supportedAccountTypes di bawah untuk
alasannya (IG Business tetap dipublish lewat Page access token, tidak
ada tipe akun IG standalone di real Meta API).
SocialProviderAdapter.supportedAccountTypes (field BARU, opsional, additive) ditegakkan di TIGA titik, bukan cuma satuInterface domain/social-provider-adapter.ts (foundation #643) tidak
punya cara bagi pemanggil untuk tahu tipe akun apa yang didukung suatu
adapter. Ditambah field opsional supportedAccountTypes?: readonly SocialAccountType[] — undefined berarti "tidak ada
pembatasan tipe" (bukan "tidak ada tipe yang didukung").
Temuan reviewer round 1 (BLOCKING, PR #644 diperbaiki sebelum merge):
versi pertama HANYA memeriksa field ini di endpoint verify (opt-in,
diagnostik) — jalur nyata connect -> dispatch -> publish yang benar-benar
memposting ke Meta TIDAK PERNAH memeriksanya. Operator bisa connect
providerKey: "meta_facebook_page" dengan providerAccountType: "profile" dan itu SUKSES; dispatcher akan tetap memanggil
adapter.publish(). Ini bertentangan langsung dengan acceptance
criterion issue ("Instagram publishing validates account eligibility ...
before job execution") dan daftar out-of-scope-nya (tidak ada personal
profile/personal Instagram posting).
Diperbaiki di DUA lapis, bukan satu:
application/social-publish-dispatch.ts — SETELAH
fetchSocialAccountTokenReferenceForDispatch (sekarang juga
mengembalikan providerAccountType) dan SEBELUM adapter.publish()
dipanggil, cek adapter.supportedAccountTypes — job gagal terminal
unsupported_account_type, retryable: false (tipe akun tidak pernah
berubah sendiri, harus reconnect manual). Ini satu-satunya titik yang
benar-benar menutup celah acceptance criterion di ATAS, karena INI yang
dilewati SETIAP job nyata, bukan hanya yang pernah di-verify.pages/api/v1/social-publishing/accounts/index.ts's POST (connect) —
defense-in-depth kedua, menolak 422 SOCIAL_ACCOUNT_UNSUPPORTED_TYPE
di titik paling awal (feedback langsung ke operator, bukan menunggu job
gagal nanti).Field ini JUGA dibaca scripts/security-readiness.ts's
checkMetaSocialPublishingAccountReadiness (readiness, bukan enforcement
runtime). Bila #645 juga menambah field yang sama (kemungkinan besar,
LinkedIn punya batasan tipe akun serupa) — field itu sendiri ADDITIVE
murni, tapi wajib tegakkan di dispatcher JUGA, bukan cuma endpoint
verify/connect — itulah pelajaran nyata dari temuan round 1 ini.
token_reference LOKAL per-adapter, skema env:VAR_NAME SATU-SATUNYA yang diimplementasikaninfrastructure/meta/meta-token-reference-resolver.ts's
resolveMetaTokenReference HANYA mendukung skema env:VAR_NAME (baca
process.env[VAR_NAME]) — skema lain yang lolos validasi FORMAT di
looksLikeRawSecretToken (secretsmanager:, vault:, kms:, ssm:)
diterima sebagai bentuk REFERENSI yang sah tapi TIDAK bisa diresolusi
deployment ini (return null, fail closed -> needs_reauth, bukan
throw). Ini SENGAJA tidak dipromosikan ke file bersama meskipun logikanya
generik — Keputusan kunci #3 §643 eksplisit bilang resolusi token adalah
tanggung jawab MASING-MASING adapter. Wajib untuk #645/#646: bila
kalian butuh resolusi serupa, salin pola ini (fungsi lokal per-modul),
JANGAN mengimpor fungsi ini dari modul Meta — itu akan membuat
social_publishing bergantung pada implementasi provider tertentu.
Fungsi yang SAMA dipakai untuk META_APP_SECRET_REFERENCE (dibutuhkan
untuk membangun app access token {appId}|{appSecret} saat memanggil
debug_token).
POST .../accounts/{id}/verify dibangun di PR #644 lalu DIGANTI oleh desain kanonik #646 (tabrakan paralel, diselesaikan oleh orkestrator)PR #644 (Meta) dan PR #646 (Telegram) SAMA-SAMA membangun
POST /api/v1/social-publishing/accounts/{id}/verify secara independen
dan paralel — keduanya lolos review sendiri-sendiri, tapi begitu #646
merge duluan ke main, orkestrator memutuskan desain #646 sebagai
KANONIK (permission accounts.verify khusus — bukan reuse
accounts.connect; wajib Idempotency-Key; 200 informasional yang
TIDAK PERNAH memaksa transisi state pada kegagalan — bukan 409
needs_reauth seperti desain awal #644) dan #644's implementasi sendiri
(application/social-account-verification.ts,
fetchSocialAccountTokenReferenceForVerification,
recordSocialAccountVerificationSuccess, route verify.ts versi awal)
DIHAPUS SELURUHNYA saat #644 merge origin/main yang sudah membawa
#646. Lihat §646 di bawah untuk desain kanonik yang sebenarnya jalan.
Pelajaran untuk #645 (LinkedIn, masih berjalan paralel): JANGAN
membangun ulang endpoint verify sendiri — route ini SEKARANG generik/
shared, sudah menangani provider apa pun via getSocialProviderAdapter.
Yang perlu #645 sentuh HANYA: implementasi verifyCredentials() di
adapter LinkedIn sendiri (dipanggil generik oleh route yang sudah ada),
dan environment/readiness check khusus LinkedIn — bukan route HTTP-nya.
Satu hal yang TETAP relevan dari desain awal #644 dan masih dipertahankan
di versi kanonik: verifyCredentials's docstring (#643) sudah eksplisit
bilang ini dipanggil dari "readiness gate atau manual verify connection
admin action" dan endpoint kanonik tetap memanggil provider strictly DI
LUAR transaksi DB (3-fase fetch/call/persist) — jadi disiplin desain
awal #644 ITU SENDIRI bukan yang salah, hanya detail response shape/
permission/idempotency-nya yang kalah terhadap #646.
mailketing-provider.tsinfrastructure/meta/meta-graph-client.ts's createMetaGraphClient
menerima fetchImpl/baseUrl opsional (default fetch global +
https://graph.facebook.com) — TEPAT pola yang sudah ada
email/infrastructure/mailketing-provider.ts's MailketingProviderConfig
pakai (bukan pola baru). Kedua adapter (meta-facebook-page-adapter.ts,
meta-instagram-adapter.ts) menerima graphClientFactory opsional di
constructor-nya sendiri (createMetaFacebookPageAdapter(options)) —
test mengganti factory ini dengan fake yang mengimplementasikan
MetaGraphClient.call(), TIDAK PERNAH melakukan panggilan fetch asli.
Wajib untuk #645/#646: pakai pola yang SAMA (client/provider object
injectable via constructor option adapter kalian sendiri) — jangan
memanggil fetch/library HTTP langsung dari dalam publish()/
verifyCredentials() tanpa lapisan yang bisa diganti saat test.
domain/meta-error-normalization.ts's normalizeMetaGraphApiError
memetakan error.code/error.type Meta ke katalog TETAP pesan aman
(meta_oauth_exception_190, meta_permission_error_10,
meta_rate_limited_32, dst.) — error.message/fbtrace_id Meta yang
asli TIDAK PERNAH masuk ke errorMessage/log/response manapun, walau
teks itu biasanya aman (issue security notes eksplisit minta "jangan log
identifier pengguna/halaman di luar yang dibutuhkan"). Wajib untuk
#645/#646: bila platform kalian juga mengembalikan pesan error
berformat bebas, JANGAN meneruskannya verbatim — buat katalog pesan
tetap sendiri seperti ini.
domain/meta-publish-content.ts's isAcceptableProviderMediaUrl
membandingkan new URL(url).host PERSIS terhadap
new URL(env.NEWS_MEDIA_R2_PUBLIC_BASE_URL).host — BUKAN
substring/prefix check (pelajaran Issue #635: trailing-dot FQDN bisa
membypass prefix check). Ini murni defense-in-depth: content.imageUrl
pada job SUDAH dijamin verified oleh create-social-publish-jobs.ts
(foundation #643) via NewsMediaPort.resolveMediaReferences — adapter
ini TIDAK pernah menerima URL gambar dari sumber lain (caption custom
editor hanya berupa TEKS, bukan URL). Re-check ini hanya jaring pengaman
titik-terakhir sebelum panggilan eksternal, bukan mekanisme enforcement
baru — jangan disalahartikan sebagai pengulangan pola tenant-state Issue
#636 (itu soal SIAPA yang boleh menulis sinyal keamanan; ini soal
validasi ulang nilai yang sudah dipercaya sebelum keluar sistem).
idempotencyKey, dedup NYATA tetap di dispatcherMeta Graph API TIDAK punya parameter idempotency-key nyata untuk
/feed//media//media_publish — adapter ini TIDAK mengimplementasikan
dedup sendiri. Dicatat eksplisit di test
(tests/unit/meta-instagram-adapter.test.ts's "idempotency" describe
block): memanggil publish() dua kali dengan idempotencyKey yang sama
tetap menghasilkan dua panggilan Graph API independen — pencegahan
duplikat NYATA adalah transisi status job (pending/approved ->
publishing -> published, tidak pernah diklaim ulang) yang SUDAH ada
di social-publish-dispatch.ts (foundation #643, teruji di
tests/integration/social-publishing.integration.test.ts dan
tests/unit/social-publish-idempotency.test.ts). Wajib untuk
#645/#646: jangan berpura-pura mengimplementasikan idempotency-level-
adapter kalau platform kalian juga tidak punya mekanisme itu — dokumentasikan
residual ini secara jujur seperti di sini.
Endpoint connect/disconnect generik #643 (POST/POST .../accounts,
.../accounts/{id}/disconnect) dipakai APA ADANYA untuk Meta — tidak
ada endpoint/redirect OAuth baru di issue ini. Operator menyelesaikan
alur OAuth Meta di luar aplikasi (atau proses operasional lain yang
menghasilkan Page Access Token jangka panjang), lalu mengisi form
connect manual dengan providerAccountId/Name/tokenReference.
META_OAUTH_REDIRECT_URI didokumentasikan untuk keperluan pendaftaran
app review/dashboard Meta saja — BUKAN endpoint yang benar-benar ada di
repo ini. POST .../accounts/{id}/verify (BARU, endpoint provider-
netral tapi baru benar-benar melakukan sesuatu untuk Meta hari ini)
memanggil debug_token secara live untuk memeriksa validitas/scope/
kedaluwarsa.
src/modules/social-publishing/domain/: meta-provider-config.ts,
meta-publish-content.ts, meta-error-normalization.ts;
social-provider-adapter.ts (+supportedAccountTypes opsional,
+param providerAccountId di verifyCredentials — perubahan
TERAKHIR ini datang dari #646, bukan #644, lihat §646).src/modules/social-publishing/infrastructure/meta/:
meta-graph-client.ts, meta-token-reference-resolver.ts,
meta-credential-verification.ts (verifyMetaCredentials sekarang
JUGA memanggil GET /{providerAccountId}?fields=id pakai token yang
diperiksa, mengonfirmasi token benar-benar bisa akses target
spesifik — bukan cuma valid secara umum), meta-facebook-page-adapter.ts,
meta-instagram-adapter.ts.src/modules/social-publishing/infrastructure/social-provider-registry.ts
(blok registrasi additive di akhir file — TIDAK bentrok dengan #646,
yang pakai pola registrasi berbeda, lihat §646 Keputusan #x).src/modules/social-publishing/application/social-account-directory.ts
(+providerAccountType di fetchSocialAccountTokenReferenceForDispatch
— dipakai social-publish-dispatch.ts's enforcement baru, Keputusan
kunci #644-2).src/modules/social-publishing/application/social-publish-dispatch.ts
(+enforcement supportedAccountTypes sebelum adapter.publish() —
Keputusan kunci #644-2, BLOKING finding reviewer round 1).src/pages/api/v1/social-publishing/accounts/index.ts's POST
(+enforcement supportedAccountTypes di connect time, defense-in-depth
kedua, 422 SOCIAL_ACCOUNT_UNSUPPORTED_TYPE).application/social-account-verification.ts,
route verify.ts versi awal #644,
tests/integration/social-publishing-meta-adapter.integration.test.ts
(seluruhnya tentang route verify yang sudah diganti).src/pages/admin/social-publishing/accounts.astro — tombol "Verify
connection" versi #644 juga DIHAPUS, digantikan tombol/script versi
#646 (permission accounts.verify, wajib Idempotency-Key).scripts/validate-env.ts (checkMetaSocialPublishingProviderConfig),
scripts/security-readiness.ts
(checkMetaSocialPublishingAccountReadiness),
src/lib/config/registry.ts, .env.example,
18_configuration_env_reference.md.src/lib/i18n/error-messages.ts (+SOCIAL_ACCOUNT_UNSUPPORTED_TYPE
saja — SOCIAL_ACCOUNT_NEEDS_REAUTH/PROVIDER_NOT_REGISTERED versi
awal #644 dihapus lagi bersama route verify awalnya, sudah tidak
dipakai di mana pun).openapi/modules/social-publishing.openapi.yaml (.../verify versi
KANONIK #646, bukan versi awal #644; +422 di POST .../accounts
untuk connect-time enforcement), asyncapi/... (+account.verified
dari #644, +account.verification-failed dari #646), module.ts
(+2 event, deskripsi diperbarui menyebut kedua adapter).i18n/en.po, i18n/id.po, i18n/messages.pot — hasil akhir gabungan
string #644+#646 (versi UI/pesan #646 yang menang untuk key yang
sama).tests/unit/meta-provider-config.test.ts,
tests/unit/meta-publish-content.test.ts,
tests/unit/meta-error-normalization.test.ts,
tests/unit/meta-token-reference-resolver.test.ts,
tests/unit/meta-facebook-page-adapter.test.ts (+verifyCredentials
test dengan param providerAccountId baru),
tests/unit/meta-instagram-adapter.test.ts (+verifyCredentials
describe block baru); diperbarui:
tests/modules/social-publishing-module.test.ts (17 event, bukan
16 — §646 juga menambah account.verification-failed),
tests/integration/social-publishing.integration.test.ts (+2 test
Keputusan kunci #644-2: dispatcher menolak tipe tidak didukung SEBELUM
publish() dipanggil; connect menolak 422 untuk kombinasi providerKey/
providerAccountType yang tidak didukung — plus SATU fix ketidaksengajaan
di test #646 yang sudah ada: resetSocialProviderRegistryForTests()
tidak pernah dipulihkan setelah test-nya sendiri, membocorkan registry
KOSONG ke SEMUA test berikutnya dalam file yang sama karena bun test
menjalankan semua file test dalam satu proses bersama — sekarang
dibungkus try/finally yang meregistrasi ulang ketiga adapter)..changeset/social-publishing-meta-adapter-issue-644.md.accounts.verify,
independen dari keputusan ini).env:VAR_NAME tetap satu-satunya
skema token_reference/META_APP_SECRET_REFERENCE yang benar-benar
bisa diresolusi.needs_reauth setelah reconnect — masih warisan
#643, belum berubah.Provider provider_key: "linkedin_organization" — adapter NYATA pertama di
modul ini (src/modules/social-publishing/infrastructure/ linkedin-provider-adapter.ts). providerAccountId diasumsikan SUDAH berupa
full URN (urn:li:organization:{id}), sesuai nama field organization_urn
di body issue — adapter ini tidak pernah mem-parsing/membangun URN sendiri.
Empat temuan, semua diperbaiki sebelum merge:
resolveLinkedInSecretReference (linkedin-provider-config.ts)
me-re-validasi nilai HASIL RESOLVE terhadap looksLikeRawSecretToken
(bukan hanya reference-nya SEBELUM diresolusi). Bug ini FATAL, bukan
sekadar redundan: token akses LinkedIn asli (150-1000+ karakter opaque)
PERSIS berbentuk blob high-entropy 64+ karakter yang memang dirancang
ditolak heuristic itu — versi lama menolak SETIAP resolusi token asli
sebagai "unresolvable", membuat publish()/verifyCredentials()
TIDAK PERNAH bisa berhasil untuk akun yang benar-benar terkonfigurasi.
Test suite sendiri tidak menangkap ini karena TEST_TOKEN fixture
sengaja pendek (~35 char), di bawah ambang 64 karakter. Diperbaiki:
hapus pengecekan kedua pada nilai hasil resolve — heuristic HANYA
berjalan pada reference string mentah (caller-supplied), sama pola
adapter Meta (resolveMetaTokenReference, PR sibling #644) yang hanya
cek bentuk env: + nilai hasil resolve non-empty. Regression test:
tests/unit/linkedin-provider-config.test.ts's dua test token
realistis (>64 char, salah satu >200 char) — JANGAN biarkan hanya
fixture token pendek jadi satu-satunya kasus "resolusi berhasil" yang
diuji.linkedin-provider-adapter.ts
memanggil redact(truncate(message, 500), token) — urutan TERBALIK.
redact() hanya cocok pada kemunculan LENGKAP token via
.split(token); bila token mentah terpotong tepat di titik 500
karakter, HANYA fragmen token yang tersisa (tidak sama dengan token
penuh), sehingga .split() gagal cocok dan fragmen itu (terbukti:
belasan karakter awal token asli) tersimpan APA ADANYA ke
awcms_micro_social_publish_jobs.last_error_message/
..._social_publish_attempts.error_message — keduanya admin-readable.
Diperbaiki: balik urutan jadi truncate(redact(message, token), 500)
di ketiga titik (organizationAcls http_error, post-creation http_error,
exception catch). Regression test WAJIB memposisikan token agar
BENAR-BENAR straddle batas 500 karakter (bukan body pendek yang tidak
pernah menyentuh titik potong) — lihat
tests/unit/linkedin-provider-adapter.test.ts's test "never leaks a
partial token fragment when the token straddles the truncation
boundary", yang menghitung margin secara eksplisit dan mem-verifikasi
(guaranteedLeakLengthUnderOldBug >= FRAGMENT_CHECK_LENGTH) bahwa
fixture-nya BENAR-benar akan gagal terhadap urutan lama sebelum
mengklaim fix-nya benar.LINKEDIN_CLIENT_SECRET_REFERENCE diklaim (changeset, doc
18, deskripsi registry.ts) divalidasi looksLikeRawSecretToken,
padahal findMissingOrInvalidLinkedInConfig hanya cek presence, tidak
pernah benar-benar memanggil heuristic itu untuk var ini. Diperbaiki
dengan MEWUJUDKAN klaim tersebut (menambah pengecekan bentuk langsung
di findMissingOrInvalidLinkedInConfig, reuse looksLikeRawSecretToken
pada reference string-nya, TANPA melalui resolveLinkedInSecretReference
karena var ini tidak pernah benar-benar diresolusi oleh kode apa pun) —
bukan melemahkan dokumentasi, karena pengecekan murah dan menangkap
kesalahan operator nyata (menempel client secret asli di var ini).providerKey yang ditambahkan issue ini terpotong
di tengah kalimat pada bundle OpenAPI/api-reference.md hasil generate
(berhenti persis di "...(Issue"). Akar masalah: # (spasi lalu hash)
di dalam PLAIN SCALAR YAML tanpa tanda kutip memulai KOMENTAR YAML —
parser yaml package memang benar secara spek, bukan bug bundler.
"(Issue #645, ..." punya spasi sebelum #645; pola AMAN yang sudah
dipakai di baris yang SAMA adalah "(#644/#645/#646)" (hash langsung
menempel tanda kurung buka, tanpa spasi). Diperbaiki dengan mengikuti
pola aman itu: "(#645, LinkedIn organization pages; ...)". Catatan
untuk seluruh repo: pola " #NNN" (spasi + hash + angka) di manapun
dalam string YAML PLAIN SCALAR tanpa tanda kutip berisiko silently
truncate — belum diaudit lintas file lain di openapi//asyncapi/,
dicatat sebagai temuan baru untuk kewaspadaan, bukan diperbaiki di luar
scope issue ini.Tiga agen mengerjakan #644 (Meta), #645 (LinkedIn ini), #646 (Telegram) BERSAMAAN di worktree terpisah. Titik sentuh bersama:
social-provider-registry.ts — HANYA disentuh secara aditif: tidak ada
panggilan registerSocialProviderAdapter DI DALAM file ini (tetap
kosong sesuai desain #643), tidak direstrukturisasi.scripts/social-publish-dispatch.ts dan scripts/security-readiness.ts
— masing-masing mendapat SATU import + SATU baris pemanggilan fungsi
registrasi milik provider ini (registerLinkedInProviderAdapterIfEnabled)
di dalam main(). #644/#646 diharapkan menambah pola yang SAMA (import +
panggilan fungsi registrasi MEREKA sendiri) — bukan mengubah baris punya
provider lain. Lihat komentar di titik pemanggilan masing-masing script
untuk konvensi ini.src/lib/config/registry.ts, .env.example, doc 18, scripts/ validate-env.ts's runEnvValidation, scripts/security-readiness.ts's
runSecurityReadinessChecks — setiap provider menambah entri/baris
SENDIRI, tidak menyentuh baris provider lain.SKILL.md ini sendiri (tabel status + section baru per provider) dan
docs/awcms-micro/repo-inventory.md/i18n hasil generate — pola merge-
conflict standar epic ini (lihat
[[news-portal-social-publishing-epic-progress]]): resolusi dengan
MENGGABUNGKAN kedua sisi, jangan pilih salah satu.Tidak ada migration baru untuk issue ini (lihat alasan desain di bawah) — mengurangi satu titik konflik lagi dibanding perkiraan awal.
Field "Account metadata" di body issue (organization_urn,
organization_name, member_role, permissions_json, token_expires_at,
last_verified_at) dipetakan SELURUHNYA ke kolom generik yang sudah ada di
awcms_micro_social_accounts (Issue #643) TANPA kolom baru:
organization_urn -> provider_account_id (sudah ada).organization_name -> provider_account_name (sudah ada).token_expires_at -> expires_at (sudah ada).last_verified_at -> sudah ada, tidak disentuh field baru.member_role dan permissions_json SENGAJA TIDAK DIPERSISTEN —
role organisasi LinkedIn seorang member bisa berubah/dicabut di sisi
LinkedIn tanpa notifikasi ke aplikasi ini; menyimpan snapshot lama bisa
memberi rasa aman palsu. Adapter ini mengecek role LIVE (dipanggil
organizationAcls, di-mock di test) pada SETIAP percobaan publish
(publish()) — bukan hanya sekali saat connect — persis menegakkan
requirement "require supported permission and organization role" issue
ini secara literal. verifyCredentials() juga melakukan pengecekan
scope (dari scopesJson, parameter yang SUDAH ada di interface) dan
validitas token (panggilan live ke /v2/userinfo, endpoint OpenID
Connect LinkedIn), terpisah dari pengecekan role (yang butuh URN
organisasi — parameter yang TIDAK ada di signature verifyCredentials,
sehingga jadi tanggung jawab publish(), bukan verifyCredentials()).Berbeda dari google-oauth-client.ts (redirect flow nyata, ada route
/callback), adapter ini TIDAK membangun redirect OAuth LinkedIn. Dua
alasan: (1) token_reference tidak boleh pernah berupa token mentah
(looksLikeRawSecretToken menolaknya) — sebuah callback OAuth nyata akan
menerima token asli dari LinkedIn, dan repo ini belum punya integrasi
secret-manager nyata untuk mengubahnya jadi referensi aman; (2) alur
connect fondasi (POST /api/v1/social-publishing/accounts) sudah generik
dan manual/operator-driven untuk SEMUA provider — LinkedIn tidak
dikecualikan. LINKEDIN_CLIENT_ID/LINKEDIN_CLIENT_SECRET_REFERENCE/
LINKEDIN_OAUTH_REDIRECT_URI tetap konfigurasi NYATA dan wajib
(divalidasi config:validate/security:readiness) — mendeskripsikan
LinkedIn App yang didaftarkan operator secara manual di LinkedIn Developer
portal (syarat app-review LinkedIn), bukan dipakai untuk redirect nyata di
kode ini. Detail penuh di linkedin-provider-config.ts's header comment.
looksLikeRawSecretToken — TIDAK ada heuristic baruSesuai instruksi security issue ini: resolveLinkedInSecretReference
(linkedin-provider-config.ts) memanggil looksLikeRawSecretToken DARI
social-account-validation.ts secara verbatim (bukan menduplikasi
heuristic-nya) untuk memvalidasi BAIK LINKEDIN_CLIENT_SECRET_REFERENCE
maupun setiap token_reference akun sebelum diresolusi. Resolusi sendiri
hanya memahami konvensi env:VAR_NAME (satu-satunya yang benar-benar bisa
diresolusi tanpa integrasi secret-manager nyata) — prefix lain
(secretsmanager:/vault:/dst.) lolos pengecekan bentuk tapi dilaporkan
"unresolvable", jujur soal keterbatasan repo ini.
Setiap panggilan HTTP ke LinkedIn (checkOrganizationRole,
uploadOrganizationImage, pembuatan post, verifyCredentials) mengirim
LinkedIn-Version (dari LINKEDIN_API_VERSION, format "YYYYMM", divalidasi
isValidLinkedInApiVersion) dan X-Restli-Protocol-Version: 2.0.0
(konstanta tetap, bukan config — ini versi protokol wire Rest.li, beda
konsep dari versi API).
content.imageUrl (sudah dijamin berasal dari objek R2 terverifikasi oleh
create-social-publish-jobs.ts's NewsMediaPort.resolveMediaReferences)
diperiksa ULANG (defense-in-depth, isTrustedR2MediaUrl, membandingkan
terhadap NEWS_MEDIA_R2_PUBLIC_BASE_URL — import lintas modul yang
sengaja dan sempit, sama pola Keputusan kunci #6's "Catatan khusus" di
atas) sebelum adapter melakukan alur upload gambar LinkedIn asli
(initializeUpload -> fetch bytes -> PUT) dan memposting sebagai
content.media. Gambar tidak-terpercaya/tidak ada, atau kegagalan APA PUN
selama upload, terdegradasi dengan baik ke post link-share
(content.article, source: canonicalUrl) — gambar bersifat non-esensial
dan tidak boleh pernah memblokir publish yang sah.
idempotencyKey (dari job.id, sudah dijamin idempoten di level DB oleh
Keputusan kunci #8) diteruskan sebagai header X-Idempotency-Key ke
LinkedIn (best-effort — LinkedIn tidak mendokumentasikan mekanisme
idempotency resmi untuk Posts API sejauh yang diketahui; jaminan idempotensi
NYATA tetap dari mekanisme outbox #643 sendiri: job yang sudah published
tidak pernah di-dispatch ulang). Setiap pesan error yang mungkin
menyertakan token diredaksi via substring-replacement literal (redact(),
bukan heuristic bentuk) menggunakan token bearer yang SUDAH diketahui
persis dalam scope panggilan tersebut — lebih andal daripada heuristic
karena nilai rahasianya sudah pasti diketahui, bukan ditebak.
verify_linkedin_connection (salah satu dari 3 "Supported initial
actions" di body issue) diimplementasikan penuh sebagai fungsi
verifyCredentials() (diuji langsung via unit test), TAPI TIDAK digerbang
ke endpoint HTTP baru — acceptance criteria issue ini tidak pernah meminta
tenant admin bisa MEMICU verifikasi secara manual (hanya connect/disconnect
yang eksplisit diminta, sudah dipenuhi endpoint generik yang ada sejak
#643). Mengurangi satu titik registrasi tambahan (SSR request path) yang
kalau tidak akan butuh wiring registrasi adapter ke proses server SSR juga
selain 2 script yang sudah ada — dicatat sebagai keputusan scope yang
disengaja, bukan kelalaian, bila endpoint ini dibutuhkan issue lanjutan.
src/modules/social-publishing/domain/linkedin-provider-config.ts
(baru).src/modules/social-publishing/infrastructure/linkedin-provider-adapter.ts
(baru) — createLinkedInProviderAdapter,
registerLinkedInProviderAdapterIfEnabled, isTrustedR2MediaUrl.scripts/social-publish-dispatch.ts,
scripts/security-readiness.ts (registrasi + checkLinkedInProviderReadiness),
scripts/validate-env.ts (checkLinkedInProviderConfig).src/lib/config/registry.ts, .env.example, doc 18 (6 var baru
LINKEDIN_*).openapi/modules/social-publishing.openapi.yaml (contoh
linkedin_organization di skema akun, bukan endpoint baru — tidak ada
endpoint HTTP baru diperkenalkan issue ini).tests/unit/linkedin-provider-config.test.ts,
tests/unit/linkedin-provider-adapter.test.ts..changeset/social-publishing-linkedin-adapter-issue-645.md.Adapter provider NYATA pertama di epic ini. provider_key
telegram_channel. Registrasi via
infrastructure/telegram-provider-registration.ts (side-effect import,
composition root — TIDAK mengubah isi social-provider-registry.ts sama
sekali, hanya memanggil registerSocialProviderAdapter yang sudah
diekspor).
verifyCredentials interface diperluas dengan providerAccountIdInterface #643 (domain/social-provider-adapter.ts) awalnya
verifyCredentials(tokenReference, scopesJson, env?) — tidak cukup untuk
Telegram (dan kemungkinan besar Meta/LinkedIn juga): sebuah bot token bisa
VALID tapi tidak punya akses ke CHANNEL SPESIFIK yang mau diverifikasi.
Diperluas jadi verifyCredentials(tokenReference, providerAccountId, scopesJson, env?) — perubahan aman/tidak breaking karena TIDAK ADA satu
pun caller nyata sebelum issue ini (foundation #643 sengaja nol adapter
nyata). SocialProviderCredentialCheck juga ditambah field opsional
details?: Record<string, unknown> (provider-specific display info,
mis. botUsername/permissions Telegram) — additive, tidak breaking.
Perhatian untuk #644/#645: bila kalian sudah mulai dari snapshot
sebelum PR ini merge, rebase dan sesuaikan signature verifyCredentials
kalian sendiri ke bentuk baru ini.
POST /accounts/{id}/verify BARU, provider-neutral, bukan Telegram-khususFoundation #643 sendiri sudah mengantisipasi ini di komentar
verifyCredentials ("a manual 'verify connection' admin action") tapi
belum ada endpoint HTTP-nya. Ditambahkan di sini
(pages/api/v1/social-publishing/accounts/[id]/verify.ts) sebagai
kapabilitas GENERIK — memanggil adapter.verifyCredentials(...) milik
provider apa pun yang terdaftar, bukan route Telegram-spesifik. Permission
baru social_publishing.accounts.verify (migration 054) — reuse action
verify yang SUDAH ADA di AccessAction union (identity-access/domain/ access-control.ts, dari tenant_domain.domains.verify migration 032),
BUKAN action baru. Tidak masuk HIGH_RISK_ACTIONS (sama alasan
domains.verify: hanya mengubah lastVerifiedAt/scopes_json, tidak
pernah tokenReference) TAPI tetap wajib Idempotency-Key (real outbound
call ke provider, sama kelas risiko accounts.connect/.disconnect).
Endpoint ini 3-fase (CLAIM-like) meniru persis pola dispatcher
social-publish-dispatch.ts: (1) transaksi — authorize + idempotency
check + fetch account/credentials; (2) DI LUAR transaksi — panggilan
provider nyata (adapter.verifyCredentials); (3) transaksi — catat hasil
withTenant manapun.Verifikasi GAGAL tetap 200 { valid: false, reason } — bukan error HTTP,
dan TIDAK mengubah connectionStatus/autoPublishEnabled (informational,
supaya admin bisa perbaiki izin channel lalu coba lagi). Hanya percobaan
publish NYATA via dispatcher yang bisa memicu needs_reauth (mekanisme
#643 yang sudah ada, terpisah).
Verifikasi TIDAK di-hardgate ke endpoint connect/enable auto-publish
yang sudah ada (POST /accounts, PATCH /accounts/{id}) — akan
mengubah perilaku SEMUA provider (bukan cuma Telegram) dan berisiko
merusak test #643 yang sudah ada (connect-lalu-langsung-enable tanpa
verify). Sebagai gantinya, "verifies bot can post to channel before
enabling auto posting" ditegakkan sebagai READINESS SIGNAL
(checkTelegramProviderReadiness, §4 di bawah) — operator harus verify
manual dulu sebelum go-live, bukan gate runtime yang memblokir API.
domain/telegram-message-formatting.ts. Default TELEGRAM_DEFAULT_PARSE_MODE
unset → TIDAK PERNAH mengirim parse_mode sama sekali → Telegram
memperlakukan SELURUH teks sebagai literal, nol kemungkinan interpretasi
formatting apa pun (title/excerpt user-authored boleh berisi tanda bintang
ganda **, garis bawah _..._, atau notasi tautan Markdown berkurung
siku-lalu-kurung — semuanya tetap literal). Bila operator secara eksplisit
set MarkdownV2/HTML (legacy Markdown SENGAJA tidak didukung), setiap
field yang diinterpolasi (title, excerpt, canonical URL) di-escape via
escapeTelegramMarkdownV2/escapeTelegramHtml — satu pass regex
tunggal atas string asli, BUKAN beberapa .replace() berurutan. Ini
penting: escaper MarkdownV2 memasukkan karakter backslash itu sendiri ke
dalam character class yang di-escape (bukan cuma _*[]()~\>#+-=|{}.!) — kalau tidak, backslash asli dalam input bisa "menguncinya" dengan backslash yang BARU kita sisipkan sehingga karakter sesudahnya lolos ter-escape (persis pola bug mdescape-backslash-bug-recurs` yang sudah 3x
muncul di repo lain — lihat memory pribadi terkait). TIDAK PERNAH
membangun tautan inline bergaya Markdown dari data pengguna — URL kanonik
selalu baris teks polos yang di-escape, dibiarkan Telegram auto-link-detect
sendiri; ini menghilangkan seluruh permukaan "constructed unexpected
inline link" yang disebut security notes issue.
Hashtag dari tag artikel (buildTelegramHashtags) diimplementasikan dan
diuji standalone TAPI belum dipakai nyata — snapshot job outbox
(awcms_micro_social_publish_jobs, migration 053) tidak punya kolom nama
tag sama sekali; menambahkannya berarti mengubah snapshot generik lintas
provider, di luar scope atomic issue adapter ini. publish() memanggil
buildTelegramMessageText(content, [], parseMode) — array hashtag selalu
kosong hari ini, didokumentasikan sebagai follow-up.
response.urlBot API Telegram menaruh token di PATH URL (.../bot<TOKEN>/<method>) —
tidak ada alternatif transport lain dari Telegram sendiri. Mitigasi di
infrastructure/telegram-provider-adapter.ts:
callTelegramApi),
dipakai untuk SATU panggilan fetch(), tidak pernah di-log/dikembalikan.response.url (properti bawaan fetch(), merefleksikan URL akhir
termasuk token) TIDAK PERNAH dibaca di file ini — jebakan nyata yang
gampang lolos code review biasa.error.message hasil fetch()/timeout TIDAK PERNAH
diinterpolasi mentah ke return value — hanya description/error_code
hasil PARSING JSON respons Telegram sendiri (aman, Telegram tidak pernah
echo token di body error) yang dipakai untuk pesan error/audit.checkTelegramProviderReadinessscripts/security-readiness.ts, critical, no-op saat
TELEGRAM_PROVIDER_ENABLED bukan "true" — independen dari
SOCIAL_PUBLISHING_ENABLED/checkSocialPublishingProviderReadiness
(deployment bisa full-online untuk Meta/LinkedIn tanpa pernah menyalakan
Telegram). Saat enabled, gagal bila ada akun telegram_channel
connected dengan autoPublishEnabled=true yang lastVerifiedAt IS NULL — sinyal operasional untuk "Adapter verifies bot can post to
channel before enabling auto posting" (lihat Keputusan #2 di atas kenapa
ini bukan hard gate runtime). checkTelegramProviderConfig
(scripts/validate-env.ts) memvalidasi
TELEGRAM_BOT_TOKEN_SECRET_REFERENCE (reuse looksLikeRawSecretToken —
JANGAN buat heuristic baru, lihat riwayat 3-ronde PR #731),
TELEGRAM_DEFAULT_PARSE_MODE, TELEGRAM_REQUEST_TIMEOUT_MS.
Bot harus ditambahkan sebagai administrator channel target dengan izin
"Post Messages" (can_post_messages). verifyCredentials memanggil
getMe (identitas bot) lalu getChatMember (status bot di channel
target) — gagal dengan reason missing_channel_permission bila status
bukan administrator/creator, atau missing_post_permission bila
administrator tapi can_post_messages: false.
sql/055_awcms_micro_social_publishing_verify_permission.sql (satu
permission accounts.verify).src/modules/social-publishing/domain/social-provider-adapter.ts
(verifyCredentials +providerAccountId, SocialProviderCredentialCheck +details).src/modules/social-publishing/domain/telegram-config.ts,
telegram-message-formatting.ts (baru).src/modules/social-publishing/infrastructure/telegram-provider-adapter.ts,
telegram-provider-registration.ts (baru).src/modules/social-publishing/application/social-account-directory.ts
(fetchSocialAccountCredentialsForVerification,
recordSocialAccountVerification).src/pages/api/v1/social-publishing/accounts/[id]/verify.ts (baru).src/modules/social-publishing/module.ts (accounts.verify permission +
2 event publishes baru).scripts/social-publish-dispatch.ts, scripts/security-readiness.ts
(side-effect import registrasi adapter).scripts/validate-env.ts (checkTelegramProviderConfig),
scripts/security-readiness.ts (checkTelegramProviderReadiness),
src/lib/config/registry.ts, .env.example,
18_configuration_env_reference.md.openapi/modules/social-publishing.openapi.yaml (POST .../accounts/{id}/verify + SocialAccountVerifyResult).asyncapi/awcms-micro-domain-events.asyncapi.yaml
(account.verified/account.verification-failed).src/pages/admin/social-publishing/accounts.astro (tombol Verify +
tampilan lastVerifiedAt).i18n/en.po, i18n/id.po, i18n/messages.pot (key baru
admin.social_publishing.accounts.{field_last_verified,not_verified, verify_button,verify_success,verify_failed_prefix}).tests/unit/telegram-message-formatting.test.ts,
tests/unit/telegram-config.test.ts,
tests/unit/telegram-provider-adapter.test.ts (Bun.serve() fake
api.telegram.org — TIDAK PERNAH panggilan jaringan nyata); diperbarui
tests/integration/social-publishing.integration.test.ts (blok "account
verify (Issue #646)", fake adapter ter-registrasi, TIDAK pernah menguji
jalur HTTP Telegram nyata di level ini)..changeset/social-publishing-telegram-adapter-issue-646.md.buildTelegramHashtags), belum
dipakai nyata (snapshot job tidak punya kolom tag).sendPhoto/preview gambar R2 — issue sendiri eksplisit izinkan
"initial scope can use safe link post through sendMessage".resolveTelegramBotToken hanya
mendukung indirection env:VAR_NAME) — residual yang sama dari #643.Issue dokumentasi murni — tidak ada kode/migration/endpoint baru. PR #756
(merged 2026-07-13) menambah lima dokumen baru di
docs/awcms-micro/news-portal/ plus pembaruan index
docs/awcms-micro/README.md, menutup epic social_publishing (#643-#647)
seluruhnya dengan dokumentasi arsitektur, operasional, batasan provider,
dan keamanan yang sebelumnya hanya tersebar di komentar kode/skill ini.
social-sharing.md — mendokumentasikan fitur social sharing
manual (Issue #642, tombol share milik PEMBACA, tanpa kredensial atau
panggilan API eksternal) dan secara eksplisit membedakannya dari
social publishing / auto-posting — tabel perbandingan §1 mencegah
pembaca mengira keduanya sistem yang sama (modul, kredensial, dan
persistence-nya sepenuhnya berbeda).social-publishing-architecture.md — arsitektur sistem auto-posting
itu sendiri: gerbang dua-flag SOCIAL_PUBLISHING_ENABLED/
SOCIAL_PUBLISHING_PROFILE (§1), model data 6 tabel (§2), dan alur
outbox/dispatcher/approval/retry — ringkasan arsitektur yang melengkapi
(bukan menggantikan) keputusan kunci #643-#646 yang sudah didetailkan
di skill ini.social-publishing-sop.md — panduan operator/redaksi (bukan
panduan kode): prasyarat sebelum mengaktifkan auto-posting, checklist
setup per provider (Meta/LinkedIn/Telegram) termasuk langkah OAuth di
luar aplikasi, dan permission yang dibutuhkan tiap peran.social-provider-limitations.md — batasan NYATA yang benar-benar
terimplementasi per provider (bukan rencana/aspirasi): tipe akun yang
didukung/ditolak, jenis post, ketiadaan idempotency-key native di Graph
API, pemetaan pesan error, dan skema token_reference yang benar-benar
bisa diresolusi (env:VAR_NAME saja).social-publishing-security-checklist.md — checklist keamanan dan
incident response: token storage (referensi, bukan token nyata), scope
query yang boleh mengembalikan token_reference, dan penegasan bahwa
seluruh nilai contoh di dokumen ini sengaja placeholder palsu (mis.
"env:META_APP_SECRET_EXAMPLE") — jangan pernah menempelkan kredensial
nyata di dokumentasi/tiket/log.docs/awcms-micro/news-portal/social-sharing.md (baru).docs/awcms-micro/news-portal/social-publishing-architecture.md (baru).docs/awcms-micro/news-portal/social-publishing-sop.md (baru).docs/awcms-micro/news-portal/social-provider-limitations.md (baru).docs/awcms-micro/news-portal/social-publishing-security-checklist.md
(baru).docs/awcms-micro/README.md (index, tautan ke lima dokumen di atas).Epic social_publishing (#643-#647) sekarang selesai seluruhnya —
lihat §Status per issue di atas.
Triase dan perbaiki temuan CodeQL code scanning AWCMS-Micro (github.com/ahliweb/awcms-micro/security/code-scanning). Gunakan saat diminta "analisis code scanning"/"perbaiki CodeQL", saat sebuah PR gagal check CodeQL, atau saat menemukan alert baru. Mendokumentasikan enam false-positive nyata yang sudah ditemukan (name-heuristic password, incompatible-types typeof/null, URL substring-sanitization di test mock, dua kasus dismiss resmi tanpa reformulasi kode, Bun.SQL tagged-template null-cast, dan build-time extension seam trivial-conditional) plus pola "unused-local-variable di test kadang menandai coverage gap" — supaya tidak diinvestigasi ulang dari nol.
Scaffold modul baru pada modular monolith AWCMS-Micro. Gunakan saat membuat modul domain baru di src/modules/ (mis. warehouse-management, accounting-tax) atau saat memerlukan struktur module.ts + domain/application/infrastructure/api + README. Ikuti struktur standar doc 10 & 11.
Terapkan kontrol akses RBAC+ABAC default-deny plus RLS pada endpoint/service AWCMS-Micro. Gunakan pada setiap endpoint non-public dan setiap akses data tenant-scoped. Menegakkan default deny, deny-overrides-allow, decision log, dan tenant isolation sesuai doc 03 & 10.
Tulis audit log untuk aksi high-risk AWCMS-Micro dengan redaction. Gunakan pada login, access assignment, profile merge, price change, transaction posted/cancel/return, stock adjustment, warehouse transfer, Coretax export, sync conflict resolution, AI tool call, dan security readiness decision. Sesuai doc 03 & 10.
Terapkan idempotency pada mutation high-risk AWCMS-Micro agar aman dari double-submit. Gunakan saat implementasi posting transaksi, cancel/return, transfer approve/ship/receive, cycle count, adjustment, VAT generate, Coretax batch, receipt send, sync push, atau workflow decision. Sesuai doc 10.
Kelola/konsumsi sistem Module Management AWCMS-Micro (registry, komposisi modul build-time untuk aplikasi turunan, manifest kompatibilitas aplikasi turunan, tenant lifecycle enable/disable, settings, permission sync/status, navigation, job registry, health/readiness). Gunakan saat menambah field descriptor baru (permissions/navigation/settings/jobs/health) di modul lain, saat menyelidiki kenapa suatu modul terlihat degraded/orphaned, saat aplikasi turunan perlu menyusun modulnya sendiri lewat `src/modules/application-registry.ts` tanpa mengedit registry base, saat aplikasi turunan perlu memverifikasi kompatibilitasnya dengan rilis base terbaru (`bun run extension:check`), atau saat mengubah perilaku enable/disable/settings/health module_management sendiri. Sesuai src/modules/module-management/README.md, epic