@tillstack/cli 0.3.0 → 0.3.2

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.
package/dist/index.js CHANGED
@@ -338,7 +338,13 @@ ${c.bold('TILLFORGE')} ${c.dim('(sovereign code hosting)')} ${c.gray('tilldev f
338
338
  ${c.bold('TILLAUTH')} ${c.dim('(auth)')} ${c.gray('tilldev auth …')}
339
339
  auth apps List auth apps
340
340
  auth apps create <name> [--slug <s>] Create an auth app
341
- auth app <appId> Show an auth app
341
+ auth app <appRef> Show an auth app (by slug or id)
342
+ auth invite <appRef> <email> [--role <r>] Invite a user to an app
343
+ auth clients <appRef> List OIDC clients registered on an app
344
+ auth clients <appRef> create <name> --redirect <uri>[,<uri>] [--scopes …] [--public] [--no-consent]
345
+ auth clients <appRef> rm <clientId> Remove an OIDC client
346
+ auth scim <appRef> [rotate] Mint/rotate the SCIM provisioning token (shown once)
347
+ auth scim <appRef> disable Turn SCIM provisioning off
342
348
 
343
349
  ${c.bold('WORKSPACE')}
344
350
  team List org members
@@ -757,11 +763,96 @@ async function cmdAuth(flags) {
757
763
  return;
758
764
  }
759
765
  case 'app': {
760
- const appId = need(flags.positional[2], 'tilldev auth app <appId>');
766
+ const appId = need(flags.positional[2], 'tilldev auth app <appRef>');
761
767
  const r = await api.getAuthApp(opts, appId);
762
768
  printJson(r);
763
769
  return;
764
770
  }
771
+ case 'invite': {
772
+ const appRef = need(flags.positional[2], 'tilldev auth invite <appRef> <email> [--role <r>]');
773
+ const email = need(flags.positional[3], 'tilldev auth invite <appRef> <email> [--role <r>]');
774
+ const role = flags.named['role']?.trim();
775
+ const r = await api.inviteAuthUser(opts, appRef, email, role);
776
+ render(flags, r, () => {
777
+ ok(`Invited ${c.bold(r.invite.email)} as ${c.dim(r.invite.role)}`);
778
+ if (r.delivered)
779
+ console.log(c.dim('An invitation email is on its way.'));
780
+ else if (r.accept_link)
781
+ console.log(`Share this link:\n ${c.bold(r.accept_link)}`);
782
+ });
783
+ return;
784
+ }
785
+ case 'clients': {
786
+ const appRef = need(flags.positional[2], 'tilldev auth clients <appRef> [create|rm]');
787
+ const action = flags.positional[3];
788
+ if (action === 'create') {
789
+ const name = need(flags.positional[4], 'tilldev auth clients <appRef> create <name> --redirect <uri>[,<uri>…] [--scopes openid,email,profile] [--public] [--no-consent]');
790
+ // Comma-separate multiple redirect URIs in a single --redirect flag.
791
+ const redirect_uris = (flags.named['redirect'] ?? '').split(',').map((s) => s.trim()).filter(Boolean);
792
+ if (redirect_uris.length === 0)
793
+ throw new Error('At least one --redirect <uri> is required');
794
+ const scopes = flags.named['scopes']?.split(',').map((s) => s.trim()).filter(Boolean);
795
+ const body = {
796
+ name,
797
+ redirect_uris,
798
+ ...(scopes ? { scopes } : {}),
799
+ ...(flags.bool.has('public') ? { public: true } : {}),
800
+ ...(flags.bool.has('no-consent') ? { require_consent: false } : {}),
801
+ };
802
+ const r = await api.createOidcClient(opts, appRef, body);
803
+ render(flags, r, () => {
804
+ ok(`Registered OIDC client ${c.bold(r.client_id)} (${c.dim(r.client_type)})`);
805
+ console.log(kv([
806
+ ['client_id', r.client_id],
807
+ ['redirect_uris', r.redirect_uris.join(', ')],
808
+ ['scopes', r.scopes.join(' ')],
809
+ ]));
810
+ if (r.client_secret) {
811
+ console.log(`\n${c.yellow('client_secret (shown once — store it now):')}\n ${c.bold(r.client_secret)}`);
812
+ }
813
+ else {
814
+ console.log(c.dim('\nPublic client — no secret; authenticate with PKCE (S256).'));
815
+ }
816
+ });
817
+ return;
818
+ }
819
+ if (action === 'rm' || action === 'delete') {
820
+ const clientId = need(flags.positional[4], 'tilldev auth clients <appRef> rm <clientId>');
821
+ const r = await api.deleteOidcClient(opts, appRef, clientId);
822
+ render(flags, r, () => ok(`Removed client ${c.dim(clientId)}`));
823
+ return;
824
+ }
825
+ const r = await api.listOidcClients(opts, appRef);
826
+ render(flags, r, () => {
827
+ const cols = [
828
+ { header: 'CLIENT ID', get: (x) => c.bold(x.client_id) },
829
+ { header: 'NAME', get: (x) => x.name },
830
+ { header: 'TYPE', get: (x) => c.dim(x.client_type) },
831
+ { header: 'SCOPES', get: (x) => (x.scopes ?? []).join(' ') },
832
+ { header: 'ENABLED', get: (x) => (x.enabled ? c.green('yes') : c.red('no')) },
833
+ ];
834
+ console.log(table(r.clients, cols));
835
+ });
836
+ return;
837
+ }
838
+ case 'scim': {
839
+ const appRef = need(flags.positional[2], 'tilldev auth scim <appRef> [rotate|disable]');
840
+ const action = flags.positional[3] ?? 'rotate';
841
+ if (action === 'disable') {
842
+ const r = await api.scimDisable(opts, appRef);
843
+ render(flags, r, () => ok('SCIM provisioning disabled for this app.'));
844
+ return;
845
+ }
846
+ // rotate (also the enable path — mints a fresh token, shown once)
847
+ const r = await api.scimRotate(opts, appRef);
848
+ render(flags, r, () => {
849
+ ok('SCIM provisioning token minted.');
850
+ console.log(kv([['scim_base_url', r.scim_base_url]]));
851
+ console.log(`\n${c.yellow('bearer token (shown once — store it now):')}\n ${c.bold(r.token)}`);
852
+ console.log(c.dim('\nConfigure your IdP (Okta, Entra ID, …) with the base URL + this token.'));
853
+ });
854
+ return;
855
+ }
765
856
  default:
766
857
  throw new Error(`unknown: tilldev auth ${sub}`);
767
858
  }