Cloudsteading
L Harness chat management interface with upstream demonstration contacts and messages

L Harness

Self-hosted LINE contact CRM, chat and messaging workflows

L Harness is a self-hosted L Message alternative built on Cloudflare (D1, Durable Objects, Pages, R2, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.

Source & license

Upstream license: MIT

License TL;DR

You can use it, change it, self-host it and sell it. Keep the original copyright and license notice with copies of the code. You don’t have to publish your changes. The authors don’t promise it will work.

Explain MIT in plain English →

Summary of the main license. Separate packages and assets can have different terms.

Inspect repository ↗Read this project’s actual license ↗

Repository owner

@Shudesu

See the upstream repository for the original creator and contributors.

Maintain this project? Maintainer verification →

Cloudflare hosting

Free tier eligible within limits

A small self-hosted CRM can fit the documented Cloudflare free allowances. A LINE Official Account and Messaging API channel are mandatory: The Japan Communication Plan includes up to 200 charge-counted messages/month at no monthly fee; higher-volume delivery incurs LINE plan fees. Optional Claude/AI services and custom domains are separate costs.

Hosting requirements
  • Operate within the region-specific LINE messaging plan and current delivery allowance; Cloudflare free hosting does not make unlimited LINE sends free.
  • Keep Worker/D1/R2/SQLite-DO use within free quotas and configure your own channel credentials, owner account and LIFF application. External AI is optional.
  • Source and configuration review establishes a deployment path and conditional costs; this candidate was not executed or load-tested.
Check current pricing ↗
Sources checked 01/10/2026

Repository snapshot: 5386460. Hosting eligibility reflects the deployment documentation and listed assumptions.

  • l-message ↗

    LINE 公式アカウントの完全オープンソース CRM。**L社 / U社 の無料代替**。 L Harness本体のソフトウェア利用料は **0円** です。LINE公式アカウントの配信料金、Cloudflareの契約・利用量、独自ドメイン、導入・保守、接続する外部AIサービスの費用は別です。Claude CodeからMCP経由で操作できます。

  • pages ↗

    name = "line-crm-liff" compatibility_date = "2024-12-01" pages_build_output_dir = "dist" [env.production] name = "line-crm-liff-prod"

  • workers ↗

    name = "line-harness" main = "src/index.ts" compatibility_date = "2024-12-01" compatibility_flags = ["nodejs_compat"] workers_dev = true # ═══════════════════════════════════════════════════════════════ # デフォルト = テスト環境 # 使い方: npx wrangler deploy # ═══════════════════════════════════════════════════════════════ account_id = "YOUR_DEV_ACCOUNT_ID" # Workers Cache pilot. Enab

  • d1 ↗

    ASSETS.fetch(req) を呼んで # SPA を返す。これが無いと static asset path が Worker をバイパスして # 直接配信され、bot 判定が効かない。 run_worker_first = true [[d1_databases]] binding = "DB" database_name = "line-harness" database_id = "YOUR_DEV_D1_DATABASE_ID" [[r2_buckets]] binding = "IMAGES" bucket_name = "line-harness-images" # ═══════════════════════════════════════════════════════════════ # TenantScheduler DO — 定期ジョブ (配信・リマインド・insight 取得) を自走させる。 # binding name "TENANT_SCHEDULER" と class_name "TenantScheduler" は # scripts/lib

  • r2 ↗

    orker_first = true [[d1_databases]] binding = "DB" database_name = "line-harness" database_id = "YOUR_DEV_D1_DATABASE_ID" [[r2_buckets]] binding = "IMAGES" bucket_name = "line-harness-images" # ═══════════════════════════════════════════════════════════════ # TenantScheduler DO — 定期ジョブ (配信・リマインド・insight 取得) を自走させる。 # binding name "TENANT_SCHEDULER" と class_name "TenantScheduler" は # scripts/lib/tenant-wrangler.ts (テナント自動プロビジョニング) にも # 同名で書く必要がある — 片方だけ変えると「デプロイは成功するが alarm は # 一生 armed されない」という気づきに

  • durable-objects ↗

    images" # ═══════════════════════════════════════════════════════════════ # TenantScheduler DO — 定期ジョブ (配信・リマインド・insight 取得) を自走させる。 # binding name "TENANT_SCHEDULER" と class_name "TenantScheduler" は # scripts/lib/tenant-wrangler.ts (テナント自動プロビジョニング) にも # 同名で書く必要がある — 片方だけ変えると「デプロイは成功するが alarm は # 一生 armed されない」という気づきにくい壊れ方をするので要注意。 # 実装: apps/worker/src/durable-objects/tenant-scheduler.ts # ═══════════════════════════════════════════════════════════════ [[durable_objects.bindings]] name = "TENANT_SCHEDULER" cl

  • free-tier-eligible ↗

    name = "line-crm-liff" compatibility_date = "2024-12-01" pages_build_output_dir = "dist" [env.production] name = "line-crm-liff-prod"

  • free-tier-eligible ↗

    | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation |

  • free-tier-eligible ↗

    | Rows read | 5 million / day | First 25 billion / month included + $0.001 / million rows |

  • free-tier-eligible ↗

    | Storage | 10 GB-month / month |

  • free-tier-eligible ↗

    Durable Objects are available both on Workers Free and Workers Paid plans.

  • free-tier-eligible ↗

    --><tr><!--[--><th align="left"><!--[--><!--]--></th><th align="center"><!--[-->Communication Plan<!--]--></th><th align="center"><!--[-->Light Plan<!--]--></th><th align="center"><!--[-->Standard Plan<!--]--></th><!--]--></tr><!--]--></thead><tbody><!--[--><tr><!--[--><td align="left"><!--[-->Fixed monthly fee <sup><a href="#user-content-fn-1" class="" aria-describedby="footnote-label" data-footnote-ref id="user-content-fnref-1"><!--[--><!--[-->1<!--]--><!--]--></a></sup><!--]--></td><td align="center"><!--[-->Free<!--]--></td><td align="center"><!--[-->JPY 5,000<!--]--></td><td align="center"><!--[-->JPY 15,000<!--]--></td><!--]--></tr><tr><!--[--><td align="left"><!--[-->Number of free messages per month<!--]--></td><td align="center"><!--[-->Up to 200<!--]--></td><td align="center"><!--[-->Up to 5,000<!--]--></td><td align="center"><!--[-->Up to 30,000<!--]--></td><!--]--></tr><tr><!--[--><td align="left"><!--[-->Cost of additional messages <sup><a href="#user-content-fn-1" class="" aria-describedby="footnote-label" data-footnote-ref id="user-content-fnref-1-2"><!--[--><!--[-->1<!--]--><!--]--></a></sup><!--]--></td><td align="center"><!--[-->N/A<!--]--></td><td align="center"><!--[-->N/A<!--]--></td><td align="center"><!--[-->Up to JPY 3/message <sup><a href="#user-content-fn-2" class="" aria-describedby="footnote-label" data-footnote-ref id="user-conte

  • free-tier-eligible ↗

    | Class A Operations | 1 million requests / month |

  • free-tier-eligible ↗

    | Class B Operations | 10 million requests / month |

  • free-tier-eligible ↗

    | Rows written | 100,000 / day | First 50 million / month included + $1.00 / million rows |

  • free-tier-eligible ↗

    | SQL Stored data <sup>5</sup> | 5 GB (total) | 5 GB-month, + $0.20/ GB-month |

  • MIT ↗

    MIT License Copyright (c) 2026 Shudesu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION

  • architecture ↗

    name = "line-crm-liff" compatibility_date = "2024-12-01" pages_build_output_dir = "dist" [env.production] name = "line-crm-liff-prod"

  • architecture ↗

    name = "line-harness" main = "src/index.ts" compatibility_date = "2024-12-01" compatibility_flags = ["nodejs_compat"] workers_dev = true # ═══════════════════════════════════════════════════════════════ # デフォルト = テスト環境 # 使い方: npx wrangler deploy # ═══════════════════════════════════════════════════════════════ account_id = "YOUR_DEV_ACCOUNT_ID" # Workers Cache pilot. Enabled only for the private test Worker; production is # explicitly disabled below until cache hit rate and auth isolation are proven. # Only routes with an explicit public Cache-Control header are eligible because # src/middleware/cache-policy.ts marks every other response no-store. [cache] enabled = true [assets] directory = "dist/client" binding = "ASSETS" # Worker を先に走らせて bot UA → OGP HTML 差し込みを可能にする。 # 通常 UA のときは Work

  • architecture ↗

    name = "line-crm-liff" compatibility_date = "2024-12-01" pages_build_output_dir = "dist" [env.production] name = "line-crm-liff-prod"

  • architecture ↗

    name = "line-harness" main = "src/index.ts" compatibility_date = "2024-12-01" compatibility_flags = ["nodejs_compat"] workers_dev = true # ═══════════════════════════════════════════════════════════════ # デフォルト = テスト環境 # 使い方: npx wrangler deploy # ═══════════════════════════════════════════════════════════════ account_id = "YOUR_DEV_ACCOUNT_ID" # Workers Cache pilot. Enab

  • architecture ↗

    ASSETS.fetch(req) を呼んで # SPA を返す。これが無いと static asset path が Worker をバイパスして # 直接配信され、bot 判定が効かない。 run_worker_first = true [[d1_databases]] binding = "DB" database_name = "line-harness" database_id = "YOUR_DEV_D1_DATABASE_ID" [[r2_buckets]] binding = "IMAGES" bucket_name = "line-harness-images" # ═══════════════════════════════════════════════════════════════ # TenantScheduler DO — 定期ジョブ (配信・リマインド・insight 取得) を自走させる。 # binding name "TENANT_SCHEDULER" と class_name "TenantScheduler" は # scripts/lib

  • architecture ↗

    orker_first = true [[d1_databases]] binding = "DB" database_name = "line-harness" database_id = "YOUR_DEV_D1_DATABASE_ID" [[r2_buckets]] binding = "IMAGES" bucket_name = "line-harness-images" # ═══════════════════════════════════════════════════════════════ # TenantScheduler DO — 定期ジョブ (配信・リマインド・insight 取得) を自走させる。 # binding name "TENANT_SCHEDULER" と class_name "TenantScheduler" は # scripts/lib/tenant-wrangler.ts (テナント自動プロビジョニング) にも # 同名で書く必要がある — 片方だけ変えると「デプロイは成功するが alarm は # 一生 armed されない」という気づきに

  • architecture ↗

    images" # ═══════════════════════════════════════════════════════════════ # TenantScheduler DO — 定期ジョブ (配信・リマインド・insight 取得) を自走させる。 # binding name "TENANT_SCHEDULER" と class_name "TenantScheduler" は # scripts/lib/tenant-wrangler.ts (テナント自動プロビジョニング) にも # 同名で書く必要がある — 片方だけ変えると「デプロイは成功するが alarm は # 一生 armed されない」という気づきにくい壊れ方をするので要注意。 # 実装: apps/worker/src/durable-objects/tenant-scheduler.ts # ═══════════════════════════════════════════════════════════════ [[durable_objects.bindings]] name = "TENANT_SCHEDULER" cl

Upstream screenshot · Shudesu/line-harness-oss repository contributors ↗. Depicts the upstream project. We have not deployed and tested a fresh installation here.

What it can replace

Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.

L Message logoL Message ↗

LINE Official Account contact management, tagging, operator chat and messaging workflows; no complete hosted LINE marketing-service parity or migration claim.

See supporting source ↗
external SaaS target
varies

How it works

The shape of L Harness on Cloudflare, and how it stacks up against the rented tools it replaces.

Architecture

Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.

View upstream source ↗
Public interface
Configured entry points3
line-crm-liff
apps/liff/wrangler.toml
line-harness
apps/worker/wrangler.toml
line-harness-plugin-myservice
packages/plugin-template/wrangler.toml
↓
App
line-crm-liff
entry
Cloudflare Pages
line-harness
entry
Cloudflare Workers
Entrypoint: src/index.tsConfigured cron (UTC): * * * * *Configured cron (UTC): 0 */6 * * *
line-harness-plugin-myservice
entry
Cloudflare Workers
Entrypoint: src/index.ts
↓

Configuration and workflow sources

Reviewed commit 53864603502a. Files were read as data; upstream applications and CI jobs were not executed.

Partial source coverage: 116 files outside collection bounds; 0 collection or parsing issues. Dynamic imports and generated entrypoints may need manual review.

Deployment configuration · 4 files
apps/liff/wrangler.toml ↗

Cloudflare Pages · compatibility 2024-12-01

line-crm-liff · default

    No resource bindings declared in this scope.

    line-crm-liff-prod · env.production

    Inherited from default: compatibility_date

      No resource bindings declared in this scope.

      apps/worker/wrangler.toml ↗

      Cloudflare Workers · compatibility 2024-12-01

      line-harness · default

      Entrypoint: src/index.ts

      Static assets: dist/client · Worker first: true

      Cron triggers (UTC): * * * * * · 0 */6 * * *

      • DB → D1
      • IMAGES → R2
      • TENANT_SCHEDULER → Durable Objects · class TenantScheduler
      • ASSETS → Static assets

      line-harness · env.production

      Inherited from default: main, compatibility_date

      Entrypoint: src/index.ts

      Static assets: dist/client · Worker first: true

      Cron triggers (UTC): * * * * * · 0 */6 * * *

      • DB → D1
      • IMAGES → R2
      • TENANT_SCHEDULER → Durable Objects · class TenantScheduler
      • ASSETS → Static assets
      examples/plugins/tag-rules/wrangler.jsonc ↗

      Cloudflare Workers · example/template, excluded from overview · compatibility 2026-09-07

      line-harness-plugin-tag-rules · default

      Entrypoint: src/index.ts

        No resource bindings declared in this scope.

        packages/plugin-template/wrangler.toml ↗

        Cloudflare Workers · compatibility 2024-06-01

        line-harness-plugin-myservice · default

        Entrypoint: src/index.ts

          No resource bindings declared in this scope.

          Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.

          Runtime source · handlers, binding usage and workflow steps

          Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.

          apps/worker/src/index.ts ↗
          • L975 · fetch handler exported
          • L976 · scheduled handler exported
          • L165 · app.use("*")
          • L170 · app.use("/api/public/media-inquiries")
          • L186 · app.use("*")
          • L195 · app.use("*")
          • L198 · app.use("*")
          • L201 · app.route("/")
          • L202 · app.route("/")
          • L203 · app.route("/")
          • L204 · app.route("/")
          • L205 · app.route("/")
          • L206 · app.route("/")
          • L207 · app.route("/")
          • L208 · app.route("/")
          • L209 · app.route("/")
          • L210 · app.route("/")
          • L211 · app.route("/")
          • L212 · app.route("/")
          • L213 · app.route("/")
          • L214 · app.route("/")
          • L215 · app.route("/")
          • L216 · app.route("/")
          • L217 · app.route("/")
          • L218 · app.route("/")
          • L221 · app.route("/")
          • L222 · app.route("/")
          • L223 · app.route("/")
          • L224 · app.route("/")
          • L225 · app.route("/")
          • L226 · app.route("/")
          • L227 · app.route("/")
          • L228 · app.route("/")
          • L229 · app.route("/")
          • L230 · app.route("/")
          • L231 · app.route("/")
          • L232 · app.route("/")
          • L233 · app.route("/")
          • L234 · app.route("/")
          • L235 · app.route("/")

          Environment references: c.env.LIFF_URL · c.env.DB · c.env.ASSETS

          examples/plugins/tag-rules/src/index.ts ↗
          • L8 · scheduled handler exported · references LINE_HARNESS_API_KEY, DRY_RUN, LINE_HARNESS_API_URL, LINE_ACCOUNT_ID, TARGET_TAG_ID · calls applyTagRules, console.log, JSON.stringify
          • L27 · fetch handler exported · calls Response.json

          Environment references: env.LINE_HARNESS_API_KEY · env.DRY_RUN · env.LINE_HARNESS_API_URL · env.LINE_ACCOUNT_ID · env.TARGET_TAG_ID

          packages/plugin-template/src/index.ts ↗
          • L24 · scheduled handler exported · calls console.log, syncExternalData
          • L41 · fetch handler exported · calls JSON.stringify, Response.json
          apps/worker/src/scheduled.ts ↗
          • L41 · isFiveMinuteTick calls (conditional paths may differ): getUTCMinutes
          • L51 · isHourlyTick calls (conditional paths may differ): getUTCMinutes
          • L64 · scheduled calls (conditional paths may differ): getLineAccounts, lineClients.set, Promise.allSettled, recoverStalledBroadcasts, recoverStuckDeliveries, refreshLineAccessTokens, console.error, processDueReminders, console.log, processDueEventReminders, processDueMeetConsultationReminders, Promise.resolve, lineProxy.fetch, exec, processWebinarReminders, processWebinarFollowups, jobs.push, startBulkSendJobs, processQueuedBroadcasts, processScheduledBroadcasts

          Environment references: env.DB · env.LINE_CHANNEL_ACCESS_TOKEN · env.WORKER_PUBLIC_URL · env.LIFF_URL · env.WORKER_URL · env.IMAGES

          apps/worker/src/durable-objects/tenant-scheduler.ts ↗
          • L46 · nextMinuteBoundary calls (conditional paths may differ): Math.floor
          • L58 · isSixHourBoundary calls (conditional paths may differ): now.getUTCMinutes, now.getUTCHours
          • L95 · runSchedulerTick calls (conditional paths may differ): storage.setAlarm, nextMinuteBoundary, now.getTime, runJobs, isSixHourBoundary, console.error
          • L126 · ensureAlarmArmed calls (conditional paths may differ): storage.getAlarm, storage.setAlarm, nextMinuteBoundary, now.getTime
          • L177 · ensureSchedulerArmed calls (conditional paths may differ): ns.get, ns.idFromName, stub.ensureArmed, console.error

          Environment references: env.TENANT_SCHEDULER

          apps/worker/src/middleware/auth.ts ↗
          • L16 · parseCookieHeader calls (conditional paths may differ): cookieHeader.split, split, part.trim, safeDecode, rawValue.join
          • L27 · bearerToken calls (conditional paths may differ): c.req.header, authHeader.startsWith, authHeader.slice
          • L33 · cookieToken calls (conditional paths may differ): parseCookieHeader, c.req.header
          • L37 · csrfTokenFromCookie calls (conditional paths may differ): parseCookieHeader, c.req.header
          • L41 · buildCookie calls (conditional paths may differ): encodeURIComponent, parts.push, parts.join
          • L55 · adminSessionCookie calls (conditional paths may differ): buildCookie
          • L67 · csrfCookie calls (conditional paths may differ): buildCookie
          • L71 · expiredCookie calls (conditional paths may differ): buildCookie
          • L86 · authenticateApiToken calls (conditional paths may differ): getStaffByApiKey, console.log
          • L119 · authMiddleware calls (conditional paths may differ): c.req.method.toUpperCase, path.startsWith, next, test, bearerToken, cookieToken, authenticateApiToken, c.set, path.match, c.json, SAFE_METHODS.has, c.req.header, csrfTokenFromCookie

          Environment references: c.env.DB · c.env.API_KEY · c.env.LEGACY_API_KEY

          apps/worker/src/middleware/rate-limit.ts ↗
          • L26 · prune calls (conditional paths may differ): Date.now, entry.timestamps.filter, store.delete
          • L37 · check calls (conditional paths may differ): Date.now, prune, store.get, store.set, entry.timestamps.filter, Math.ceil, Math.max, entry.timestamps.push
          • L71 · isUnauthenticatedPath calls (conditional paths may differ): UNAUTHENTICATED_PATTERNS.some, p.test
          • L77 · getClientIp calls (conditional paths may differ): c.req.header
          • L86 · getAdminCookieToken calls (conditional paths may differ): c.req.header, cookie.split, split, part.trim, rawValue.join, decodeURIComponent
          • L124 · rateLimitMiddleware calls (conditional paths may differ): path.startsWith, next, isUnauthenticatedPath, getClientIp, c.req.header, authHeader.slice, getAdminCookieToken, token.slice, check, c.json, String, c.header
          apps/worker/src/routes/webhook.ts ↗
          • L77 · webhook.post("/webhook")
          • L37 · ensureFriendFromWebhookUser calls (conditional paths may differ): getFriendByLineUserId, lineClient.getProfile, console.error, upsertFriend, console.log, jstNow, run, bind, db.prepare
          • L198 · handleEvent calls (conditional paths may differ): console.log, lineClient.getProfile, console.error, upsertFriend, run, bind, db.prepare, jstNow, awardActivityMileage, setTimeout, getFriendById, getEntryRouteByRefCode, getScenarios, enrollFriendInScenario, pushImmediateFirstStep, getMessageTemplateById, buildMessage, lineClient.pushMessage, fireEvent, updateFriendFollowStatus

          Environment references: c.env.DB · c.env.LINE_CHANNEL_ACCESS_TOKEN · c.env.LINE_CHANNEL_SECRET · c.env.WORKER_URL · c.env.LIFF_URL · c.env.IMAGES

          apps/worker/src/routes/friends.ts ↗
          • L87 · friends.get("/api/friends")
          • L348 · friends.get("/api/friends/count")
          • L367 · friends.get("/api/friends/ref-stats")
          • L393 · friends.get("/api/friends/:id/mileage")
          • L417 · friends.get("/api/friends/:id")
          • L454 · friends.post("/api/friends/:id/tags")
          • L491 · friends.delete("/api/friends/:id/tags/:tagId")
          • L509 · friends.put("/api/friends/:id/metadata")
          • L552 · friends.get("/api/friends/:id/messages")
          • L577 · friends.post("/api/friends/:id/messages")
          • L33 · serializeFriend calls (conditional paths may differ): Boolean, JSON.parse
          • L57 · serializeFriendListRow calls (conditional paths may differ): serializeFriend

          Environment references: c.env.DB · c.env.LINE_CHANNEL_ACCESS_TOKEN · c.env.WORKER_URL

          apps/worker/src/routes/tags.ts ↗
          • L34 · tags.get("/api/tags")
          • L48 · tags.patch("/api/tags/:id/mileage")
          • L95 · tags.post("/api/tags")
          • L123 · tags.delete("/api/tags/:id")
          • L15 · serializeTag calls (conditional paths may differ): Number

          Environment references: c.env.DB

          apps/worker/src/routes/scenarios.ts ↗
          • L177 · scenarios.get("/api/scenarios")
          • L213 · scenarios.get("/api/scenarios/:id")
          • L236 · scenarios.post("/api/scenarios")
          • L285 · scenarios.put("/api/scenarios/:id")
          • L321 · scenarios.delete("/api/scenarios/:id")
          • L333 · scenarios.post("/api/scenarios/:id/steps")
          • L412 · scenarios.put("/api/scenarios/:id/steps/:stepId")
          • L577 · scenarios.delete("/api/scenarios/:id/steps/:stepId")
          • L589 · scenarios.post("/api/scenarios/:id/steps/reorder")
          • L666 · scenarios.get("/api/scenarios/:id/preview")
          • L757 · scenarios.get("/api/scenarios/:id/stats")
          • L784 · scenarios.get("/api/scenarios/:id/enrollments")
          • L875 · scenarios.post("/api/scenarios/:id/enroll/:friendId")
          • L32 · serializeScenario calls (conditional paths may differ): Boolean
          • L79 · validateStepCondition calls (conditional paths may differ): isSupportedConditionType, String, SUPPORTED_CONDITION_TYPES.join, JSON.parse, Array.isArray
          • L124 · validateStepSchedule calls (conditional paths may differ): HHMM_RE.test

          Environment references: c.env.DB

          apps/worker/src/routes/broadcasts.ts ↗
          • L145 · broadcasts.get("/api/broadcasts")
          • L157 · broadcasts.get("/api/broadcasts/:id")
          • L177 · broadcasts.get("/api/broadcasts/:id/preview-count")
          • L244 · broadcasts.get("/api/broadcasts/:id/per-account-stats")
          • L333 · broadcasts.post("/api/broadcasts")
          • L429 · broadcasts.put("/api/broadcasts/:id")
          • L523 · broadcasts.delete("/api/broadcasts/:id")
          • L572 · broadcasts.post("/api/broadcasts/:id/send")
          • L836 · broadcasts.post("/api/broadcasts/:id/send-segment")
          • L932 · broadcasts.get("/api/broadcasts/:id/insight")
          • L964 · broadcasts.post("/api/broadcasts/:id/fetch-insight")
          • L1129 · broadcasts.post("/api/broadcasts/:id/test-send")
          • L1215 · broadcasts.get("/api/broadcasts/:id/progress")
          • L1233 · broadcasts.post("/api/segments/count")
          • L40 · unsupportedVariablesError calls (conditional paths may differ): getUnsupportedBroadcastVariables, join, unsupported.map
          • L54 · parseJsonArray calls (conditional paths may differ): Array.isArray, JSON.parse
          • L66 · parseJsonObject calls (conditional paths may differ): JSON.parse, Array.isArray
          • L94 · sameCreateRequest calls (conditional paths may differ): parseJsonArray, JSON.stringify
          • L110 · serializeBroadcast calls (conditional paths may differ): parseJsonObject, parseJsonArray
          • L551 · isLockedUsageRecord calls (conditional paths may differ): quotaConfig, monthStartJst

          Environment references: c.env.DB · c.env.LINE_CHANNEL_ACCESS_TOKEN · c.env.WORKER_URL

          apps/worker/src/routes/users.ts ↗
          • L38 · users.get("/api/users")
          • L49 · users.get("/api/users/:id")
          • L64 · users.post("/api/users")
          • L82 · users.put("/api/users/:id")
          • L110 · users.delete("/api/users/:id")
          • L121 · users.post("/api/users/:id/link")
          • L139 · users.get("/api/users/:id/accounts")
          • L159 · users.post("/api/users/match")
          • L19 · logUsersRouteError calls (conditional paths may differ): console.error

          Environment references: c.env.DB

          apps/worker/src/routes/line-accounts.ts ↗
          • L99 · lineAccounts.get("/api/line-accounts")
          • L193 · lineAccounts.get("/api/line-accounts/delivery-health")
          • L303 · lineAccounts.get("/api/line-accounts/:id")
          • L321 · lineAccounts.get("/api/line-accounts/:id/follower-insight")
          • L356 · lineAccounts.get("/api/line-accounts/:id/follower-import")
          • L363 · lineAccounts.post("/api/line-accounts/:id/follower-import/detect")
          • L385 · lineAccounts.post("/api/line-accounts/:id/follower-import/start")
          • L403 · lineAccounts.post("/api/line-accounts/:id/follower-import/step")
          • L512 · lineAccounts.post("/api/line-accounts")
          • L626 · lineAccounts.patch("/api/line-accounts/order")
          • L666 · lineAccounts.patch("/api/line-accounts/:id")
          • L786 · lineAccounts.put("/api/line-accounts/:id")
          • L894 · lineAccounts.delete("/api/line-accounts/:id")
          • L27 · serializeLineAccount calls (conditional paths may differ): Boolean
          • L52 · serializeLineAccountFull calls (conditional paths may differ): serializeLineAccount
          • L62 · maskSecret calls (conditional paths may differ): value.slice
          • L75 · serializeLineAccountMasked calls (conditional paths may differ): serializeLineAccount, maskSecret
          • L85 · fetchBotProfile calls (conditional paths may differ): fetch, res.json
          • L150 · getFollowersInsightCached calls (conditional paths may differ): encodeURIComponent, cache.match, hit.json, client.getFollowersInsight, cache.put, JSON.stringify
          • L430 · normalizeOptionalString calls (conditional paths may differ): v.trim
          • L481 · checkUniqueLoginAndLiff calls (conditional paths may differ): checks.push, first, bind, db.prepare

          Environment references: c.env.DB

          apps/worker/src/routes/conversions.ts ↗
          • L24 · conversions.get("/api/conversions/points")
          • L44 · conversions.post("/api/conversions/points")
          • L74 · conversions.delete("/api/conversions/points/:id")
          • L87 · conversions.post("/api/conversions/track")
          • L131 · conversions.get("/api/conversions/events")
          • L162 · conversions.get("/api/conversions/report")
          • L183 · conversions.get("/api/conversions/approvals")
          • L211 · conversions.patch("/api/conversions/events/:id/approval")

          Environment references: c.env.DB

          apps/worker/src/routes/affiliates.ts ↗
          • L40 · affiliates.get("/api/affiliates")
          • L51 · affiliates.get("/api/affiliates/:id")
          • L77 · affiliates.post("/api/affiliates")
          • L198 · affiliates.put("/api/affiliates/:id")
          • L224 · affiliates.delete("/api/affiliates/:id")
          • L237 · affiliates.get("/api/affiliates/:id/report")
          • L257 · affiliates.get("/api/affiliates/:id/journeys")
          • L277 · affiliates.get("/api/affiliates/:id/links")
          • L300 · affiliates.get("/api/friends/:id/journey")
          • L311 · affiliates.post("/api/affiliates/click")
          • L337 · affiliates.get("/api/affiliates-report")
          • L27 · serializeAffiliate calls (conditional paths may differ): Boolean

          Environment references: c.env.DB

          apps/worker/src/routes/affiliate-offers.ts ↗
          • L42 · affiliateOffers.get("/api/affiliate-offers")
          • L54 · affiliateOffers.get("/api/affiliate-offers/:id")
          • L68 · affiliateOffers.post("/api/affiliate-offers")
          • L116 · affiliateOffers.put("/api/affiliate-offers/:id")
          • L20 · serializeOffer calls (conditional paths may differ): Boolean
          • L37 · isValidReward calls (conditional paths may differ): Number.isInteger

          Environment references: c.env.DB

          apps/worker/src/routes/duplicates.ts ↗
          • L70 · duplicates.get("/api/duplicates/stats")
          • L54 · serializeDuplicatesStats calls (conditional paths may differ): stats.per_account.map, stats.pairwise_overlap.map

          Environment references: c.env.DB

          apps/worker/src/routes/users-grouped.ts ↗
          • L8 · usersGrouped.get("/api/users-grouped")

          Environment references: c.env.DB

          apps/worker/src/routes/usage.ts ↗
          • L9 · usage.get("/api/usage")

          Environment references: c.env.DB

          apps/worker/src/routes/inbox.ts ↗
          • L18 · inbox.get("/api/inbox/activity-digest")
          • L36 · inbox.get("/api/inbox/unanswered")
          • L60 · inbox.get("/api/inbox/unanswered/count")

          Environment references: c.env.DB

          apps/worker/src/routes/openapi.ts ↗
          • L1002 · openapi.get("/openapi.json")
          • L1007 · openapi.get("/docs")
          apps/worker/src/routes/liff.ts ↗
          • L317 · liffRoutes.get("/auth/line")
          • L572 · liffRoutes.get("/auth/oauth")
          • L652 · liffRoutes.get("/auth/callback")
          • L1107 · liffRoutes.get("/api/liff/config")
          • L1151 · liffRoutes.post("/api/liff/profile")
          • L1179 · liffRoutes.post("/api/liff/link")
          • L1405 · liffRoutes.get("/api/analytics/ref-summary")
          • L1478 · liffRoutes.get("/api/analytics/ref/:refCode")
          • L1536 · liffRoutes.post("/api/links/wrap")
          • L1787 · liffRoutes.post("/api/liff/send-form-link")
          • L47 · encodeState calls (conditional paths may differ): btoa, String.fromCharCode, encode
          • L50 · decodeState calls (conditional paths may differ): decode, Uint8Array.from, atob, ch.charCodeAt
          • L64 · linkIgIgsid calls (conditional paths may differ): run, bind, c.env.DB.prepare, first, console.error, console.warn, awardActivityMileage, c.executionCtx.waitUntil, catch, then, fetch, JSON.stringify, res.text
          • L146 · saveIgAccountMeta calls (conditional paths may differ): first, bind, db.prepare, JSON.parse, igAccountUsername.replace, run, JSON.stringify, console.error
          • L199 · applyRefAttribution calls (conditional paths may differ): ref.startsWith, isReservedRef, getEntryRouteByRefCode, getTrackedLinkById, getAffiliateLinkByRefCode, getAffiliateById, getAffiliateOfferById, notifyAffiliateFriendAdd, console.error, attachTagAndFireSideEffects, pushImmediateFirstStep
          • L1563 · authLandingPage calls (conditional paths may differ): liffUrl.match, liffUrl.indexOf, liffUrl.slice, escapeHtml
          • L1638 · completionPage calls (conditional paths may differ): escapeHtml
          • L1673 · errorPage calls (conditional paths may differ): escapeHtml
          • L1697 · escapeHtml calls (conditional paths may differ): replace, str.replace
          • L1707 · applyXHarnessActions calls (conditional paths may differ): first, bind, db.prepare, crypto.randomUUID, jstNow, addTagToFriend, console.log, console.error, enrollFriendInScenario
          • L1760 · resolveXHarnessToken calls (conditional paths may differ): setTimeout, controller.abort, fetch, res.json, clearTimeout

          Environment references: c.env.DB · c.env.IG_HARNESS_URL · c.env.IG_HARNESS_LINK_SECRET · c.env.LINE_CHANNEL_ACCESS_TOKEN · c.env.WORKER_URL · c.env.LINE_LOGIN_CHANNEL_ID · c.env.LIFF_URL · c.env.LINE_LOGIN_CHANNEL_SECRET · c.env.LINE_CHANNEL_SECRET · env.X_HARNESS_URL

          apps/worker/src/routes/affiliate-self.ts ↗
          • L167 · affiliateSelfRoutes.get("/api/liff/mileage/me")
          • L213 · affiliateSelfRoutes.post("/api/liff/affiliate/register")
          • L283 · affiliateSelfRoutes.get("/api/liff/affiliate/me")
          • L330 · affiliateSelfRoutes.post("/api/liff/affiliate/links")
          • L403 · affiliateSelfRoutes.get("/api/liff/affiliate/offers")
          • L462 · affiliateSelfRoutes.post("/api/liff/affiliate/offers/:id/enroll")
          • L54 · resolveFriendFromLineToken calls (conditional paths may differ): fetch, encodeURIComponent, catch, v.json, allowedChannelIds.add, getLineAccounts, allowedChannelIds.has, prof.json, getFriendByLineUserId
          • L103 · unresolvedResponse calls (conditional paths may differ): c.json
          • L145 · loadOfferNames calls (conditional paths may differ): listAffiliateOffers, offers.map
          • L150 · serializeAffiliate calls (conditional paths may differ): Boolean

          Environment references: env.DB · env.LINE_LOGIN_CHANNEL_ID · c.env.DB · c.env.LINE_CHANNEL_SECRET

          Build and deployment pipeline · 9 GitHub Actions workflows

          Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.

          Core Safety CI · .github/workflows/core-safety-ci.yml ↗

          Triggers: pull_request, push, workflow_dispatch

          worker · no job dependencies declared

          1. actions/checkout@v4actions/checkout@v4
          2. pnpm/action-setup@v4pnpm/action-setup@v4
          3. actions/setup-node@v4actions/setup-node@v4
          4. Shell commandpnpm install --frozen-lockfile
          5. Shell commandpnpm --filter @line-crm/shared --filter @line-crm/line-sdk --filter @line-harness/update-engine build
          6. Shell commandpnpm --filter worker typecheck
          7. Shell commandpnpm --filter worker test
          8. Shell commandpnpm --filter worker build

          setup-and-upgrade · no job dependencies declared

          1. actions/checkout@v4actions/checkout@v4
          2. Ensure historical schema fixtures are availableset -euo pipefail for tag in v0.14.1 v0.15.0 v0.16.0 v0.17.0 v0.18.0 v0.18.1; do if ! git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then git fetch --depth=1 --no-tags https://github.com/Shudesu/line-harness-oss.git "refs/tags/$tag:refs/tags/$tag" fi done
          3. pnpm/action-setup@v4pnpm/action-setup@v4
          4. actions/setup-node@v4actions/setup-node@v4
          5. Shell commandpnpm install --frozen-lockfile
          6. Shell commandpnpm --filter @line-harness/update-engine build
          7. Shell commandpnpm --filter @line-crm/db test
          8. Shell commandpnpm --filter @line-crm/db typecheck
          9. Shell commandpnpm --filter @line-harness/update-engine test
          10. Shell commandpnpm --filter @line-harness/update-engine typecheck
          11. Shell commandpnpm --filter create-line-harness test
          12. Shell commandpnpm --filter create-line-harness build
          13. Shell commandpnpm --filter create-line-harness exec tsc --noEmit
          14. Shell commandpnpm exec tsx scripts/check-migrations.ts
          15. Shell commandnode packages/db/scripts/generate-bootstrap.mjs --check
          Deploy Cloudflare Admin · .github/workflows/deploy-cloudflare-admin.yml ↗

          Triggers: workflow_dispatch, push

          deploy · no job dependencies declared

          Condition: github.repository != 'Shudesu/line-harness-oss' && vars.LINE_HARNESS_CLOUDFLARE_DEPLOY == 'true'

          1. actions/checkout@v4actions/checkout@v4
          2. pnpm/action-setup@v4pnpm/action-setup@v4
          3. actions/setup-node@v4actions/setup-node@v4
          4. Shell commandpnpm install --frozen-lockfile
          5. Shell commandpnpm --filter @line-crm/shared --filter @line-harness/update-engine build
          6. Capture build metadataecho "build_time=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT"
          7. Build Admin Panelpnpm --filter web build
          8. Deploy to Cloudflare Pagestest -n "$CLOUDFLARE_API_TOKEN" test -n "$CLOUDFLARE_ACCOUNT_ID" test -n "$NEXT_PUBLIC_API_URL" npx wrangler pages deploy apps/web/out \ --project-name="$PAGES_PROJECT_NAME" \ --commit-message="deploy ${GITHUB_SHA}"
          Deploy Cloudflare Worker · .github/workflows/deploy-cloudflare-worker.yml ↗

          Triggers: workflow_dispatch, push

          deploy · no job dependencies declared

          Condition: github.repository != 'Shudesu/line-harness-oss' && vars.LINE_HARNESS_CLOUDFLARE_DEPLOY == 'true'

          1. actions/checkout@v4actions/checkout@v4
          2. pnpm/action-setup@v4pnpm/action-setup@v4
          3. actions/setup-node@v4actions/setup-node@v4
          4. Shell commandpnpm install --frozen-lockfile
          5. Shell commandpnpm --filter @line-crm/shared --filter @line-crm/line-sdk --filter @line-crm/db --filter @line-harness/update-engine build
          6. Run pending D1 migrationstest -n "$CLOUDFLARE_API_TOKEN" test -n "$CLOUDFLARE_ACCOUNT_ID" test -n "$D1_DATABASE_NAME" npx wrangler d1 execute "$D1_DATABASE_NAME" --remote --command \ "CREATE TABLE IF NOT EXISTS _migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)" for f in $(ls packages/db/migrations/*.sql | sort); do name=$(basename "$f") applied=$(npx wrangler d1 execute "$D1_DATABASE_NAME" --remote \ --command "SELECT name FROM _migrations WHERE name = '${name}'" --json \ | node -e "const d=JSON.parse(requi…
          7. Build Worker and LIFF assetspnpm --filter worker build
          8. Patch wrangler configtest -n "$D1_DATABASE_ID" cd apps/worker sed -i \ -e "s|\"account_id\":\"[^\"]*\"|\"account_id\":\"${CLOUDFLARE_ACCOUNT_ID}\"|g" \ -e "s|\"database_name\":\"[^\"]*\"|\"database_name\":\"${D1_DATABASE_NAME}\"|g" \ -e "s|\"database_id\":\"[^\"]*\"|\"database_id\":\"${D1_DATABASE_ID}\"|g" \ dist/line_harness/wrangler.json # Inject admin CORS vars into the deployed config when ADMIN_ORIGIN is # set. ADMIN_ALLOW_CROSS_SITE defaults to "true" (SameSite=None; Secure # cookies) since cross-origin admin…
          9. Deploy to Cloudflare Workerscloudflare/wrangler-action@v3Wrangler command: deploy --config dist/line_harness/wrangler.json --name ${{ vars.WORKER_NAME || 'your-worker-name' }}
          Deploy Pages · .github/workflows/deploy-pages.yml ↗

          Triggers: push, workflow_dispatch

          deploy · no job dependencies declared

          1. actions/checkout@v4actions/checkout@v4
          2. actions/configure-pages@v5actions/configure-pages@v5
          3. actions/upload-pages-artifact@v3actions/upload-pages-artifact@v3
          4. actions/deploy-pages@v4actions/deploy-pages@v4
          Plugin Market CI · .github/workflows/plugin-market-ci.yml ↗

          Triggers: pull_request, push, workflow_dispatch

          market-web · no job dependencies declared

          1. actions/checkout@v4actions/checkout@v4
          2. pnpm/action-setup@v4pnpm/action-setup@v4
          3. actions/setup-node@v4actions/setup-node@v4
          4. Shell commandpnpm install --frozen-lockfile
          5. Shell commandpnpm --filter @line-crm/shared --filter @line-harness/update-engine build
          6. Shell commandpnpm exec vitest run scripts/create-plugin.test.ts scripts/plugin-tag-rules.test.ts
          7. Shell commandpnpm --filter web test
          8. Shell commandpnpm --filter web build

          plugin-starters · no job dependencies declared

          1. actions/checkout@v4actions/checkout@v4
          2. actions/setup-node@v4actions/setup-node@v4
          3. Generate outside the main checkoutnode examples/plugins/create-plugin.mjs "$RUNNER_TEMP/plugin-smoke" "${{ matrix.template }}"
          4. Install and build the independent pluginnpm install --no-audit --no-fund npm run typecheck npm run build npm run build:mcp --if-present
          Publish Update CLI · .github/workflows/publish-update-cli.yml ↗

          Triggers: workflow_dispatch

          publish · no job dependencies declared

          1. actions/checkout@v6actions/checkout@v6
          2. pnpm/action-setup@v4pnpm/action-setup@v4
          3. actions/setup-node@v6actions/setup-node@v6
          4. Install dependencies and current npm CLInpm install --global npm@11 pnpm install --frozen-lockfile
          5. Test and build publishable packagespnpm --filter @line-harness/update-engine test pnpm --filter @line-harness/update-engine build pnpm --filter create-line-harness test pnpm --filter create-line-harness build
          6. Pack and publish with npm OIDCpack_dir="$RUNNER_TEMP/line-harness-packs" mkdir -p "$pack_dir" update_version=$(node -p "require('./packages/update-engine/package.json').version") cli_version=$(node -p "require('./packages/create-line-harness/package.json').version") pnpm --dir packages/update-engine pack --pack-destination "$pack_dir" pnpm --dir packages/create-line-harness pack --pack-destination "$pack_dir" if npm view "@line-harness/update-engine@$update_version" version >/dev/null 2>&1; then echo "@line-harness/update-e…
          Release · .github/workflows/release.yml ↗

          Triggers: push

          release · no job dependencies declared

          1. actions/checkout@v4actions/checkout@v4
          2. pnpm/action-setup@v4pnpm/action-setup@v4
          3. actions/setup-node@v4actions/setup-node@v4
          4. Shell commandpnpm install --frozen-lockfile
          5. Resolve version from tag# Strip the leading "v" — manifest entries use plain semver. tag="${GITHUB_REF_NAME}" version="${tag#v}" echo "version=${version}" >> "$GITHUB_OUTPUT" echo "tag=${tag}" >> "$GITHUB_OUTPUT"
          6. Migration safety checkpnpm tsx scripts/check-migrations.ts
          7. Run unit tests (scripts)pnpm test:scripts
          8. Build shared workspace packagespnpm --filter @line-crm/shared --filter @line-crm/line-sdk --filter @line-crm/db --filter @line-harness/update-engine --filter @line-harness/sdk build
          9. Run every workspace typecheckpnpm -r --workspace-concurrency=1 --if-present run typecheck
          10. Run every populated workspace test suitepnpm --filter @line-crm/db test pnpm --filter @line-harness/update-engine test pnpm --filter create-line-harness test pnpm --filter worker test pnpm --filter web test pnpm --filter liff test
          11. Capture build metadataecho "build_time=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT"
          12. Build Admin (Next.js static export)pnpm --filter web build
          13. Build LIFFpnpm --filter liff build
          14. Build Worker Assetspnpm --filter worker build
          15. Write CI wrangler config for artifact buildscat > apps/worker/wrangler.ci.toml <<'TOML' name = "line-harness" main = "src/index.ts" compatibility_date = "2024-12-01" compatibility_flags = ["nodejs_compat"] TOML
          16. Build Worker artifact (first pass — for identity hashing)pnpm --filter worker exec wrangler deploy --dry-run \ --config wrangler.ci.toml --outdir dist-release
          17. Compute hashes + inject _version.ts# Hash the wrangler-built single-file artifact. This value is # baked into _version.ts as the Worker's self-reported identity # (worker_hash); the final artifact's byte hash is computed # separately as worker_bundle_hash after the rebuild. json=$(pnpm tsx apps/worker/scripts/inject-version.ts \ --version "${{ steps.version.outputs.version }}" \ --worker apps/worker/dist-release/index.js \ --worker-assets apps/worker/dist/client \ --admin apps/web/out \ --liff apps/liff/dist \ --out apps/worker/…
          18. Rebuild Worker artifact (with embedded _version.ts)rm -rf apps/worker/dist-release pnpm --filter worker exec wrangler deploy --dry-run \ --config wrangler.ci.toml --outdir dist-release
          19. Compute worker_bundle_hash (detached hash of the final artifact)worker_bundle_hash=$(node -e "const{createHash}=require('node:crypto');const{readFileSync}=require('node:fs');console.log('sha256:'+createHash('sha256').update(readFileSync('apps/worker/dist-release/index.js')).digest('hex'))") echo "worker_bundle_hash=${worker_bundle_hash}" >> "$GITHUB_OUTPUT"
          20. Build release bundlemkdir -p dist pnpm tsx scripts/release/build-bundle.ts \ --worker-js apps/worker/dist-release/index.js \ --worker-assets apps/worker/dist/client \ --admin apps/web/out \ --liff apps/liff/dist \ --migrations packages/db/migrations \ --out dist/bundle.tar.gz
          21. Compute bundle sizesize=$(stat -c%s dist/bundle.tar.gz) echo "size=${size}" >> "$GITHUB_OUTPUT"
          22. Determine cumulative migrations for safe upgrades# Include the supported additive bridge from 037 onward. v0.14.1's # base+setup already establishes 001-036, while two older files use # destructive table rebuilds and must never be replayed statement by # statement. The checksum ledger makes this cumulative set safe. migrations_json=$(ls packages/db/migrations \ | awk '/^03[7-9]_|^0[4-9][0-9]_|^[1-9][0-9][0-9]_/' \ | jq -R . | jq -sc .) if [ -z "$migrations_json" ] || [ "$migrations_json" = "null" ]; then migrations_json="[]" fi echo "list=${m…
          23. Generate release-entry.jsonbundle_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${{ steps.version.outputs.tag }}/bundle.tar.gz" changelog_url="https://github.com/${GITHUB_REPOSITORY}/blob/${{ steps.version.outputs.tag }}/CHANGELOG.md" # min_from_version: 0.0.0 for the first release so the upgrade # flow accepts any prior install. Bump for breaking releases # that require a prior version. cat > release-entry.json <<JSON { "version": "${{ steps.version.outputs.version }}", "legacy_mileage_projection_versio…
          24. Fetch previous release-manifest.json (if any)# Pull the manifest from the most recent published release that # isn't this tag. If none exists, start fresh — update-manifest.ts # initialises { schema_version: 1, latest, releases: [] } on first # write. prev_tag=$(gh release list --limit 5 --json tagName --jq '.[].tagName' 2>/dev/null \ | grep -v "^${{ steps.version.outputs.tag }}$" \ | head -n 1 || true) if [ -n "$prev_tag" ]; then echo "Pulling manifest from previous release: $prev_tag" gh release download "$prev_tag" --pattern 'release-m…
          25. Update release-manifest.jsonpnpm tsx scripts/release/update-manifest.ts \ --manifest release-manifest.json \ --release release-entry.json cat release-manifest.json
          26. Publish draft GitHub Releasesoftprops/action-gh-release@v2
          Update from upstream · .github/workflows/update-from-upstream.yml ↗

          Triggers: workflow_dispatch, schedule

          update · no job dependencies declared

          Condition: github.repository != 'Shudesu/line-harness-oss'

          1. actions/checkout@v4actions/checkout@v4
          2. Configure gitgit config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          3. Fetch upstreamgit remote add upstream https://github.com/Shudesu/line-harness-oss.git git fetch upstream main
          4. Detect upstream changesif git merge-base --is-ancestor upstream/main HEAD; then echo "has_changes=false" >> "$GITHUB_OUTPUT" else echo "has_changes=true" >> "$GITHUB_OUTPUT" fi
          5. Create update branchbranch="upstream/update-$(date -u +%Y%m%d-%H%M%S)" git switch -c "$branch" echo "name=$branch" >> "$GITHUB_OUTPUT"Condition: steps.detect.outputs.has_changes == 'true'
          6. Merge upstream/maingit merge --no-edit upstream/mainCondition: steps.detect.outputs.has_changes == 'true'
          7. Push update branchgit push origin "${{ steps.branch.outputs.name }}"Condition: steps.detect.outputs.has_changes == 'true'
          8. Open pull requestgh pr create \ --base main \ --head "${{ steps.branch.outputs.name }}" \ --title "chore: update from upstream" \ --body "This PR imports the latest Shudesu/line-harness-oss main into this fork.\n\nMerge this PR to receive upstream fixes and features, then let the existing Cloudflare deploy workflows update your environment."Condition: steps.detect.outputs.has_changes == 'true'
          9. No upstream changesecho "Fork is already up to date with upstream/main."Condition: steps.detect.outputs.has_changes == 'false'
          Worker CI · .github/workflows/worker-ci.yml ↗

          Triggers: pull_request, push, workflow_dispatch

          worker-ci · no job dependencies declared

          1. actions/checkout@v4actions/checkout@v4
          2. pnpm/action-setup@v4pnpm/action-setup@v4
          3. actions/setup-node@v4actions/setup-node@v4
          4. Shell commandpnpm install --frozen-lockfile
          5. Shell commandpnpm --filter @line-crm/shared --filter @line-crm/line-sdk --filter @line-crm/db --filter @line-harness/update-engine build
          6. Shell commandpnpm --filter worker typecheck
          7. Shell commandpnpm --filter worker test
          8. Shell commandpnpm --filter worker build
          package.json ↗
          • build: pnpm -r build
          • deploy:worker: pnpm --filter worker deploy
          • deploy:web: pnpm --filter web build
          • deploy:setup: node packages/create-line-harness/dist/index.js
          • deploy:update: node packages/create-line-harness/dist/index.js update
          • build:bundle: tsx scripts/release/build-bundle.ts
          • build:wfp-bundle: tsx scripts/release/build-wfp-bundle.ts
          apps/liff/package.json ↗
          • build: tsc -b && vite build
          • deploy: pnpm build && wrangler pages deploy dist
          apps/web/package.json ↗
          • build: next build
          apps/worker/package.json ↗
          • build: vite build
          • deploy: vite build && wrangler deploy
          examples/plugins/tag-rules/package.json ↗
          • build: wrangler deploy --dry-run
          • deploy: wrangler deploy
          packages/create-line-harness/package.json ↗
          • build: tsup
          • prepublishOnly: node ./scripts/guard-source-publish.mjs
          packages/line-sdk/package.json ↗
          • build: tsc -p tsconfig.build.json
          • prepublishOnly: npm run build
          packages/plugin-template/package.json ↗
          • build: tsup src/index.ts --format esm --dts
          • build:mcp: tsup mcp-server/index.ts --format esm --out-dir dist-mcp
          • deploy: wrangler deploy
          packages/shared/package.json ↗
          • build: tsc -p tsconfig.build.json
          • prepublishOnly: npm run build
          packages/update-engine/package.json ↗
          • build: tsup && node scripts/preserve-node-builtins.mjs

          Full upstream document by @Shudesu · README.md · snapshot 5386460

          🌐 日本語 | English | 简体中文 | 한국어 | Español

          L Harness

          名称について: 2026年8月19日から製品表示名を「L Harness」に統一しました。 既存導入を壊さないため、リポジトリURL、create-line-harness、 @line-harness/*は互換識別子として維持します。詳細はBRAND.md。

          LINE で無料体験する 👈

          LINE 公式アカウントの完全オープンソース CRM。L社 / U社 の無料代替。 L Harness本体のソフトウェア利用料は 0円 です。LINE公式アカウントの配信料金、Cloudflareの契約・利用量、独自ドメイン、導入・保守、接続する外部AIサービスの費用は別です。Claude CodeからMCP経由で操作できます。

          費用の条件は開発元の公式料金説明で確認できます。Cloudflare D1の無料プランは日次の読み書き上限に達するとクエリがエラーになり、有料プランの超過課金とは挙動が異なります。外部AIを接続する場合は、たとえばClaudeのプラン・API料金を別に確認してください(2026年9月6日確認)。

          ▶️ 動画で見る (YouTube・約20分)

          クリックで YouTube を再生 — L Harness 導入の全手順

          現バージョン: v0.21.0 ・ MIT License ・ TypeScript / Cloudflare Workers + D1


          プラグインマーケット β

          その数行を、あなたのプラグインに。 配信ルール・自動処理・外部サービス連携を、本体とは別のリポジトリ・Workerへ切り出せます。本体更新による独自コードの上書きを避けながら、SDKを通じて機能を拡張します。

          管理画面の「プラグインマーケット」から拡張と開発リソースを探せます。β版は導入ガイド付きのカタログで、各プラグインを個別にセットアップする方式です。

          # 本体リポジトリのルートで実行(Node.js 22以上)
          pnpm plugin:create ../my-plugin
          

          独自ソースが上書きされないことと、API互換性は別です。本体・SDKを更新するときは、テスト環境で接続を確認してください。


          公式情報・検証資料

          L Harnessの表示名、開発者、運営法人、公開コード、研究資料の関係は以下を正本とします。各Researchサイトは開発元が運営する一次情報であり、独立した第三者レビューではありません。

          公式リンク 内容
          L Harness 公式製品ガイド 機能、料金、導入方法、更新情報を開発元が説明する製品ページ
          L Harness 公式エンティティ Harnessシリーズ内での製品名・開発者・運営法人・リポジトリの対応
          L Harness 公式別名ドメイン(JP) 正規製品ページへ恒久転送する公式の短縮・別名ドメイン
          L Harness 公式別名ドメイン(COM) 正規製品ページへ恒久転送する公式の短縮・別名ドメイン
          L Harness Cloud 開発元が構築・基盤運用を提供する公式マネージドサービス(L Harness OSSとは別エンティティ)
          L Harness Research 固定Gitコミットを根拠に、配信・Webhook・D1・権限・更新機構を検証した技術資料
          Research JSONカタログ 技術資料を機械可読なSchema.org DataCatalog形式で公開
          AI向け全文索引 研究本文、出典、検証手順、証明できない範囲をまとめた全文索引
          Harness Wiki — L Harness セットアップ、操作、更新、トラブル解決の公式ナレッジベース
          開発者・野田修一 Shudesu / @ai_shunodaと同一人物であることを示す公式プロフィール
          運営会社・AIエージェント株式会社 Harnessシリーズの運営法人

          なぜ L Harness?

          L社 U社 L Harness
          月額 2万円〜 1万円〜 0円(ソフトウェア利用料)
          ステップ配信 ✅ ✅ ✅
          セグメント配信 ✅ ✅ ✅
          リッチメニュー切替 ✅ ✅ ✅
          フォーム (LIFF) ✅ ✅ ✅
          スコアリング ✅ ❌ ✅
          IF-THEN 自動化 一部 一部 ✅
          API 公開 ❌ ❌ 全機能
          Claude Code (AI) 対応 ❌ ❌ MCP server 同梱
          BAN 検知 & 自動アカウント切替 ❌ ❌ ✅
          マルチアカウント 別契約 別契約 標準搭載
          友だち重複検出 ❌ ❌ ✅ (picture_url トークン照合)
          ソースコード 非公開 非公開 MIT (このリポ)

          クイックスタート

          1 コマンドで完全セットアップ

          npx create-line-harness
          

          CLI が以下を全部やる:

          • Cloudflare アカウント認証 (wrangler login)
          • D1 データベース作成 + スキーマ・マイグレーション適用
          • Worker / 管理画面のデプロイ
          • LINE 公式アカウントの credentials 登録
          • LIFF アプリの自動作成
          • 管理画面初回ログイン用 Owner ユーザー作成

          所要時間: 約 5 分。完了すれば管理画面 (https://<your-name>-admin.pages.dev) で即運用開始。

          必要なもの

          • Cloudflare アカウント(無料枠で OK)
          • LINE 公式アカウント + Messaging API channel
          • Node.js 22+ / pnpm

          主要機能

          配信

          • ステップ配信 — delay_minutes で分単位制御、条件分岐、ステルス送信
          • ブロードキャスト — 全員 / タグ / セグメント、即時 or 予約、500 人超は自動キュー化
          • リマインダー — 指定日時からのカウントダウン配信(セミナー 3 日前 / 1 日前 / 当日)
          • テンプレート — {{name}} {{uid}} {{auth_url:CHANNEL_ID}} で個別パーソナライズ
          • トラッキングリンク — クリック計測 → 自動タグ付け → シナリオ起動

          CRM

          • 友だち管理 — Webhook 自動登録、プロフィール取得、カスタムメタデータ
          • タグ — 配信条件・シナリオトリガー
          • スコアリング — 行動ベースのリードスコア自動計算
          • オペレーターチャット — 管理画面から直接 1:1 返信
          • Conversation Inbox — 未返信の会話を放置時間順で一覧(自動配信は除外判定)
          • 重複検出 — picture_url 中間トークンで複数アカウント間の同一ユーザーを自動タグ付け

          マーケティング

          • リッチメニュー — ユーザー別 / タグ別の自動切替
          • フォーム (LIFF) — LINE 内完結フォーム、回答 → メタデータ自動保存
          • カレンダー予約 — Google Calendar 連携の予約システム (LIFF)
          • ライブCTA即時予約 — ウェビナーのフォーム送信後、その場で空き日時を選び、Google Meet発行・LINEリマインドまで自動化
          • スタッフ管理 — Owner / Admin / Staff の 3 ロール、API key 個別発行

          アフィリエイト計測(ASP)

          • アフィリリンク発行 — アフィリエイター自身が LIFF からワンタップで発行、短縮ドメイン対応
          • 案件管理 — 案件ごとの固定額報酬を設定、複数案件を並列運用
          • 時系列トラッキング — クリック → 友だち追加 → CV を時系列で記録、last-touch 帰属で成果紐づけ
          • 成果承認フロー — 帰属 CV を承認/却下して報酬を確定、重複アカウント検知フラグで水増しを可視化
          • LINE push 通知 — 成果確定時にアフィリエイターへ自動プッシュ通知
          • 詳細: docs/wiki/27-Affiliate-ASP.md

          自動化

          • IF-THEN ルール — 7 種のトリガー × 6 種のアクション
          • 自動返信 — キーワード完全一致 / 部分一致
          • Webhook IN/OUT — Stripe / Slack 等の外部サービス連携
          • 通知ルール — 条件付きアラート配信
          • 配信タイミング — delay_minutes と scheduled_at で完全制御(v0.13.2 で時間ゲート全廃、運用側ハンドル)

          マルチアカウント

          • 複数 LINE 公式アカウント を 1 つのダッシュボードで管理
          • アカウント別シナリオ・タグ・配信 スコープ
          • BAN 検知 → 自動で次のアカウントへ友だち移行(pool 機能)
          • トラフィックプール — 複数アカウントへ自動振り分け

          AI 統合

          • MCP Server 同梱 (@line-harness/mcp-server) — Claude Code から自然言語で全操作
            • list_conversations / get_conversation — 未返信会話の AI 監視
            • create_scenario / update_step — シナリオを AI に作らせる
            • broadcast / send_message — メッセージ送信(要ユーザー確認)
          • 公式 SDK (@line-harness/sdk) — TypeScript の型付き SDK、ESM + CJS、ゼロ依存

          iOS アプリ対応

          • GET /api/capabilities — iOS 公式アプリ (the-harness-ios) との互換判定エンドポイント
          • Owner / Admin / Staff いずれのロールでも利用可能

          アーキテクチャ

          [ LINE Platform ] ⇄ [ Cloudflare Worker (Hono) ] ⇄ [ D1 SQLite ]
                                        ⇅
                              [ Cloudflare Pages (Next.js 15) ]
                                        ⇅
                              [ MCP Server / SDK / Claude Code ]
          
          • Worker (apps/worker): API + LIFF + Webhook 受信、cron で配信処理
          • Web (apps/web): Next.js 15 ダッシュボード(19 セクション)
          • Packages:
            • @line-harness/sdk — TypeScript SDK
            • @line-harness/mcp-server — Claude Code 用 MCP server
            • create-line-harness — セットアップ CLI
            • @line-harness/plugin-template — プラグイン拡張用テンプレート
            • @line-harness/db — D1 マイグレーション + ヘルパー
            • @line-harness/line-sdk — LINE API 薄ラッパー
            • @line-harness/shared — 型定義共有

          ドキュメント


          ライセンス

          MIT License. 商用利用・改変・再配布自由。


          コントリビュート

          Issue / PR 歓迎。OSS リポへの PR は Shudesu/line-harness-oss (このリポ) に投げてください。


          開発者 / Author

          野田修一(Shudesu) — Harness シリーズ(L Harness / IG Harness / X Harness)開発者、AIエージェント株式会社 代表


          L Harness by @Shudesu — AI ネイティブ時代の OSS LINE CRM

          Frequently asked about L Harness

          What is L Harness?+

          L Harness is a self-hosted L Message alternative built on the Cloudflare developer platform. Self-hosted LINE contact CRM, chat and messaging workflows

          What does L Harness replace?+

          L Harness is listed as an alternative to L Message. Compare the features and tradeoffs before migrating.

          What Cloudflare primitives does L Harness use?+

          L Harness is built on D1, Durable Objects, Pages, R2, Workers.

          How much does L Harness cost to run?+

          A small self-hosted CRM can fit the documented Cloudflare free allowances. A LINE Official Account and Messaging API channel are mandatory: The Japan Communication Plan includes up to 200 charge-counted messages/month at no monthly fee; higher-volume delivery incurs LINE plan fees. Optional Claude/AI services and custom domains are separate costs. Operate within the region-specific LINE messaging plan and current delivery allowance; Cloudflare free hosting does not make unlimited LINE sends free. Keep Worker/D1/R2/SQLite-DO use within free quotas and configure your own channel credentials, owner account and LIFF application. External AI is optional. Source and configuration review establishes a deployment path and conditional costs; this candidate was not executed or load-tested. Check current Cloudflare pricing before deploying.

          Is L Harness open source?+

          The upstream repository declares the MIT license. Read its terms at https://raw.githubusercontent.com/Shudesu/line-harness-oss/53864603502ae4ccdf350f39a71264b4b41155f2/LICENSE. Source code and contributor credit are available at https://github.com/Shudesu/line-harness-oss.

          Discussion · 0

          sign in to comment →
          No comments yet — be the first.