@krx3d/tizentube2 1.30.20 → 1.30.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,8 +9,8 @@
9
9
  # - dist/userScript.js (browser userscript, bundled & minified)
10
10
  # - dist/service.js (DIAL service, bundled with Rollup)
11
11
  # - Publishes the package to https://www.npmjs.com on release only
12
- # - Automatically removes old versions from npm (within 72-hour window)
13
- # - Deprecates older versions (beyond 72-hour window) if any remain
12
+ # - Deprecates older versions once the new one is confirmed live on npm
13
+ # - Fails the run if npm's `latest` tag does not match what it published
14
14
  # - Purges jsDelivr CDN cache after publishing
15
15
  #
16
16
  # WHEN IT RUNS
@@ -30,14 +30,25 @@
30
30
  #
31
31
  # AUTOMATIC VERSION CLEANUP
32
32
  # -------------------------
33
- # After publishing a new version, this workflow automatically:
34
- # - Identifies all versions except the newly published one
35
- # - Unpublishes versions that are ≤72 hours old (npm policy allows this)
36
- # - Deprecates versions >72 hours old (unpublish no longer allowed)
37
- # - Result: Only the latest version remains active on npm
33
+ # After publishing, this workflow tidies up older versions — but only once the
34
+ # new version is actually readable on npm, and only with writes that change
35
+ # something. Both rules exist because of a real failure:
38
36
  #
39
- # Note: npm allows unpublishing versions within ~72 hours of publication.
40
- # After that window, versions can only be deprecated, not removed.
37
+ # - npm publish is asynchronous. The CLI exits 0 while the version is still
38
+ # being processed, and 1.30.30 only appeared in the registry 96 seconds later.
39
+ # - Unpublish and deprecate both work by rewriting the ENTIRE package document.
40
+ # A rewrite built from a snapshot taken before the new version landed deletes
41
+ # it again — which is how 1.30.30 vanished one second after it appeared,
42
+ # leaving `latest` at 1.30.20 while the GitHub release said 1.30.30.
43
+ #
44
+ # So:
45
+ # - Nothing touches the registry until GET /<pkg>/<version> succeeds
46
+ # - Versions that already carry a deprecation message are skipped entirely
47
+ # - The current version and every dist-tag target are never touched
48
+ # - Removal is attempted every run; npm rejects it from a granular token that
49
+ # bypasses 2FA, so those versions are deprecated instead and a local script
50
+ # (scripts/npm-cleanup) does the real removal with a 2FA code
51
+ # - The job fails if `latest` does not end up pointing at what it published
41
52
  #
42
53
  # VERSION BUMP RULE (push to main only)
43
54
  # -------------------------------------
@@ -144,6 +155,13 @@ env:
144
155
  PACKAGE_NAME_GH: 'KrX3D/TizenTube'
145
156
  NODE_VERSION: '24'
146
157
  UNPUBLISH_WINDOW_HOURS: '72'
158
+ # Removal is still attempted on every run. npm currently refuses it from a
159
+ # granular access token with bypass-2FA (its only token type since November
160
+ # 2025), so the attempts 403 and the versions are deprecated instead — but a
161
+ # refused request writes nothing, and if npm allows this again for
162
+ # automation, removal resumes on the next run with no change here.
163
+ # Set to 'false' only to stop attempting it altogether.
164
+ ALLOW_UNPUBLISH: 'true'
147
165
 
148
166
  jobs:
149
167
  build-publish-clean:
@@ -414,63 +432,137 @@ jobs:
414
432
  echo "Publishing package..."
415
433
  npm publish --access public
416
434
 
417
- - name: Wait for npm propagation (after publish)
435
+ # --------------------
436
+ # CONFIRM THE PUBLISH LANDED
437
+ # --------------------
438
+ # npm's publish endpoint is asynchronous. The CLI prints "Your package is
439
+ # being processed and may take a few minutes to become available" and exits
440
+ # 0 well before the version exists in the registry document.
441
+ #
442
+ # That is not a cosmetic delay, because every cleanup step below works by
443
+ # PUTting the WHOLE package document back. npm builds that document from
444
+ # whatever it can read at the time, so a PUT issued while the new version
445
+ # is still in flight writes a snapshot that does not contain it — and the
446
+ # version that was just published is gone again.
447
+ #
448
+ # 1.30.30 is what that looks like: published 18:40:16, appeared in the
449
+ # registry's own time map at 18:41:52, erased at 18:41:53 by the last
450
+ # npm deprecate of the very same job. The GitHub release said 1.30.30
451
+ # while the latest tag pointed back at 1.30.20, so every TV loading the
452
+ # userscript from the CDN got a bundle one version behind its own app.
453
+ #
454
+ # So nothing touches the registry until the new version is readable, and if
455
+ # it never becomes readable the job fails instead of cleaning up around a
456
+ # version that isn't there.
457
+ - name: Confirm the published version is live on npm
418
458
  if: steps.publish_npm.outcome == 'success'
459
+ id: confirm_publish
460
+ env:
461
+ PKG: ${{ env.PACKAGE_NAME }}
419
462
  run: |
420
- echo "Waiting for npm propagation..."
421
- sleep 10
463
+ set -euo pipefail
464
+ VER=$(node -e "console.log(require('./package.json').version)")
465
+ echo "$VER" > /tmp/current_version.txt
466
+ echo "Waiting for ${PKG}@${VER} to become readable..."
467
+ for i in $(seq 1 40); do
468
+ # The registry is read directly rather than through npm view, which
469
+ # has its own HTTP cache and will happily answer from a packument
470
+ # fetched before the publish landed.
471
+ if curl -fsSL "https://registry.npmjs.org/${PKG}/${VER}" -o /tmp/published.json; then
472
+ echo "${PKG}@${VER} is live (attempt ${i})."
473
+ echo "confirmed=true" >> "$GITHUB_OUTPUT"
474
+ exit 0
475
+ fi
476
+ sleep 15
477
+ done
478
+ echo "confirmed=false" >> "$GITHUB_OUTPUT"
479
+ echo "::error::${PKG}@${VER} never became readable on npm after 10 minutes. Cleanup is skipped so nothing can overwrite it."
480
+ exit 1
422
481
 
423
482
  # --------------------
424
- # FETCH VERSIONS
483
+ # FETCH THE REGISTRY DOCUMENT
425
484
  # --------------------
426
- - name: Gather versions and publish times
427
- if: steps.publish_npm.outcome == 'success'
485
+ - name: Gather versions, times and deprecation state
486
+ if: steps.confirm_publish.outputs.confirmed == 'true'
428
487
  env:
429
488
  PKG: ${{ env.PACKAGE_NAME }}
430
489
  run: |
431
490
  set -euo pipefail
432
- echo "Fetching versions & times for ${PKG} ..."
433
- npm view "${PKG}" versions --json > /tmp/versions.json
434
- npm view "${PKG}" time --json > /tmp/time.json
435
-
436
- CUR_VER=$(node -e "console.log(require('./package.json').version)")
437
- echo "Current repo version: $CUR_VER"
438
- echo "$CUR_VER" > /tmp/current_version.txt
491
+ echo "Fetching the registry document for ${PKG} ..."
492
+ curl -fsSL "https://registry.npmjs.org/${PKG}" -o /tmp/packument.json
493
+ node -e '
494
+ const fs = require("fs");
495
+ const doc = JSON.parse(fs.readFileSync("/tmp/packument.json","utf8"));
496
+ const versions = Object.keys(doc.versions || {});
497
+ const deprecated = versions.filter(function (v) { return doc.versions[v].deprecated; });
498
+ fs.writeFileSync("/tmp/versions.json", JSON.stringify(versions));
499
+ fs.writeFileSync("/tmp/time.json", JSON.stringify(doc.time || {}));
500
+ fs.writeFileSync("/tmp/deprecated.json", JSON.stringify(deprecated));
501
+ fs.writeFileSync("/tmp/dist_tags.json", JSON.stringify(doc["dist-tags"] || {}));
502
+ console.log("Live versions: " + versions.length + ", already deprecated: " + deprecated.length);
503
+ console.log("dist-tags: " + JSON.stringify(doc["dist-tags"] || {}));
504
+ '
505
+ echo "Current repo version: $(cat /tmp/current_version.txt)"
439
506
 
440
507
  # --------------------
441
508
  # DECIDE WHAT TO REMOVE
442
509
  # --------------------
510
+ # Two rules, both about never issuing a registry write that changes
511
+ # nothing — since each one rewrites the whole package document, and one of
512
+ # those rewrites is what erased 1.30.30:
513
+ #
514
+ # * a version that already carries a deprecation message is skipped. All
515
+ # 80 live versions were already deprecated, so the previous run made
516
+ # about 80 full-document writes for no effect at all.
517
+ #
518
+ # * unpublishing is off unless ALLOW_UNPUBLISH is "true". The npm token
519
+ # here is a granular access token, and npm refuses unpublish from tokens
520
+ # that bypass two-factor authentication ("Granular access tokens that
521
+ # bypass two-factor authentication may not perform this action"). Every
522
+ # attempt returned 403 — and every one of those was still a write.
523
+ # Re-enable it only with a token that can actually perform it.
524
+ #
525
+ # The version just published is kept, as before, and so is whatever each
526
+ # dist-tag resolves to: installing the tag npm points people at must not
527
+ # print a deprecation warning. The latest tag was itself deprecated by the
528
+ # run that lost 1.30.30.
443
529
  - name: Decide versions to unpublish / deprecate
444
- if: steps.publish_npm.outcome == 'success'
530
+ if: steps.confirm_publish.outputs.confirmed == 'true'
445
531
  env:
446
532
  UNPUBLISH_WINDOW_HOURS: ${{ env.UNPUBLISH_WINDOW_HOURS }}
533
+ ALLOW_UNPUBLISH: ${{ env.ALLOW_UNPUBLISH }}
447
534
  run: |
448
535
  set -euo pipefail
449
536
  node -e '
450
537
  const fs = require("fs");
451
538
  const versions = JSON.parse(fs.readFileSync("/tmp/versions.json","utf8")||"[]");
452
539
  const times = JSON.parse(fs.readFileSync("/tmp/time.json","utf8")||"{}");
540
+ const deprecated = new Set(JSON.parse(fs.readFileSync("/tmp/deprecated.json","utf8")||"[]"));
541
+ const distTags = JSON.parse(fs.readFileSync("/tmp/dist_tags.json","utf8")||"{}");
453
542
  const current = fs.readFileSync("/tmp/current_version.txt","utf8").trim();
454
543
 
455
544
  const windowHours = Number(process.env.UNPUBLISH_WINDOW_HOURS || 72);
545
+ const allowUnpublish = String(process.env.ALLOW_UNPUBLISH || "") === "true";
456
546
  const now = Date.now();
457
547
 
548
+ const keep = new Set([current].concat(Object.keys(distTags).map(function (tag) { return distTags[tag]; })));
549
+
458
550
  const toUnpublish = [];
459
551
  const toDeprecate = [];
460
552
 
461
553
  for (const v of versions) {
462
- if (v === current) continue;
554
+ if (keep.has(v)) continue;
463
555
  const t = times[v] ? new Date(times[v]) : null;
464
- if (!t) { toDeprecate.push(v); continue; }
465
- const ageHours = (now - t.getTime()) / 36e5;
466
- if (ageHours <= windowHours) toUnpublish.push(v);
467
- else toDeprecate.push(v);
556
+ const ageHours = t ? (now - t.getTime()) / 36e5 : Infinity;
557
+ if (allowUnpublish && t && ageHours <= windowHours) { toUnpublish.push(v); continue; }
558
+ if (!deprecated.has(v)) toDeprecate.push(v);
468
559
  }
469
560
 
470
561
  fs.writeFileSync("/tmp/to_unpublish.txt", toUnpublish.length ? toUnpublish.join("\n") + "\n" : "");
471
562
  fs.writeFileSync("/tmp/to_deprecate.txt", toDeprecate.length ? toDeprecate.join("\n") + "\n" : "");
472
- console.log("To unpublish (<= " + windowHours + "h):", toUnpublish);
473
- console.log("To deprecate (> " + windowHours + "h + unknown):", toDeprecate);
563
+ console.log("Keeping (current version + every dist-tag): " + Array.from(keep).join(", "));
564
+ console.log("To unpublish (<= " + windowHours + "h, enabled=" + allowUnpublish + "):", toUnpublish);
565
+ console.log("To deprecate (not already deprecated):", toDeprecate);
474
566
  '
475
567
 
476
568
  echo ""
@@ -484,8 +576,30 @@ jobs:
484
576
  # --------------------
485
577
  # UNPUBLISH
486
578
  # --------------------
579
+ # Unpublishing is the only thing that actually removes a version;
580
+ # deprecating just labels it. So this still tries, every run, for every
581
+ # version inside npm's 72-hour window.
582
+ #
583
+ # It cannot currently succeed. npm removed legacy tokens in November 2025,
584
+ # leaving granular access tokens as the only kind, and since August 2026 a
585
+ # granular token with "bypass 2FA" is refused for sensitive actions, with
586
+ # unpublish among them — the 403 says so in as many words. Deprecating
587
+ # still works with the same token.
588
+ #
589
+ # Attempting anyway is deliberate and costs nothing. A refused PUT writes
590
+ # nothing, so those 403s never touched the package document; the version
591
+ # that got erased was erased by a deprecate that SUCCEEDED. And if npm
592
+ # restores this for automation, removal resumes with no change here.
593
+ #
594
+ # What is not repeated is the noise. The 2FA refusal is reported once and
595
+ # the remaining versions go straight to the deprecate fallback, instead of
596
+ # making the same doomed request nineteen more times.
597
+ #
598
+ # To actually remove versions today, run
599
+ # scripts/npm-cleanup/unpublish-old-versions.ps1 locally; it prompts for a
600
+ # 2FA code, which is the only path npm still allows.
487
601
  - name: Unpublish old versions (verified) and fallback to deprecate
488
- if: steps.publish_npm.outcome == 'success'
602
+ if: steps.confirm_publish.outputs.confirmed == 'true'
489
603
  env:
490
604
  PKG: ${{ env.PACKAGE_NAME }}
491
605
  run: |
@@ -510,6 +624,29 @@ jobs:
510
624
  return 1
511
625
  }
512
626
 
627
+ # Deprecating something that is already deprecated is rejected as 422,
628
+ # and gets there by rewriting the whole package document — the write
629
+ # that erased 1.30.30. So the fallback checks first.
630
+ already_deprecated(){
631
+ node -e '
632
+ const fs = require("fs");
633
+ let list = [];
634
+ try { list = JSON.parse(fs.readFileSync("/tmp/deprecated.json","utf8")); } catch (e) { }
635
+ process.exit(list.indexOf(process.argv[1]) === -1 ? 1 : 0);
636
+ ' "$1"
637
+ }
638
+
639
+ deprecate_fallback(){
640
+ ver="$1"
641
+ if already_deprecated "$ver"; then
642
+ echo "${PKG}@${ver} is already deprecated; leaving it alone."
643
+ return 0
644
+ fi
645
+ npm deprecate "${PKG}@${ver}" "Deprecated — please upgrade to a newer release." 2>&1 | tee /tmp/npm_deprecate_${ver}.log || echo "::warning::deprecate failed for ${ver}"
646
+ }
647
+
648
+ UNPUBLISH_BLOCKED=0
649
+
513
650
  if [ ! -s /tmp/to_unpublish.txt ]; then
514
651
  echo "No versions to unpublish."
515
652
  else
@@ -517,6 +654,11 @@ jobs:
517
654
  ver="$(echo "$ver" | tr -d '\r' | xargs)"
518
655
  [ -z "$ver" ] && continue
519
656
 
657
+ if [ "$UNPUBLISH_BLOCKED" = "1" ]; then
658
+ deprecate_fallback "$ver"
659
+ continue
660
+ fi
661
+
520
662
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
521
663
  echo "Attempting unpublish: ${PKG}@${ver}"
522
664
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -527,12 +669,17 @@ jobs:
527
669
  echo "Verified ${PKG}@${ver} removed."
528
670
  else
529
671
  echo "::warning::Still visible after unpublish attempts -> deprecate fallback"
530
- npm deprecate "${PKG}@${ver}" "Deprecated — please upgrade to a newer release." 2>&1 | tee /tmp/npm_deprecate_${ver}.log || echo "::warning::deprecate failed for ${ver}"
672
+ deprecate_fallback "$ver"
531
673
  fi
532
674
  else
533
- echo "::warning::npm unpublish failed or returned non-zero for ${PKG}@${ver}"
534
- echo "Attempting to deprecate as fallback..."
535
- npm deprecate "${PKG}@${ver}" "Deprecated please upgrade to a newer release." 2>&1 | tee /tmp/npm_deprecate_${ver}.log || echo "::warning::deprecate failed for ${ver}"
675
+ if grep -q "bypass two-factor authentication" "/tmp/npm_unpublish_${ver}.log"; then
676
+ UNPUBLISH_BLOCKED=1
677
+ echo "::warning::npm refuses unpublish from this token: a granular access token with bypass-2FA may not perform it (npm policy, August 2026). The remaining versions would fail identically, so they are deprecated instead. To remove versions, run scripts/npm-cleanup/unpublish-old-versions.ps1 locally."
678
+ else
679
+ echo "::warning::npm unpublish failed or returned non-zero for ${PKG}@${ver}"
680
+ fi
681
+ echo "Deprecating instead..."
682
+ deprecate_fallback "$ver"
536
683
  fi
537
684
 
538
685
  done < /tmp/to_unpublish.txt
@@ -542,7 +689,7 @@ jobs:
542
689
  # DEPRECATE older (>72h)
543
690
  # --------------------
544
691
  - name: Deprecate older versions
545
- if: steps.publish_npm.outcome == 'success'
692
+ if: steps.confirm_publish.outputs.confirmed == 'true'
546
693
  env:
547
694
  PKG: ${{ env.PACKAGE_NAME }}
548
695
  run: |
@@ -563,13 +710,18 @@ jobs:
563
710
  # Final wait + verify final state
564
711
  # --------------------
565
712
  - name: Wait before final verify
566
- if: steps.publish_npm.outcome == 'success'
713
+ if: steps.confirm_publish.outputs.confirmed == 'true'
567
714
  run: |
568
715
  echo "Short wait to let registry propagate final state..."
569
716
  sleep 8
570
717
 
718
+ # The cleanup steps above rewrite the whole package document, so the last
719
+ # thing this job does is check that the version it published is still the
720
+ # one npm hands out. Without this the job reported success while `latest`
721
+ # had silently fallen back a release, and the first sign of it was a TV
722
+ # loading a userscript older than its own app.
571
723
  - name: Verify final state
572
- if: steps.publish_npm.outcome == 'success'
724
+ if: steps.confirm_publish.outputs.confirmed == 'true'
573
725
  env:
574
726
  PKG: ${{ env.PACKAGE_NAME }}
575
727
  run: |
@@ -577,14 +729,31 @@ jobs:
577
729
  echo "Final versions on npm:"
578
730
  npm view "${PKG}" versions --json
579
731
  echo ""
580
- echo "Latest version:"
581
- npm view "${PKG}" version
732
+
733
+ # Read through the registry rather than `npm view`, whose cache is
734
+ # what made the original breakage invisible here.
735
+ curl -fsSL "https://registry.npmjs.org/${PKG}" -o /tmp/packument_final.json
736
+ node -e '
737
+ const fs = require("fs");
738
+ const doc = JSON.parse(fs.readFileSync("/tmp/packument_final.json","utf8"));
739
+ const want = fs.readFileSync("/tmp/current_version.txt","utf8").trim();
740
+ const latest = (doc["dist-tags"] || {}).latest;
741
+ const live = !!(doc.versions || {})[want];
742
+ const deprecated = live && !!doc.versions[want].deprecated;
743
+ console.log("published: " + want + ", latest tag: " + latest + ", still live: " + live);
744
+ let failed = false;
745
+ if (!live) { console.log("::error::" + want + " is no longer on npm. A cleanup write removed the version this run published."); failed = true; }
746
+ if (latest !== want) { console.log("::error::npm latest is " + latest + ", not " + want + ". The CDN will serve the wrong bundle to every TV."); failed = true; }
747
+ if (deprecated) { console.log("::error::" + want + " was published deprecated."); failed = true; }
748
+ if (failed) process.exit(1);
749
+ console.log("npm latest matches the version this run published.");
750
+ '
582
751
 
583
752
  # --------------------
584
753
  # Purge jsDelivr cache
585
754
  # --------------------
586
755
  - name: Generate jsDelivr URLs to purge
587
- if: steps.publish_npm.outcome == 'success'
756
+ if: steps.confirm_publish.outputs.confirmed == 'true'
588
757
  id: cdn_urls
589
758
  run: |
590
759
  set -euo pipefail
@@ -636,7 +805,7 @@ jobs:
636
805
  printf '%s\n' "${URLS[@]}"
637
806
 
638
807
  - name: Purge jsDelivr cache
639
- if: steps.publish_npm.outcome == 'success'
808
+ if: steps.confirm_publish.outputs.confirmed == 'true'
640
809
  # jsDelivr rate-limits how often the same path can be re-purged
641
810
  # ("Purging request ... was throttled") — expected on frequent
642
811
  # releases, not an actual failure of the publish itself. The next
@@ -652,7 +821,7 @@ jobs:
652
821
  attempts: 3
653
822
 
654
823
  - name: Purge individual URLs (fallback if batch fails)
655
- if: steps.publish_npm.outcome == 'success' && failure()
824
+ if: steps.confirm_publish.outputs.confirmed == 'true' && failure()
656
825
  continue-on-error: true
657
826
  run: |
658
827
  set +e
@@ -698,6 +867,6 @@ jobs:
698
867
  echo "Individual purge attempts complete"
699
868
 
700
869
  - name: Cache purge complete
701
- if: steps.publish_npm.outcome == 'success'
870
+ if: steps.confirm_publish.outputs.confirmed == 'true'
702
871
 
703
872
  run: echo "✅ jsDelivr cache purge complete (npm + GitHub)"
package/AGENTS.md CHANGED
@@ -102,6 +102,9 @@ standalone/ # standalone installable app — see "Standalone mo
102
102
 
103
103
  scripts/tampermonkey/ # local Chrome-based dev/test loader — see README.md
104
104
  scripts/log-receiver/ # PC-side PS1 receiver for logServer.js's remote logging (see Feature map)
105
+ scripts/npm-cleanup/ # local `npm unpublish` of old versions, with a 2FA code — the publish
106
+ # workflow attempts removal too, but npm refuses it from any token it
107
+ # still issues, so this is the only path that works
105
108
  scripts/pc-installer/ # PC-side install/update script (SDB from the PC, no on-device installer app
106
109
  # needed) — workaround for the Host PC IP 127.0.0.1 conflict with standalone's
107
110
  # proxy path; in progress, see AGENTS.md "Known unresolved issues"